Skip to content
This repository was archived by the owner on Mar 25, 2021. It is now read-only.

Commit d52d587

Browse files
ajafffadidahiya
authored andcommitted
semicolon: split walker implementation, make "never" spec compliant (#2655)
Separate the implementations of "never" and "always", because "never" became a little more complex. [enhancement] `semicolon`: option `"never"` is now spec compliant [bugfix] `semicolon`: don't warn about unnecesary semicolon when it is actually needed, e.g. when followed by type assertion or template string Ref: microsoft/TypeScript#15444
1 parent dcb400b commit d52d587

File tree

5 files changed

+360
-110
lines changed

5 files changed

+360
-110
lines changed

src/rules/semicolonRule.ts

Lines changed: 202 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ const OPTION_IGNORE_BOUND_CLASS_METHODS = "ignore-bound-class-methods";
2626
const OPTION_IGNORE_INTERFACES = "ignore-interfaces";
2727

2828
interface Options {
29-
always: boolean;
3029
boundClassMethods: boolean;
3130
interfaces: boolean;
3231
}
@@ -78,100 +77,89 @@ export class Rule extends Lint.Rules.AbstractRule {
7877

7978
public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
8079
const options: Options = {
81-
always: this.ruleArguments.indexOf(OPTION_NEVER) === -1,
8280
boundClassMethods: this.ruleArguments.indexOf(OPTION_IGNORE_BOUND_CLASS_METHODS) === -1,
8381
interfaces: this.ruleArguments.indexOf(OPTION_IGNORE_INTERFACES) === -1,
8482
};
85-
return this.applyWithWalker(new SemicolonWalker(sourceFile, this.ruleName, options));
83+
const Walker = this.ruleArguments.indexOf(OPTION_NEVER) === -1 ? SemicolonAlwaysWalker : SemicolonNeverWalker;
84+
return this.applyWithWalker(new Walker(sourceFile, this.ruleName, options));
8685
}
8786
}
8887

