From f2e52e3bd9cdc4dc23e066cadf15bdd3fdc7427c Mon Sep 17 00:00:00 2001 From: Rasmus Eneman Date: Wed, 17 Feb 2016 22:08:36 +0100 Subject: [PATCH] Extend semicolon rule with a never option --- README.md | 4 +- docs/sample.tslint.json | 2 +- src/configuration.ts | 2 +- src/rules/semicolonRule.ts | 33 ++++-- .../rules/semicolon/{ => always}/test.ts.lint | 0 test/rules/semicolon/always/tslint.json | 5 + test/rules/semicolon/enabled/test.ts.lint | 74 ++++++++++++ .../rules/semicolon/{ => enabled}/tslint.json | 0 test/rules/semicolon/never/test.ts.lint | 106 ++++++++++++++++++ test/rules/semicolon/never/tslint.json | 5 + 10 files changed, 221 insertions(+), 10 deletions(-) rename test/rules/semicolon/{ => always}/test.ts.lint (100%) create mode 100644 test/rules/semicolon/always/tslint.json create mode 100644 test/rules/semicolon/enabled/test.ts.lint rename test/rules/semicolon/{ => enabled}/tslint.json (100%) create mode 100644 test/rules/semicolon/never/test.ts.lint create mode 100644 test/rules/semicolon/never/tslint.json diff --git a/README.md b/README.md index c98023def21..6ec265a8fea 100644 --- a/README.md +++ b/README.md @@ -216,7 +216,9 @@ A sample configuration file with all options is available [here](https://github. * `"jsx-double"` enforces double quotes for JSX attributes. * `"avoid-escape"` allows you to use the "other" quotemark in cases where escaping would normally be required. For example, `[true, "double", "avoid-escape"]` would not report a failure on the string literal `'Hello "World"'`. * `radix` enforces the radix parameter of `parseInt`. -* `semicolon` enforces semicolons at the end of every statement. +* `semicolon` enforces consistent semicolon usage at the end of every statement. Rule options: + * `"always"` enforces semicolons at the end of every statement. + * `"never"` disallows semicolons at the end of every statement except for when they are necessary. * `switch-default` enforces a `default` case in `switch` statements. * `trailing-comma` enforces or disallows trailing comma within array and object literals, destructuring assignment and named imports. Each rule option requires a value of `"always"` or `"never"`. Rule options: diff --git a/docs/sample.tslint.json b/docs/sample.tslint.json index e7af131be9b..1d8ac35f273 100644 --- a/docs/sample.tslint.json +++ b/docs/sample.tslint.json @@ -84,7 +84,7 @@ "avoid-escape" ], "radix": true, - "semicolon": true, + "semicolon": [true, "always"], "switch-default": true, "trailing-comma": [ true, diff --git a/src/configuration.ts b/src/configuration.ts index 1024f771611..28cf2243a59 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -32,7 +32,7 @@ export const DEFAULT_CONFIG = { "no-var-keyword": true, "one-line": [true, "check-open-brace", "check-whitespace"], "quotemark": [true, "double"], - "semicolon": true, + "semicolon": [true, "always"], "triple-equals": [true, "allow-null-check"], "typedef-whitespace": [true, { "call-signature": "nospace", diff --git a/src/rules/semicolonRule.ts b/src/rules/semicolonRule.ts index 1aa0d19b2ec..4405601334a 100644 --- a/src/rules/semicolonRule.ts +++ b/src/rules/semicolonRule.ts @@ -18,8 +18,12 @@ import * as ts from "typescript"; import * as Lint from "../lint"; +const OPTION_ALWAYS = "always"; +const OPTION_NEVER = "never"; + export class Rule extends Lint.Rules.AbstractRule { - public static FAILURE_STRING = "missing semicolon"; + public static FAILURE_STRING_MISSING = "missing semicolon"; + public static FAILURE_STRING_UNNECESSARY = "unnecessary semicolon"; public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { return this.applyWithWalker(new SemicolonWalker(sourceFile, this.getOptions())); @@ -90,13 +94,28 @@ class SemicolonWalker extends Lint.RuleWalker { } private checkSemicolonAt(node: ts.Node) { - const children = node.getChildren(this.getSourceFile()); + const sourceFile = this.getSourceFile(); + const children = node.getChildren(sourceFile); const hasSemicolon = children.some((child) => child.kind === ts.SyntaxKind.SemicolonToken); - - if (!hasSemicolon) { - const sourceFile = this.getSourceFile(); - const position = node.getStart(sourceFile) + node.getWidth(sourceFile); - this.addFailure(this.createFailure(Math.min(position, this.getLimit()), 0, Rule.FAILURE_STRING)); + const position = node.getStart(sourceFile) + node.getWidth(sourceFile); + // Backwards compatible with plain {"semicolon": true} + const always = this.hasOption(OPTION_ALWAYS) || (this.getOptions() && this.getOptions().length === 0); + + if (always && !hasSemicolon) { + this.addFailure(this.createFailure(Math.min(position, this.getLimit()), 0, Rule.FAILURE_STRING_MISSING)); + } else if (this.hasOption(OPTION_NEVER) && hasSemicolon) { + const scanner = ts.createScanner(ts.ScriptTarget.ES5, false, ts.LanguageVariant.Standard, sourceFile.text); + scanner.setTextPos(position); + + let tokenKind = scanner.scan(); + while (tokenKind === ts.SyntaxKind.WhitespaceTrivia || tokenKind === ts.SyntaxKind.NewLineTrivia) { + tokenKind = scanner.scan(); + } + + if (tokenKind !== ts.SyntaxKind.OpenParenToken && tokenKind !== ts.SyntaxKind.OpenBracketToken + && tokenKind !== ts.SyntaxKind.PlusToken && tokenKind !== ts.SyntaxKind.MinusToken) { + this.addFailure(this.createFailure(Math.min(position - 1, this.getLimit()), 1, Rule.FAILURE_STRING_UNNECESSARY)); + } } } } diff --git a/test/rules/semicolon/test.ts.lint b/test/rules/semicolon/always/test.ts.lint similarity index 100% rename from test/rules/semicolon/test.ts.lint rename to test/rules/semicolon/always/test.ts.lint diff --git a/test/rules/semicolon/always/tslint.json b/test/rules/semicolon/always/tslint.json new file mode 100644 index 00000000000..3df79679a99 --- /dev/null +++ b/test/rules/semicolon/always/tslint.json @@ -0,0 +1,5 @@ +{ + "rules": { + "semicolon": [true, "always"] + } +} diff --git a/test/rules/semicolon/enabled/test.ts.lint b/test/rules/semicolon/enabled/test.ts.lint new file mode 100644 index 00000000000..f7cf090a042 --- /dev/null +++ b/test/rules/semicolon/enabled/test.ts.lint @@ -0,0 +1,74 @@ +var x = 3 + ~nil [missing semicolon] +a += b + ~nil [missing semicolon] + +c = () => { +} + ~nil [missing semicolon] + +d = function() { } + ~nil [missing semicolon] + +console.log("i am adam, am i?") + ~nil [missing semicolon] + +function xyz() { + return + ~nil [missing semicolon] +} + +switch(xyz) { + case 1: + break + ~nil [missing semicolon] + case 2: + continue + ~nil [missing semicolon] +} + +throw new Error("some error") + ~nil [missing semicolon] + +do { + var a = 4 + ~nil [missing semicolon] +} while(x == 3) + ~nil [missing semicolon] + +debugger + ~nil [missing semicolon] + +import v = require("i") + ~nil [missing semicolon] +module M { + export var x + ~nil [missing semicolon] +} + +function useStrictMissingSemicolon() { + "use strict" + ~nil [missing semicolon] + return null; +} + +class MyClass { + public name : string + ~nil [missing semicolon] + private index : number + ~nil [missing semicolon] + private email : string; +} + +interface ITest { + foo?: string + ~nil [missing semicolon] + bar: number + ~nil [missing semicolon] + baz: boolean; +} + +import {Router} from 'aurelia-router'; + +import {Controller} from 'my-lib' + ~nil [missing semicolon] diff --git a/test/rules/semicolon/tslint.json b/test/rules/semicolon/enabled/tslint.json similarity index 100% rename from test/rules/semicolon/tslint.json rename to test/rules/semicolon/enabled/tslint.json diff --git a/test/rules/semicolon/never/test.ts.lint b/test/rules/semicolon/never/test.ts.lint new file mode 100644 index 00000000000..555aab78adc --- /dev/null +++ b/test/rules/semicolon/never/test.ts.lint @@ -0,0 +1,106 @@ +var x = 3; + ~ [unnecessary semicolon] +a += b; + ~ [unnecessary semicolon] + +c = () => { +}; + ~ [unnecessary semicolon] + +d = function() { }; + ~ [unnecessary semicolon] + +console.log("i am adam, am i?"); + ~ [unnecessary semicolon] + +function xyz() { + return; + ~ [unnecessary semicolon] +} + +switch(xyz) { + case 1: + break; + ~ [unnecessary semicolon] + case 2: + continue; + ~ [unnecessary semicolon] +} + +throw new Error("some error"); + ~ [unnecessary semicolon] + +do { + var a = 4; + ~ [unnecessary semicolon] +} while(x == 3); + ~ [unnecessary semicolon] + +debugger; + ~ [unnecessary semicolon] + +import v = require("i"); + ~ [unnecessary semicolon] +module M { + export var x; + ~ [unnecessary semicolon] +} + +function useStrictUnnecessarySemicolon() { + "use strict"; + ~ [unnecessary semicolon] + return null +} + +class MyClass { + public name : string; + ~ [unnecessary semicolon] + private index : number; + ~ [unnecessary semicolon] + private email : string +} + +interface ITest { + foo?: string; + ~ [unnecessary semicolon] + bar: number; + ~ [unnecessary semicolon] + baz: boolean +} + +import {Router} from 'aurelia-router' + +import {Controller} from 'my-lib'; + ~ [unnecessary semicolon] + +// Edge cases when not omitting semicolon needs to be supported + +var a = 1; +("1" + "2").length + +var a = 1; +[].length + +var a = 1; ++"a" + +var a = 1; +-1 + +var a = 1 +;("1" + "2").length + +var a = 1 +;[].length + +var a = 1 +;+"a" + +var a = 1 +;-1 + +// For loops uses semicolons as well so make sure we aren't breaking those + +for (var i = 0; i < 10; ++i) { + // do something +} diff --git a/test/rules/semicolon/never/tslint.json b/test/rules/semicolon/never/tslint.json new file mode 100644 index 00000000000..94dc12a5077 --- /dev/null +++ b/test/rules/semicolon/never/tslint.json @@ -0,0 +1,5 @@ +{ + "rules": { + "semicolon": [true, "never"] + } +}