89-
class SemicolonWalker extends Lint.AbstractWalker<Options> {
90-
private scanner?: ts.Scanner = undefined;
88+
abstract class SemicolonWalker extends Lint.AbstractWalker<Options> {
9189
public walk(sourceFile: ts.SourceFile) {
9290
const cb = (node: ts.Node): void => {
93-
switch (node.kind) {
94-
case ts.SyntaxKind.VariableStatement:
95-
case ts.SyntaxKind.ExpressionStatement:
96-
case ts.SyntaxKind.ReturnStatement:
97-
case ts.SyntaxKind.BreakStatement:
98-
case ts.SyntaxKind.ContinueStatement:
99-
case ts.SyntaxKind.ThrowStatement:
100-
case ts.SyntaxKind.ImportEqualsDeclaration:
101-
case ts.SyntaxKind.DoStatement:
102-
case ts.SyntaxKind.ExportAssignment:
103-
this.checkSemicolonAt(node as ts.Statement);
104-
break;
105-
case ts.SyntaxKind.TypeAliasDeclaration:
106-
case ts.SyntaxKind.ImportDeclaration:
107-
case ts.SyntaxKind.ExportDeclaration:
108-
case ts.SyntaxKind.DebuggerStatement:
109-
this.checkSemicolonOrLineBreak(node);
110-
break;
111-
case ts.SyntaxKind.ModuleDeclaration:
112-
// shorthand module declaration
113-
if ((node as ts.ModuleDeclaration).body === undefined) {
114-
this.checkSemicolonOrLineBreak(node);
115-
}
116-
break;
117-
case ts.SyntaxKind.PropertyDeclaration:
118-
this.visitPropertyDeclaration(node as ts.PropertyDeclaration);
119-
break;
120-
case ts.SyntaxKind.MethodDeclaration:
121-
case ts.SyntaxKind.FunctionDeclaration:
122-
if ((node as ts.FunctionLikeDeclaration).body === undefined) {
123-
this.checkSemicolonOrLineBreak(node);
124-
}
125-
break;
126-
case ts.SyntaxKind.InterfaceDeclaration:
127-
if (this.options.interfaces) {
128-
this.checkInterface(node as ts.InterfaceDeclaration);
129-
}
130-
break;
131-
case ts.SyntaxKind.SemicolonClassElement:
132-
return this.reportUnnecessary(node.end - 1);
133-
case ts.SyntaxKind.EmptyStatement:
134-
return this.checkEmptyStatement(node);
135-
default:
136-
}
91+
this.visitNode(node);
13792
return ts.forEachChild(node, cb);
13893
};
13994
return ts.forEachChild(sourceFile, cb);
14095
}
14196

142-
private visitPropertyDeclaration(node: ts.PropertyDeclaration) {
143-
// check if this is a multi-line arrow function
144-
if (node.initializer !== undefined &&
145-
node.initializer.kind === ts.SyntaxKind.ArrowFunction &&
146-
!utils.isSameLine(this.sourceFile, node.getStart(this.sourceFile), node.end)) {
147-
if (this.options.boundClassMethods) {
148-
if (this.sourceFile.text[node.end - 1] === ";" &&
149-
this.isFollowedByLineBreak(node.end)) {
150-
this.reportUnnecessary(node.end - 1);
151-
}
152-
}
153-
} else {
154-
this.checkSemicolonOrLineBreak(node);
97+
protected visitNode(node: ts.Node) {
98+
switch (node.kind) {
99+
case ts.SyntaxKind.SemicolonClassElement:
100+
return this.reportUnnecessary(node.end);
101+
case ts.SyntaxKind.EmptyStatement:
102+
return this.checkEmptyStatement(node);
103+
case ts.SyntaxKind.PropertyDeclaration:
104+
return this.visitPropertyDeclaration(node as ts.PropertyDeclaration);
155105
}
156106
}
157107

158-
private isFollowedByLineBreak(pos: number) {
159-
const scanner = this.scanner !== undefined ? this.scanner :
160-
(this.scanner = ts.createScanner(this.sourceFile.languageVersion, true, this.sourceFile.languageVariant, this.sourceFile.text));
161-
scanner.setTextPos(pos);
162-
return scanner.scan() === ts.SyntaxKind.EndOfFileToken || scanner.hasPrecedingLineBreak();
108+
protected reportUnnecessary(pos: number, noFix = false) {
109+
this.addFailure(pos - 1, pos, Rule.FAILURE_STRING_UNNECESSARY, noFix ? undefined : Lint.Replacement.deleteText(pos - 1, 1));
163110
}
164111

165-
private checkSemicolonOrLineBreak(node: ts.Node) {
166-
const hasSemicolon = this.sourceFile.text[node.end - 1] === ";";
167-
if (this.options.always && !hasSemicolon) {
168-
this.reportMissing(node.end);
169-
} else if (!this.options.always && hasSemicolon && this.isFollowedByLineBreak(node.end)) {
170-
// semicolon can be removed if followed by line break;
171-
this.reportUnnecessary(node.end - 1);
112+
protected checkSemicolonOrLineBreak(node: ts.Node) {
113+
if (this.sourceFile.text[node.end - 1] !== ";") {
114+
return;
115+
}
116+
const nextToken = utils.getNextToken(node, this.sourceFile)!;
117+
switch (nextToken.kind) {
118+
case ts.SyntaxKind.EndOfFileToken:
119+
case ts.SyntaxKind.CloseBraceToken:
120+
return this.reportUnnecessary(node.end);
121+
default:
122+
if (!utils.isSameLine(this.sourceFile, node.end, nextToken.end)) {
123+
this.reportUnnecessary(node.end);
124+
}
125+
}
126+
}
127+
128+
protected checkUnnecessary(node: ts.Node) {
129+
if (this.sourceFile.text[node.end - 1] !== ";") {
130+
return;
131+
}
132+
const lastToken = utils.getPreviousToken(node.getLastToken(this.sourceFile)!, this.sourceFile)!;
133+
// yield does not continue on the next line if there is no yielded expression
134+
if (lastToken.kind === ts.SyntaxKind.YieldKeyword && lastToken.parent!.kind === ts.SyntaxKind.YieldExpression ||
135+
// arrow functions with block as body don't continue on the next line
136+
lastToken.kind === ts.SyntaxKind.CloseBraceToken && lastToken.parent!.kind === ts.SyntaxKind.Block &&
137+
lastToken.parent!.parent!.kind === ts.SyntaxKind.ArrowFunction) {
138+
return this.checkSemicolonOrLineBreak(node);
139+
}
140+
const nextToken = utils.getNextToken(node, this.sourceFile)!;
141+
switch (nextToken.kind) {
142+
case ts.SyntaxKind.OpenParenToken:
143+
case ts.SyntaxKind.OpenBracketToken:
144+
case ts.SyntaxKind.PlusToken:
145+
case ts.SyntaxKind.MinusToken:
146+
case ts.SyntaxKind.RegularExpressionLiteral:
147+
case ts.SyntaxKind.LessThanToken:
148+
case ts.SyntaxKind.NoSubstitutionTemplateLiteral:
149+
case ts.SyntaxKind.TemplateHead:
150+
break;
151+
case ts.SyntaxKind.CloseBraceToken:
152+
case ts.SyntaxKind.EndOfFileToken:
153+
return this.reportUnnecessary(node.end);
154+
default:
155+
if (!utils.isSameLine(this.sourceFile, node.end, nextToken.end)) {
156+
this.reportUnnecessary(node.end);
157+
}
172158
}
173159
}
174160

161+
protected abstract checkPropertyDeclaration(node: ts.PropertyDeclaration): void;
162+
175163
private checkEmptyStatement(node: ts.Node) {
176164
// An empty statement is only ever useful when it is the only statement inside a loop
177165
if (!utils.isIterationStatement(node.parent!)) {
@@ -181,61 +169,165 @@ class SemicolonWalker extends Lint.AbstractWalker<Options> {
181169
const noFix = parentKind === ts.SyntaxKind.IfStatement ||
182170
parentKind === ts.SyntaxKind.LabeledStatement ||
183171
parentKind === ts.SyntaxKind.WithStatement;
184-
this.reportUnnecessary(node.end - 1, noFix);
172+
this.reportUnnecessary(node.end, noFix);
185173
}
186174
}
187175

188-
private checkInterface(node: ts.InterfaceDeclaration) {
189-
for (const member of node.members) {
190-
const lastChar = this.sourceFile.text[member.end - 1];
191-
const hasSemicolon = lastChar === ";";
192-
if (this.options.always && !hasSemicolon) {
193-
if (lastChar === ",") {
194-
this.addFailureAt(member.end - 1, 1, Rule.FAILURE_STRING_COMMA, new Lint.Replacement(member.end - 1, 1, ";"));
195-
} else {
196-
this.reportMissing(member.end);
197-
}
198-
} else if (!this.options.always && hasSemicolon &&
199-
(member === node.members[node.members.length - 1] || this.isFollowedByLineBreak(member.end))) {
200-
this.reportUnnecessary(member.end - 1);
176+
private visitPropertyDeclaration(node: ts.PropertyDeclaration) {
177+
// check if this is a multi-line arrow function
178+
if (node.initializer !== undefined &&
179+
node.initializer.kind === ts.SyntaxKind.ArrowFunction &&
180+
!utils.isSameLine(this.sourceFile, node.getStart(this.sourceFile), node.end)) {
181+
if (this.options.boundClassMethods) {
182+
this.checkUnnecessary(node);
201183
}
184+
} else {
185+
this.checkPropertyDeclaration(node);
202186
}
203187
}
188+
}
204189

205-
private reportMissing(pos: number) {
206-
this.addFailureAt(pos, 0, Rule.FAILURE_STRING_MISSING, Lint.Replacement.appendText(pos, ";"));
190+
class SemicolonAlwaysWalker extends SemicolonWalker {
191+
protected visitNode(node: ts.Node) {
192+
switch (node.kind) {
193+
case ts.SyntaxKind.VariableStatement:
194+
case ts.SyntaxKind.ExpressionStatement:
195+
case ts.SyntaxKind.ReturnStatement:
196+
case ts.SyntaxKind.BreakStatement:
197+
case ts.SyntaxKind.ContinueStatement:
198+
case ts.SyntaxKind.ThrowStatement:
199+
case ts.SyntaxKind.ImportEqualsDeclaration:
200+
case ts.SyntaxKind.DoStatement:
201+
case ts.SyntaxKind.ExportAssignment:
202+
case ts.SyntaxKind.TypeAliasDeclaration:
203+
case ts.SyntaxKind.ImportDeclaration:
204+
case ts.SyntaxKind.ExportDeclaration:
205+
case ts.SyntaxKind.DebuggerStatement:
206+
return this.checkMissing(node);
207+
case ts.SyntaxKind.ModuleDeclaration:
208+
case ts.SyntaxKind.MethodDeclaration:
209+
case ts.SyntaxKind.FunctionDeclaration:
210+
// check shorthand module declarations and method / function signatures
211+
if ((node as ts.FunctionLikeDeclaration | ts.ModuleDeclaration).body === undefined) {
212+
this.checkMissing(node);
213+
}
214+
break;
215+
case ts.SyntaxKind.InterfaceDeclaration:
216+
if (this.options.interfaces) {
217+
this.checkInterface(node as ts.InterfaceDeclaration);
218+
}
219+
break;
220+
default:
221+
return super.visitNode(node);
222+
}
207223
}
208224

209-
private reportUnnecessary(pos: number, noFix?: boolean) {
210-
this.addFailureAt(pos, 1, Rule.FAILURE_STRING_UNNECESSARY, noFix === true ? undefined : Lint.Replacement.deleteText(pos, 1));
225+
protected checkPropertyDeclaration(node: ts.PropertyDeclaration) {
226+
return this.checkMissing(node);
211227
}
212228

213-
private checkSemicolonAt(node: ts.Statement) {
214-
const hasSemicolon = this.sourceFile.text[node.end - 1] === ";";
215-
216-
if (this.options.always && !hasSemicolon) {
229+
private checkMissing(node: ts.Node) {
230+
if (this.sourceFile.text[node.end - 1] !== ";") {
217231
this.reportMissing(node.end);
218-
} else if (!this.options.always && hasSemicolon) {
219-
switch (utils.getNextToken(node, this.sourceFile)!.kind) {
220-
case ts.SyntaxKind.OpenParenToken:
221-
case ts.SyntaxKind.OpenBracketToken:
222-
case ts.SyntaxKind.PlusToken:
223-
case ts.SyntaxKind.MinusToken:
224-
case ts.SyntaxKind.RegularExpressionLiteral:
232+
}
233+
}
234+
235+
private reportMissing(pos: number) {
236+
this.addFailureAt(pos, 0, Rule.FAILURE_STRING_MISSING, Lint.Replacement.appendText(pos, ";"));
237+
}
238+
239+
private checkInterface(node: ts.InterfaceDeclaration) {
240+
for (const member of node.members) {
241+
switch (this.sourceFile.text[member.end - 1]) {
242+
case ";": break;
243+
case ",":
244+
this.addFailureAt(member.end - 1, 1, Rule.FAILURE_STRING_COMMA, new Lint.Replacement(member.end - 1, 1, ";"));
225245
break;
226246
default:
227-
if (!this.isFollowedByStatement(node)) {
228-
this.reportUnnecessary(node.end - 1);
229-
}
247+
this.reportMissing(member.end);
230248
}
231249
}
232250
}
251+
}
233252

234-
private isFollowedByStatement(node: ts.Statement): boolean {
253+
class SemicolonNeverWalker extends SemicolonWalker {
254+
protected visitNode(node: ts.Node) {
255+
switch (node.kind) {
256+
case ts.SyntaxKind.ExpressionStatement:
257+
case ts.SyntaxKind.ThrowStatement:
258+
case ts.SyntaxKind.ExportAssignment:
259+
return this.checkUnnecessary(node as ts.Statement);
260+
case ts.SyntaxKind.VariableStatement:
261+
return this.checkVariableStatement(node as ts.VariableStatement);
262+
case ts.SyntaxKind.ReturnStatement:
263+
if ((node as ts.ReturnStatement).expression === undefined) {
264+
// return does not continue on the next line if the is no returned expression
265+
return this.checkSemicolonOrLineBreak(node);
266+
}
267+
return this.checkUnnecessary(node);
268+
case ts.SyntaxKind.TypeAliasDeclaration:
269+
case ts.SyntaxKind.ImportEqualsDeclaration:
270+
case ts.SyntaxKind.ImportDeclaration:
271+
case ts.SyntaxKind.ExportDeclaration:
272+
case ts.SyntaxKind.DebuggerStatement:
273+
case ts.SyntaxKind.BreakStatement:
274+
case ts.SyntaxKind.ContinueStatement:
275+
case ts.SyntaxKind.DoStatement:
276+
return this.checkSemicolonOrLineBreak(node);
277+
case ts.SyntaxKind.ModuleDeclaration:
278+
// shorthand module declaration
279+
if ((node as ts.ModuleDeclaration).body === undefined) {
280+
this.checkShorthandModuleDeclaration(node as ts.ModuleDeclaration);
281+
}
282+
break;
283+
case ts.SyntaxKind.MethodDeclaration:
284+
// check method signature
285+
if ((node as ts.MethodDeclaration).body === undefined) {
286+
this.checkSemicolonOrLineBreak(node);
287+
}
288+
break;
289+
case ts.SyntaxKind.FunctionDeclaration:
290+
// check function signature
291+
if ((node as ts.FunctionDeclaration).body === undefined) {
292+
this.checkSemicolonOrLineBreak(node);
293+
}
294+
break;
295+
case ts.SyntaxKind.InterfaceDeclaration:
296+
if (this.options.interfaces) {
297+
this.checkInterface(node as ts.InterfaceDeclaration);
298+
}
299+
break;
300+
default:
301+
return super.visitNode(node);
302+
}
303+
}
304+
305+
protected checkPropertyDeclaration(node: ts.PropertyDeclaration) {
306+
if (node.initializer === undefined) {
307+
return this.checkSemicolonOrLineBreak(node);
308+
}
309+
return this.checkUnnecessary(node);
310+
}
311+
312+
private checkVariableStatement(node: ts.VariableStatement) {
313+
const declarations = node.declarationList.declarations;
314+
if (declarations[declarations.length - 1].initializer === undefined) {
315+
// variable declaration does not continue on the next line if it has no initializer
316+
return this.checkSemicolonOrLineBreak(node);
317+
}
318+
return this.checkUnnecessary(node);
319+
}
320+
321+
private checkShorthandModuleDeclaration(node: ts.ModuleDeclaration) {
235322
const nextStatement = utils.getNextStatement(node);
236-
if (nextStatement === undefined) {
237-
return false;
323+
if (nextStatement === undefined || nextStatement.kind !== ts.SyntaxKind.Block) {
324+
this.checkSemicolonOrLineBreak(node);
325+
}
326+
}
327+
328+
private checkInterface(node: ts.InterfaceDeclaration) {
329+
for (const member of node.members) {
330+
this.checkSemicolonOrLineBreak(member);
238331
}
239-
return utils.isSameLine(this.sourceFile, node.end, nextStatement.getStart(this.sourceFile));
240332
}
241333
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
var foo = bar;
2+
~ [Unnecessary semicolon]
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
return;
2+
~ [Unnecessary semicolon]

0 commit comments

Comments
 (0)