From 101c05ff2ee870c6428337f803c09594ef37823e Mon Sep 17 00:00:00 2001 From: Noah Chen Date: Fri, 6 Jan 2017 09:42:38 -0800 Subject: [PATCH 001/131] Update change log to add v4.3.0-dev.0 --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 461b11e5964..50df0204c61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ Change Log === +v4.3.0-dev.0 +--- + +* Include latest v4.3.1 changes + v4.3.1 --- From 4418cef4b73a660ad70eba522cd9dcdbdf95434c Mon Sep 17 00:00:00 2001 From: Noah Chen Date: Fri, 6 Jan 2017 10:21:25 -0800 Subject: [PATCH 002/131] Revert change log entry v4.3.0-dev.0 --- CHANGELOG.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 50df0204c61..461b11e5964 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,6 @@ Change Log === -v4.3.0-dev.0 ---- - -* Include latest v4.3.1 changes - v4.3.1 --- From e649082567fc5cf005c130e4f3fcf5d3a1d42d19 Mon Sep 17 00:00:00 2001 From: Andrii Dieiev Date: Sat, 7 Jan 2017 07:47:53 +0200 Subject: [PATCH 003/131] Allow fast null checks in no-unused-expression rule (#1638) --- src/rules/noUnusedExpressionRule.ts | 59 +++++++- .../{ => allow-fast-null-checks}/test.js.lint | 18 +++ .../{ => allow-fast-null-checks}/test.ts.lint | 18 +++ .../allow-fast-null-checks/tslint.json | 8 + .../no-unused-expression/default/test.js.lint | 133 +++++++++++++++++ .../no-unused-expression/default/test.ts.lint | 139 ++++++++++++++++++ .../{ => default}/tslint.json | 0 7 files changed, 372 insertions(+), 3 deletions(-) rename test/rules/no-unused-expression/{ => allow-fast-null-checks}/test.js.lint (81%) rename test/rules/no-unused-expression/{ => allow-fast-null-checks}/test.ts.lint (83%) create mode 100644 test/rules/no-unused-expression/allow-fast-null-checks/tslint.json create mode 100644 test/rules/no-unused-expression/default/test.js.lint create mode 100644 test/rules/no-unused-expression/default/test.ts.lint rename test/rules/no-unused-expression/{ => default}/tslint.json (100%) diff --git a/src/rules/noUnusedExpressionRule.ts b/src/rules/noUnusedExpressionRule.ts index b0789d6a652..953652beedb 100644 --- a/src/rules/noUnusedExpressionRule.ts +++ b/src/rules/noUnusedExpressionRule.ts @@ -18,6 +18,9 @@ import * as ts from "typescript"; import * as Lint from "../index"; +import { unwrapParentheses } from "../language/utils"; + +const ALLOW_FAST_NULL_CHECKS = "allow-fast-null-checks"; export class Rule extends Lint.Rules.AbstractRule { /* tslint:disable:object-literal-sort-keys */ @@ -29,9 +32,21 @@ export class Rule extends Lint.Rules.AbstractRule { (and thus usually no-ops).`, rationale: Lint.Utils.dedent` Detects potential errors where an assignment or function call was intended.`, - optionsDescription: "Not configurable.", - options: null, - optionExamples: ["true"], + optionsDescription: Lint.Utils.dedent` + One argument may be optionally provided: + + * \`${ALLOW_FAST_NULL_CHECKS}\` allows to use logical operators to perform fast null checks and perform + method or function calls for side effects (e.g. \`e && e.preventDefault()\`).`, + options: { + type: "array", + items: { + type: "string", + enum: [ALLOW_FAST_NULL_CHECKS], + }, + minLength: 0, + maxLength: 1, + }, + optionExamples: ["true", `[true, "${ALLOW_FAST_NULL_CHECKS}"]`], type: "functionality", typescriptOnly: false, }; @@ -108,6 +123,15 @@ export class NoUnusedExpressionWalker extends Lint.RuleWalker { case ts.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken: this.expressionIsUnused = false; break; + case ts.SyntaxKind.AmpersandAmpersandToken: + case ts.SyntaxKind.BarBarToken: + if (this.hasOption(ALLOW_FAST_NULL_CHECKS) && isTopLevelExpression(node)) { + this.expressionIsUnused = !hasCallExpression(node.right); + break; + } else { + this.expressionIsUnused = true; + break; + } default: this.expressionIsUnused = true; } @@ -177,3 +201,32 @@ export class NoUnusedExpressionWalker extends Lint.RuleWalker { } } } + +function hasCallExpression(node: ts.Expression): boolean { + const nodeToCheck = unwrapParentheses(node); + + if (nodeToCheck.kind === ts.SyntaxKind.CallExpression) { + return true; + } + + if (nodeToCheck.kind === ts.SyntaxKind.BinaryExpression) { + const operatorToken = (nodeToCheck as ts.BinaryExpression).operatorToken; + + if (operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken || + operatorToken.kind === ts.SyntaxKind.BarBarToken) { + return hasCallExpression((nodeToCheck as ts.BinaryExpression).right); + } + } + + return false; +} + +function isTopLevelExpression(node: ts.Expression): boolean { + let nodeToCheck = node.parent as ts.Node; + + while (nodeToCheck.kind === ts.SyntaxKind.ParenthesizedExpression) { + nodeToCheck = nodeToCheck.parent as ts.Node; + } + + return nodeToCheck.kind === ts.SyntaxKind.ExpressionStatement; +} diff --git a/test/rules/no-unused-expression/test.js.lint b/test/rules/no-unused-expression/allow-fast-null-checks/test.js.lint similarity index 81% rename from test/rules/no-unused-expression/test.js.lint rename to test/rules/no-unused-expression/allow-fast-null-checks/test.js.lint index 80e8e7bd054..b2386eac84a 100644 --- a/test/rules/no-unused-expression/test.js.lint +++ b/test/rules/no-unused-expression/allow-fast-null-checks/test.js.lint @@ -109,4 +109,22 @@ a => fun2(a); "use strct"; ~~~~~~~~~~~~ [0] +afterEach((el) => { + el && el.remove(); +}); + +checkParams((a, b) => { + (a || required('a')) && (b || required('b')); +}); + +checkParams((a, b) => { + ((a && b) || required('a, b')); +}); + +function interactionHandler(e) { + // fails in all cases since logical NOT operator is redundant + e && !e.preventDefault(); + ~~~~~~~~~~~~~~~~~~~~~~~~~ [0] +} + [0]: expected an assignment or function call diff --git a/test/rules/no-unused-expression/test.ts.lint b/test/rules/no-unused-expression/allow-fast-null-checks/test.ts.lint similarity index 83% rename from test/rules/no-unused-expression/test.ts.lint rename to test/rules/no-unused-expression/allow-fast-null-checks/test.ts.lint index e88ea48cc3d..c72dcbf6ad3 100644 --- a/test/rules/no-unused-expression/test.ts.lint +++ b/test/rules/no-unused-expression/allow-fast-null-checks/test.ts.lint @@ -115,4 +115,22 @@ a => fun2(a); "use strct"; ~~~~~~~~~~~~ [0] +afterEach((el) => { + el && el.remove(); +}); + +checkParams((a, b) => { + (a || required('a')) && (b || required('b')); +}); + +checkParams((a, b) => { + ((a && b) || required('a, b')); +}); + +function interactionHandler(e) { + // fails in all cases since logical NOT operator is redundant + e && !e.preventDefault(); + ~~~~~~~~~~~~~~~~~~~~~~~~~ [0] +} + [0]: expected an assignment or function call diff --git a/test/rules/no-unused-expression/allow-fast-null-checks/tslint.json b/test/rules/no-unused-expression/allow-fast-null-checks/tslint.json new file mode 100644 index 00000000000..c8a2a238f40 --- /dev/null +++ b/test/rules/no-unused-expression/allow-fast-null-checks/tslint.json @@ -0,0 +1,8 @@ +{ + "rules": { + "no-unused-expression": [true, "allow-fast-null-checks"] + }, + "jsRules": { + "no-unused-expression": [true, "allow-fast-null-checks"] + } +} diff --git a/test/rules/no-unused-expression/default/test.js.lint b/test/rules/no-unused-expression/default/test.js.lint new file mode 100644 index 00000000000..936dd446696 --- /dev/null +++ b/test/rules/no-unused-expression/default/test.js.lint @@ -0,0 +1,133 @@ +"use strict"; +'use asm'; +"ngInject"; +''; + +function fun1() { + "use strict"; + 'someOtherDirective'; + return 0; +} + +(function() { "directive"; +'foo' +'directive2' +console.log('foo'); +'notdirective'; +~~~~~~~~~~~~~~~ [0] +})(); + +const a = () => { +'use strict'; "use cool"; "use lint"; var a = 1; "notdirective"; } + ~~~~~~~~~~~~~~~ [0] + +function fun2(a) { + return 0; +} + +function fun3(a, b) { + return 0; +} + +class Foo { + constructor() { + "ngInject"; + var a = 1; + 'notdirective'; + ~~~~~~~~~~~~~~~ [0] + } + + bar() { + 'use strict'; + } + + get baz() { + 'use asm'; + } + + set baz(newValue) { + "use asm"; + } +} + +// valid code: + +var i; +var j = 3; +i = 1 + 2; +j = fun1(); +fun1(); +fun2(2); +fun3(2, fun1()); +i++; +i += 2; +--i; +i <<= 2; +i = fun1() + fun1(); +j = (j === 0 ? 5 : 6); +(j === 0 ? fun1() : fun2(j)); +(a => 5)(4); +var obj = {}; +delete obj.key; +function* g() { + for (let i = 0; i < 100; i++) { + yield i; + } +} + +async function f(foo) { + await foo; + return 0; +} + +new Foo(); + +// invalid code: + +5; +~~ [0] +i; +~~ [0] +3 + 5; +~~~~~~ [0] +fun1() + fun1(); +~~~~~~~~~~~~~~~~ [0] +fun2(i) + fun3(4,7); +~~~~~~~~~~~~~~~~~~~~ [0] +fun1() + 4; +~~~~~~~~~~~ [0] +4 + fun2(j); +~~~~~~~~~~~~ [0] +(j === 0 ? fun1() : 5); +~~~~~~~~~~~~~~~~~~~~~~~ [0] +(j === 0 ? i : fun2(j)); +~~~~~~~~~~~~~~~~~~~~~~~~ [0] +a => fun2(a); +~~~~~~~~~~~~~ [0] +() => {return fun1();}; +~~~~~~~~~~~~~~~~~~~~~~~ [0] +"use strct"; +~~~~~~~~~~~~ [0] + +afterEach((el) => { + el && el.remove(); + ~~~~~~~~~~~~~~~~~~ [0] +}); + +checkParams((a, b) => { + (a || required('a')) && (b || required('b')); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] +}); + +checkParams((a, b) => { + ((a && b) || required('a, b')); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] +}); + +function interactionHandler(e) { + // fails in all cases since logical NOT operator is redundant + e && !e.preventDefault(); + ~~~~~~~~~~~~~~~~~~~~~~~~~ [0] +} + +[0]: expected an assignment or function call diff --git a/test/rules/no-unused-expression/default/test.ts.lint b/test/rules/no-unused-expression/default/test.ts.lint new file mode 100644 index 00000000000..66ec5fadbe9 --- /dev/null +++ b/test/rules/no-unused-expression/default/test.ts.lint @@ -0,0 +1,139 @@ +"use strict"; +'use asm'; +"ngInject"; +''; + +function fun1() { + "use strict"; + 'someOtherDirective'; + return 0; +} + +(function() { "directive"; +'foo' +'directive2' +console.log('foo'); +'notdirective'; +~~~~~~~~~~~~~~~ [0] +})(); + +const a = () => { +'use strict'; "use cool"; "use lint"; var a = 1; "notdirective"; } + ~~~~~~~~~~~~~~~ [0] + +function fun2(a: number) { + return 0; +} + +function fun3(a: number, b: number) { + return 0; +} + +namespace Fam { 'use strict'; 'use cool'; } +module Bam { 'use strict'; 'use cool'; } +namespace Az.Bz.Cz.Dz { + 'ngInject'; +} + +class Foo { + constructor() { + "ngInject"; + var a = 1; + 'notdirective'; + ~~~~~~~~~~~~~~~ [0] + } + + bar() { + 'use strict'; + } + + get baz() { + 'use asm'; + } + + set baz(newValue) { + "use asm"; + } +} + +// valid code: + +var i: number; +var j = 3; +i = 1 + 2; +j = fun1(); +fun1(); +fun2(2); +fun3(2, fun1()); +i++; +i += 2; +--i; +i <<= 2; +i = fun1() + fun1(); +j = (j === 0 ? 5 : 6); +(j === 0 ? fun1() : fun2(j)); +(a => 5)(4); +var obj = {}; +delete obj.key; +function* g(): Iterable { + for (let i = 0; i < 100; i++) { + yield i; + } +} + +async function f(foo: Promise): Promise { + await foo; + return 0; +} + +new Foo(); + +// invalid code: + +5; +~~ [0] +i; +~~ [0] +3 + 5; +~~~~~~ [0] +fun1() + fun1(); +~~~~~~~~~~~~~~~~ [0] +fun2(i) + fun3(4,7); +~~~~~~~~~~~~~~~~~~~~ [0] +fun1() + 4; +~~~~~~~~~~~ [0] +4 + fun2(j); +~~~~~~~~~~~~ [0] +(j === 0 ? fun1() : 5); +~~~~~~~~~~~~~~~~~~~~~~~ [0] +(j === 0 ? i : fun2(j)); +~~~~~~~~~~~~~~~~~~~~~~~~ [0] +a => fun2(a); +~~~~~~~~~~~~~ [0] +() => {return fun1();}; +~~~~~~~~~~~~~~~~~~~~~~~ [0] +"use strct"; +~~~~~~~~~~~~ [0] + +afterEach((el) => { + el && el.remove(); + ~~~~~~~~~~~~~~~~~~ [0] +}); + +checkParams((a, b) => { + (a || required('a')) && (b || required('b')); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] +}); + +checkParams((a, b) => { + ((a && b) || required('a, b')); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] +}); + +function interactionHandler(e) { + // fails in all cases since logical NOT operator is redundant + e && !e.preventDefault(); + ~~~~~~~~~~~~~~~~~~~~~~~~~ [0] +} + +[0]: expected an assignment or function call diff --git a/test/rules/no-unused-expression/tslint.json b/test/rules/no-unused-expression/default/tslint.json similarity index 100% rename from test/rules/no-unused-expression/tslint.json rename to test/rules/no-unused-expression/default/tslint.json From 26880217f4edf28b730a4fa996ef1455ac8dbd99 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Fri, 6 Jan 2017 22:52:23 -0800 Subject: [PATCH 004/131] Add `prefer-arrow-shorthand-return` rule (#1972) --- docs/_data/rules.json | 20 +++- docs/rules/arrow-parens/index.html | 2 +- docs/rules/arrow-return-shorthand/index.html | 26 +++++ .../prefer-arrow-shorthand-return/index.html | 14 +++ src/language/walker/syntaxWalker.ts | 4 +- src/rules/arrowParensRule.ts | 4 +- src/rules/arrowReturnShorthandRule.ts | 110 ++++++++++++++++++ src/rules/cyclomaticComplexityRule.ts | 2 +- src/rules/noUnusedExpressionRule.ts | 2 +- src/rules/oneLineRule.ts | 2 +- src/rules/spaceBeforeFunctionParenRule.ts | 2 +- src/rules/trailingCommaRule.ts | 2 +- src/rules/typedefRule.ts | 2 +- src/rules/whitespaceRule.ts | 2 +- .../default/test.js.fix | 23 ++++ .../default/test.js.lint | 32 +++++ .../default/test.ts.fix | 21 ++++ .../default/test.ts.lint | 30 +++++ .../default/tslint.json | 8 ++ .../multiline/test.ts.fix | 20 ++++ .../multiline/test.ts.lint | 33 ++++++ .../multiline/tslint.json | 5 + 22 files changed, 353 insertions(+), 13 deletions(-) create mode 100644 docs/rules/arrow-return-shorthand/index.html create mode 100644 docs/rules/prefer-arrow-shorthand-return/index.html create mode 100644 src/rules/arrowReturnShorthandRule.ts create mode 100644 test/rules/arrow-return-shorthand/default/test.js.fix create mode 100644 test/rules/arrow-return-shorthand/default/test.js.lint create mode 100644 test/rules/arrow-return-shorthand/default/test.ts.fix create mode 100644 test/rules/arrow-return-shorthand/default/test.ts.lint create mode 100644 test/rules/arrow-return-shorthand/default/tslint.json create mode 100644 test/rules/arrow-return-shorthand/multiline/test.ts.fix create mode 100644 test/rules/arrow-return-shorthand/multiline/test.ts.lint create mode 100644 test/rules/arrow-return-shorthand/multiline/tslint.json diff --git a/docs/_data/rules.json b/docs/_data/rules.json index 396975f0c9d..5d5528fa11c 100644 --- a/docs/_data/rules.json +++ b/docs/_data/rules.json @@ -61,7 +61,7 @@ "description": "Requires parentheses around the parameters of arrow function definitions.", "hasFix": true, "rationale": "Maintains stylistic consistency with other arrow function definitions.", - "optionsDescription": "\nif `ban-single-arg-parens` is specified, then arrow functions with one parameter\nmust not have parentheses if removing them is allowed by TypeScript.", + "optionsDescription": "\nIf `ban-single-arg-parens` is specified, then arrow functions with one parameter\nmust not have parentheses if removing them is allowed by TypeScript.", "options": { "type": "string", "enum": [ @@ -75,6 +75,24 @@ "type": "style", "typescriptOnly": false }, + { + "ruleName": "arrow-return-shorthand", + "description": "Suggests to convert `() => { return x; }` to `() => x`.", + "hasFix": true, + "optionsDescription": "\nIf `multiline` is specified, then this will warn even if the function spans multiple lines.", + "options": { + "type": "string", + "enum": [ + "multiline" + ] + }, + "optionExamples": [ + "[true]", + "[true, \"multiline\"]" + ], + "type": "style", + "typescriptOnly": false + }, { "ruleName": "ban", "description": "Bans the use of specific functions or global methods.", diff --git a/docs/rules/arrow-parens/index.html b/docs/rules/arrow-parens/index.html index 95180701d85..d6bec9d8a20 100644 --- a/docs/rules/arrow-parens/index.html +++ b/docs/rules/arrow-parens/index.html @@ -5,7 +5,7 @@ rationale: Maintains stylistic consistency with other arrow function definitions. optionsDescription: |- - if `ban-single-arg-parens` is specified, then arrow functions with one parameter + If `ban-single-arg-parens` is specified, then arrow functions with one parameter must not have parentheses if removing them is allowed by TypeScript. options: type: string diff --git a/docs/rules/arrow-return-shorthand/index.html b/docs/rules/arrow-return-shorthand/index.html new file mode 100644 index 00000000000..69ca43199ca --- /dev/null +++ b/docs/rules/arrow-return-shorthand/index.html @@ -0,0 +1,26 @@ +--- +ruleName: arrow-return-shorthand +description: 'Suggests to convert `() => { return x; }` to `() => x`.' +hasFix: true +optionsDescription: |- + + If `multiline` is specified, then this will warn even if the function spans multiple lines. +options: + type: string + enum: + - multiline +optionExamples: + - '[true]' + - '[true, "multiline"]' +type: style +typescriptOnly: false +layout: rule +title: 'Rule: arrow-return-shorthand' +optionsJSON: |- + { + "type": "string", + "enum": [ + "multiline" + ] + } +--- \ No newline at end of file diff --git a/docs/rules/prefer-arrow-shorthand-return/index.html b/docs/rules/prefer-arrow-shorthand-return/index.html new file mode 100644 index 00000000000..615e5a844da --- /dev/null +++ b/docs/rules/prefer-arrow-shorthand-return/index.html @@ -0,0 +1,14 @@ +--- +ruleName: prefer-arrow-shorthand-return +description: 'Suggests to convert `() => { return x; }` to `() => x`.' +optionsDescription: Not configurable. +options: null +optionExamples: + - '[true]' + - '[true, "multiline"]' +type: functionality +typescriptOnly: false +layout: rule +title: 'Rule: prefer-arrow-shorthand-return' +optionsJSON: 'null' +--- \ No newline at end of file diff --git a/src/language/walker/syntaxWalker.ts b/src/language/walker/syntaxWalker.ts index d42e0664dad..c5ec8aace5a 100644 --- a/src/language/walker/syntaxWalker.ts +++ b/src/language/walker/syntaxWalker.ts @@ -34,7 +34,7 @@ export class SyntaxWalker { this.walkChildren(node); } - protected visitArrowFunction(node: ts.FunctionLikeDeclaration) { + protected visitArrowFunction(node: ts.ArrowFunction) { this.walkChildren(node); } @@ -361,7 +361,7 @@ export class SyntaxWalker { break; case ts.SyntaxKind.ArrowFunction: - this.visitArrowFunction(node as ts.FunctionLikeDeclaration); + this.visitArrowFunction(node as ts.ArrowFunction); break; case ts.SyntaxKind.BinaryExpression: diff --git a/src/rules/arrowParensRule.ts b/src/rules/arrowParensRule.ts index dc68ac2041e..99aa7f885f5 100644 --- a/src/rules/arrowParensRule.ts +++ b/src/rules/arrowParensRule.ts @@ -29,7 +29,7 @@ export class Rule extends Lint.Rules.AbstractRule { hasFix: true, rationale: "Maintains stylistic consistency with other arrow function definitions.", optionsDescription: Lint.Utils.dedent` - if \`${BAN_SINGLE_ARG_PARENS}\` is specified, then arrow functions with one parameter + If \`${BAN_SINGLE_ARG_PARENS}\` is specified, then arrow functions with one parameter must not have parentheses if removing them is allowed by TypeScript.`, options: { type: "string", @@ -58,7 +58,7 @@ class ArrowParensWalker extends Lint.RuleWalker { this.avoidOnSingleParameter = this.hasOption(BAN_SINGLE_ARG_PARENS); } - public visitArrowFunction(node: ts.FunctionLikeDeclaration) { + public visitArrowFunction(node: ts.ArrowFunction) { if (node.parameters.length === 1 && node.typeParameters === undefined) { const parameter = node.parameters[0]; diff --git a/src/rules/arrowReturnShorthandRule.ts b/src/rules/arrowReturnShorthandRule.ts new file mode 100644 index 00000000000..58bb7e869d6 --- /dev/null +++ b/src/rules/arrowReturnShorthandRule.ts @@ -0,0 +1,110 @@ +/** + * @license + * Copyright 2017 Palantir Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as ts from "typescript"; + +import * as Lint from "../index"; + +const OPTION_MULTILINE = "multiline"; + +export class Rule extends Lint.Rules.AbstractRule { + /* tslint:disable:object-literal-sort-keys */ + public static metadata: Lint.IRuleMetadata = { + ruleName: "arrow-return-shorthand", + description: "Suggests to convert `() => { return x; }` to `() => x`.", + hasFix: true, + optionsDescription: Lint.Utils.dedent` + If \`${OPTION_MULTILINE}\` is specified, then this will warn even if the function spans multiple lines.`, + options: { + type: "string", + enum: [OPTION_MULTILINE], + }, + optionExamples: [ + `[true]`, + `[true, "${OPTION_MULTILINE}"]`, + ], + type: "style", + typescriptOnly: false, + }; + /* tslint:enable:object-literal-sort-keys */ + + public static FAILURE_STRING = + "This arrow function body can be simplified by omitting the curly braces and the keyword 'return'."; + + public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { + return this.applyWithWalker(new Walker(sourceFile, this.getOptions())); + } +} + +class Walker extends Lint.RuleWalker { + public visitArrowFunction(node: ts.ArrowFunction) { + if (node.body && node.body.kind === ts.SyntaxKind.Block) { + const expr = getSimpleReturnExpression(node.body as ts.Block); + if (expr !== undefined && (this.hasOption(OPTION_MULTILINE) || !this.isMultiline(node.body))) { + this.addFailureAtNode(node.body, Rule.FAILURE_STRING, this.createArrowFunctionFix(node, node.body as ts.Block, expr)); + } + } + + super.visitArrowFunction(node); + } + + private isMultiline(node: ts.Node): boolean { + const getLine = (position: number) => this.getLineAndCharacterOfPosition(position).line; + return getLine(node.getEnd()) > getLine(node.getStart()); + } + + private createArrowFunctionFix(arrowFunction: ts.FunctionLikeDeclaration, body: ts.Block, expr: ts.Expression): Lint.Fix | undefined { + const text = this.getSourceFile().text; + const statement = expr.parent!; + const returnKeyword = Lint.childOfKind(statement, ts.SyntaxKind.ReturnKeyword)!; + const arrow = Lint.childOfKind(arrowFunction, ts.SyntaxKind.EqualsGreaterThanToken)!; + const openBrace = Lint.childOfKind(body, ts.SyntaxKind.OpenBraceToken)!; + const closeBrace = Lint.childOfKind(body, ts.SyntaxKind.CloseBraceToken)!; + const semicolon = Lint.childOfKind(statement, ts.SyntaxKind.SemicolonToken); + + const anyComments = hasComments(arrow) || hasComments(openBrace) || hasComments(statement) || hasComments(returnKeyword) || + hasComments(expr) || (semicolon && hasComments(semicolon)) || hasComments(closeBrace); + return anyComments ? undefined : this.createFix( + // Object literal must be wrapped in `()` + ...(expr.kind === ts.SyntaxKind.ObjectLiteralExpression ? [ + this.appendText(expr.getStart(), "("), + this.appendText(expr.getEnd(), ")"), + ] : []), + // " {" + deleteFromTo(arrow.end, openBrace.end), + // "return " + deleteFromTo(statement.getStart(), expr.getStart()), + // " }" (may include semicolon) + deleteFromTo(expr.end, closeBrace.end), + ); + + function hasComments(node: ts.Node): boolean { + return ts.getTrailingCommentRanges(text, node.getEnd()) !== undefined; + } + } +} + +/** Given `{ return x; }`, return `x`. */ +function getSimpleReturnExpression(block: ts.Block): ts.Expression | undefined { + return block.statements.length === 1 && block.statements[0].kind === ts.SyntaxKind.ReturnStatement + ? (block.statements[0] as ts.ReturnStatement).expression + : undefined; +} + +function deleteFromTo(start: number, end: number): Lint.Replacement { + return new Lint.Replacement(start, end - start, ""); +} diff --git a/src/rules/cyclomaticComplexityRule.ts b/src/rules/cyclomaticComplexityRule.ts index d03a54321dd..046ef56a2af 100644 --- a/src/rules/cyclomaticComplexityRule.ts +++ b/src/rules/cyclomaticComplexityRule.ts @@ -89,7 +89,7 @@ class CyclomaticComplexityWalker extends Lint.RuleWalker { super(sourceFile, options); } - protected visitArrowFunction(node: ts.FunctionLikeDeclaration) { + protected visitArrowFunction(node: ts.ArrowFunction) { this.startFunction(); super.visitArrowFunction(node); this.endFunction(node); diff --git a/src/rules/noUnusedExpressionRule.ts b/src/rules/noUnusedExpressionRule.ts index 953652beedb..f6821f09204 100644 --- a/src/rules/noUnusedExpressionRule.ts +++ b/src/rules/noUnusedExpressionRule.ts @@ -159,7 +159,7 @@ export class NoUnusedExpressionWalker extends Lint.RuleWalker { this.expressionIsUnused = true; } - public visitArrowFunction(node: ts.FunctionLikeDeclaration) { + public visitArrowFunction(node: ts.ArrowFunction) { super.visitArrowFunction(node); this.expressionIsUnused = true; } diff --git a/src/rules/oneLineRule.ts b/src/rules/oneLineRule.ts index b60db6617ee..89ae394d58d 100644 --- a/src/rules/oneLineRule.ts +++ b/src/rules/oneLineRule.ts @@ -238,7 +238,7 @@ class OneLineWalker extends Lint.RuleWalker { super.visitConstructorDeclaration(node); } - public visitArrowFunction(node: ts.FunctionLikeDeclaration) { + public visitArrowFunction(node: ts.ArrowFunction) { const body = node.body; if (body != null && body.kind === ts.SyntaxKind.Block) { const arrowToken = Lint.childOfKind(node, ts.SyntaxKind.EqualsGreaterThanToken); diff --git a/src/rules/spaceBeforeFunctionParenRule.ts b/src/rules/spaceBeforeFunctionParenRule.ts index 81245fb211d..894bdc5e7cc 100644 --- a/src/rules/spaceBeforeFunctionParenRule.ts +++ b/src/rules/spaceBeforeFunctionParenRule.ts @@ -86,7 +86,7 @@ class FunctionWalker extends Lint.RuleWalker { this.cacheOptions(); } - protected visitArrowFunction(node: ts.FunctionLikeDeclaration): void { + protected visitArrowFunction(node: ts.ArrowFunction): void { const option = this.getOption("asyncArrow"); const syntaxList = Lint.childOfKind(node, ts.SyntaxKind.SyntaxList)!; const isAsyncArrow = syntaxList.getStart() === node.getStart() && syntaxList.getText() === "async"; diff --git a/src/rules/trailingCommaRule.ts b/src/rules/trailingCommaRule.ts index cf241ae03f8..b96f552905f 100644 --- a/src/rules/trailingCommaRule.ts +++ b/src/rules/trailingCommaRule.ts @@ -78,7 +78,7 @@ class TrailingCommaWalker extends Lint.RuleWalker { super.visitArrayLiteralExpression(node); } - public visitArrowFunction(node: ts.FunctionLikeDeclaration) { + public visitArrowFunction(node: ts.ArrowFunction) { this.lintChildNodeWithIndex(node, 1); super.visitArrowFunction(node); } diff --git a/src/rules/typedefRule.ts b/src/rules/typedefRule.ts index 6f24bae53c5..b4c97e1d1db 100644 --- a/src/rules/typedefRule.ts +++ b/src/rules/typedefRule.ts @@ -75,7 +75,7 @@ class TypedefWalker extends Lint.RuleWalker { super.visitFunctionExpression(node); } - public visitArrowFunction(node: ts.FunctionLikeDeclaration) { + public visitArrowFunction(node: ts.ArrowFunction) { const location = (node.parameters != null) ? node.parameters.end : null; if (location != null diff --git a/src/rules/whitespaceRule.ts b/src/rules/whitespaceRule.ts index 57d980b6d42..cdabe97eb5e 100644 --- a/src/rules/whitespaceRule.ts +++ b/src/rules/whitespaceRule.ts @@ -139,7 +139,7 @@ class WhitespaceWalker extends Lint.SkippableTokenAwareRuleWalker { }); } - public visitArrowFunction(node: ts.FunctionLikeDeclaration) { + public visitArrowFunction(node: ts.ArrowFunction) { this.checkEqualsGreaterThanTokenInNode(node); super.visitArrowFunction(node); } diff --git a/test/rules/arrow-return-shorthand/default/test.js.fix b/test/rules/arrow-return-shorthand/default/test.js.fix new file mode 100644 index 00000000000..feec29b2bd8 --- /dev/null +++ b/test/rules/arrow-return-shorthand/default/test.js.fix @@ -0,0 +1,23 @@ +// Copy of test.ts.lint + +// Invalid: +(() => 0); +(() => ({ x: 1 })); +(() => { + return 0; +}); + +// Valid: +(() => 0); +(() => {}); +(() => { throw 0; }) +(() => { const x = 0; return x; }); + +// No fix if there's a comment. +(() => /**/ { return 0; }); +(() => { /**/ return 0; }); +(() => { return /**/ 0; }); +(() => { return 0 /**/ }); +(() => { return 0 /**/; }); +(() => { return 0; /**/ }); + diff --git a/test/rules/arrow-return-shorthand/default/test.js.lint b/test/rules/arrow-return-shorthand/default/test.js.lint new file mode 100644 index 00000000000..aa44465676c --- /dev/null +++ b/test/rules/arrow-return-shorthand/default/test.js.lint @@ -0,0 +1,32 @@ +// Copy of test.ts.lint + +// Invalid: +(() => { return 0; }); + ~~~~~~~~~~~~~ [0] +(() => { return { x: 1 } }); + ~~~~~~~~~~~~~~~~~~~ [0] +(() => { + return 0; +}); + +// Valid: +(() => 0); +(() => {}); +(() => { throw 0; }) +(() => { const x = 0; return x; }); + +// No fix if there's a comment. +(() => /**/ { return 0; }); + ~~~~~~~~~~~~~ [0] +(() => { /**/ return 0; }); + ~~~~~~~~~~~~~~~~~~ [0] +(() => { return /**/ 0; }); + ~~~~~~~~~~~~~~~~~~ [0] +(() => { return 0 /**/ }); + ~~~~~~~~~~~~~~~~~ [0] +(() => { return 0 /**/; }); + ~~~~~~~~~~~~~~~~~~ [0] +(() => { return 0; /**/ }); + ~~~~~~~~~~~~~~~~~~ [0] + +[0]: This arrow function body can be simplified by omitting the curly braces and the keyword 'return'. diff --git a/test/rules/arrow-return-shorthand/default/test.ts.fix b/test/rules/arrow-return-shorthand/default/test.ts.fix new file mode 100644 index 00000000000..2c485de7c8f --- /dev/null +++ b/test/rules/arrow-return-shorthand/default/test.ts.fix @@ -0,0 +1,21 @@ +// Invalid: +(() => 0); +(() => ({ x: 1 })); +(() => { + return 0; +}); + +// Valid: +(() => 0); +(() => {}); +(() => { throw 0; }) +(() => { const x = 0; return x; }); + +// No fix if there's a comment. +(() => /**/ { return 0; }); +(() => { /**/ return 0; }); +(() => { return /**/ 0; }); +(() => { return 0 /**/ }); +(() => { return 0 /**/; }); +(() => { return 0; /**/ }); + diff --git a/test/rules/arrow-return-shorthand/default/test.ts.lint b/test/rules/arrow-return-shorthand/default/test.ts.lint new file mode 100644 index 00000000000..a980b720ea9 --- /dev/null +++ b/test/rules/arrow-return-shorthand/default/test.ts.lint @@ -0,0 +1,30 @@ +// Invalid: +(() => { return 0; }); + ~~~~~~~~~~~~~ [0] +(() => { return { x: 1 } }); + ~~~~~~~~~~~~~~~~~~~ [0] +(() => { + return 0; +}); + +// Valid: +(() => 0); +(() => {}); +(() => { throw 0; }) +(() => { const x = 0; return x; }); + +// No fix if there's a comment. +(() => /**/ { return 0; }); + ~~~~~~~~~~~~~ [0] +(() => { /**/ return 0; }); + ~~~~~~~~~~~~~~~~~~ [0] +(() => { return /**/ 0; }); + ~~~~~~~~~~~~~~~~~~ [0] +(() => { return 0 /**/ }); + ~~~~~~~~~~~~~~~~~ [0] +(() => { return 0 /**/; }); + ~~~~~~~~~~~~~~~~~~ [0] +(() => { return 0; /**/ }); + ~~~~~~~~~~~~~~~~~~ [0] + +[0]: This arrow function body can be simplified by omitting the curly braces and the keyword 'return'. diff --git a/test/rules/arrow-return-shorthand/default/tslint.json b/test/rules/arrow-return-shorthand/default/tslint.json new file mode 100644 index 00000000000..09560397671 --- /dev/null +++ b/test/rules/arrow-return-shorthand/default/tslint.json @@ -0,0 +1,8 @@ +{ + "rules": { + "arrow-return-shorthand": true + }, + "jsRules": { + "arrow-return-shorthand": true + } +} diff --git a/test/rules/arrow-return-shorthand/multiline/test.ts.fix b/test/rules/arrow-return-shorthand/multiline/test.ts.fix new file mode 100644 index 00000000000..f80bb6ed8f4 --- /dev/null +++ b/test/rules/arrow-return-shorthand/multiline/test.ts.fix @@ -0,0 +1,20 @@ +// Invalid: +(() => 0); +(() => ({ x: 1 })); +(() => + 0); + +// Valid: +(() => 0); +(() => {}); +(() => { throw 0; }) +(() => { const x = 0; return x; }); + +// No fix if there's a comment. +(() => /**/ { return 0; }); +(() => { /**/ return 0; }); +(() => { return /**/ 0; }); +(() => { return 0 /**/ }); +(() => { return 0 /**/; }); +(() => { return 0; /**/ }); + diff --git a/test/rules/arrow-return-shorthand/multiline/test.ts.lint b/test/rules/arrow-return-shorthand/multiline/test.ts.lint new file mode 100644 index 00000000000..aba75445edf --- /dev/null +++ b/test/rules/arrow-return-shorthand/multiline/test.ts.lint @@ -0,0 +1,33 @@ +// Invalid: +(() => { return 0; }); + ~~~~~~~~~~~~~ [0] +(() => { return { x: 1 } }); + ~~~~~~~~~~~~~~~~~~~ [0] +(() => { + ~ + return 0; +~~~~~~~~~~~~~ +}); +~ [0] + +// Valid: +(() => 0); +(() => {}); +(() => { throw 0; }) +(() => { const x = 0; return x; }); + +// No fix if there's a comment. +(() => /**/ { return 0; }); + ~~~~~~~~~~~~~ [0] +(() => { /**/ return 0; }); + ~~~~~~~~~~~~~~~~~~ [0] +(() => { return /**/ 0; }); + ~~~~~~~~~~~~~~~~~~ [0] +(() => { return 0 /**/ }); + ~~~~~~~~~~~~~~~~~ [0] +(() => { return 0 /**/; }); + ~~~~~~~~~~~~~~~~~~ [0] +(() => { return 0; /**/ }); + ~~~~~~~~~~~~~~~~~~ [0] + +[0]: This arrow function body can be simplified by omitting the curly braces and the keyword 'return'. diff --git a/test/rules/arrow-return-shorthand/multiline/tslint.json b/test/rules/arrow-return-shorthand/multiline/tslint.json new file mode 100644 index 00000000000..429bf4cc655 --- /dev/null +++ b/test/rules/arrow-return-shorthand/multiline/tslint.json @@ -0,0 +1,5 @@ +{ + "rules": { + "arrow-return-shorthand": [true, "multiline"] + } +} From 5ff7615a4fb02d47585cf5b5a13ebc0eebab34ae Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Fri, 6 Jan 2017 22:58:23 -0800 Subject: [PATCH 005/131] no-magic-numbers: Allow default parameter values to be number literals (#2004) --- src/rules/noMagicNumbersRule.ts | 1 + test/executable/executableTests.ts | 3 +-- test/rules/no-magic-numbers/default/test.ts.lint | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/rules/noMagicNumbersRule.ts b/src/rules/noMagicNumbersRule.ts index e2c60a26309..bbbad8e7885 100644 --- a/src/rules/noMagicNumbersRule.ts +++ b/src/rules/noMagicNumbersRule.ts @@ -57,6 +57,7 @@ export class Rule extends Lint.Rules.AbstractRule { [ts.SyntaxKind.VariableDeclarationList]: true, [ts.SyntaxKind.EnumMember]: true, [ts.SyntaxKind.PropertyDeclaration]: true, + [ts.SyntaxKind.Parameter]: true, }; public static DEFAULT_ALLOWED = [ -1, 0, 1 ]; diff --git a/test/executable/executableTests.ts b/test/executable/executableTests.ts index 110b042421e..a28cffe3832 100644 --- a/test/executable/executableTests.ts +++ b/test/executable/executableTests.ts @@ -25,7 +25,6 @@ const EXECUTABLE_DIR = path.resolve(process.cwd(), "test", "executable"); const EXECUTABLE_PATH = path.resolve(EXECUTABLE_DIR, "npm-like-executable"); const TEMP_JSON_PATH = path.resolve(EXECUTABLE_DIR, "tslint.json"); -/* tslint:disable:only-arrow-functions */ describe("Executable", function(this: Mocha.ISuiteCallbackContext) { // tslint:disable:no-invalid-this this.slow(3000); // the executable is JIT-ed each time it runs; avoid showing slowness warnings @@ -289,7 +288,7 @@ function execCli(args: string[], options: cp.ExecFileOptions, cb: ExecFileCallba function execCli(args: string[], options: cp.ExecFileOptions | ExecFileCallback, cb?: ExecFileCallback): cp.ChildProcess { let filePath = EXECUTABLE_PATH; - // Specify extension for Windows executable to avoid ENOENT errors + // Specify extension for Windows executable to avoid ENOENT errors if (os.platform() === "win32") { filePath += ".cmd"; } diff --git a/test/rules/no-magic-numbers/default/test.ts.lint b/test/rules/no-magic-numbers/default/test.ts.lint index ec6214e7159..0849808a712 100644 --- a/test/rules/no-magic-numbers/default/test.ts.lint +++ b/test/rules/no-magic-numbers/default/test.ts.lint @@ -22,7 +22,6 @@ class A { static test = 1337; constructor (private a = 1337) { - ~~~~ ['magic numbers' are not allowed] } } From 4ee78397af558db8f40c24f0e8c1d4a229b62ada Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Sat, 7 Jan 2017 09:04:43 -0800 Subject: [PATCH 006/131] Set `--lib es6`, and use `Map`/`Set` methods where possible. (#1984) --- docs/_data/rules.json | 17 +++++-- docs/rules/no-unused-expression/index.html | 30 ++++++++++-- package.json | 8 ++-- scripts/tsconfig.json | 3 +- src/enableDisableRules.ts | 7 +-- src/formatterLoader.ts | 2 +- src/formatters/msbuildFormatter.ts | 6 +-- src/formatters/proseFormatter.ts | 11 ++--- src/language/languageServiceHost.ts | 23 ++++----- src/language/utils.ts | 12 +---- .../walker/skippableTokenAwareRuleWalker.ts | 18 +++---- src/ruleLoader.ts | 5 +- src/rules/adjacentOverloadSignaturesRule.ts | 6 +-- src/rules/commentFormatRule.ts | 6 +-- src/rules/completedDocsRule.ts | 8 ++-- src/rules/jsdocFormatRule.ts | 6 +-- src/rules/noMagicNumbersRule.ts | 40 +++++++--------- src/rules/noTrailingWhitespaceRule.ts | 6 +-- src/rules/noUseBeforeDeclareRule.ts | 10 ++-- src/rules/preferConstRule.ts | 47 ++++++++++--------- src/rules/preferForOfRule.ts | 26 ++++------ src/rules/unifiedSignaturesRule.ts | 18 +++---- src/rules/whitespaceRule.ts | 5 +- src/test/parse.ts | 25 +++++----- src/tsconfig.json | 1 + src/utils.ts | 8 ++++ test/tsconfig.json | 1 + 27 files changed, 183 insertions(+), 172 deletions(-) diff --git a/docs/_data/rules.json b/docs/_data/rules.json index 5d5528fa11c..fa358c66214 100644 --- a/docs/_data/rules.json +++ b/docs/_data/rules.json @@ -951,10 +951,21 @@ "description": "Disallows unused expression statements.", "descriptionDetails": "\nUnused expressions are expression statements which are not assignments or function calls\n(and thus usually no-ops).", "rationale": "\nDetects potential errors where an assignment or function call was intended.", - "optionsDescription": "Not configurable.", - "options": null, + "optionsDescription": "\nOne argument may be optionally provided:\n\n* `allow-fast-null-checks` allows to use logical operators to perform fast null checks and perform\nmethod or function calls for side effects (e.g. `e && e.preventDefault()`).", + "options": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "allow-fast-null-checks" + ] + }, + "minLength": 0, + "maxLength": 1 + }, "optionExamples": [ - "true" + "true", + "[true, \"allow-fast-null-checks\"]" ], "type": "functionality", "typescriptOnly": false diff --git a/docs/rules/no-unused-expression/index.html b/docs/rules/no-unused-expression/index.html index 82075de330a..6856223df77 100644 --- a/docs/rules/no-unused-expression/index.html +++ b/docs/rules/no-unused-expression/index.html @@ -8,13 +8,37 @@ rationale: |- Detects potential errors where an assignment or function call was intended. -optionsDescription: Not configurable. -options: null +optionsDescription: |- + + One argument may be optionally provided: + + * `allow-fast-null-checks` allows to use logical operators to perform fast null checks and perform + method or function calls for side effects (e.g. `e && e.preventDefault()`). +options: + type: array + items: + type: string + enum: + - allow-fast-null-checks + minLength: 0 + maxLength: 1 optionExamples: - 'true' + - '[true, "allow-fast-null-checks"]' type: functionality typescriptOnly: false layout: rule title: 'Rule: no-unused-expression' -optionsJSON: 'null' +optionsJSON: |- + { + "type": "array", + "items": { + "type": "string", + "enum": [ + "allow-fast-null-checks" + ] + }, + "minLength": 0, + "maxLength": 1 + } --- \ No newline at end of file diff --git a/package.json b/package.json index 4a5c52f02c0..d2c4fcfe6d5 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,6 @@ "glob": "^7.1.1", "optimist": "~0.6.0", "resolve": "^1.1.7", - "underscore.string": "^3.3.4", "update-notifier": "^1.0.2" }, "peerDependencies": { @@ -60,8 +59,6 @@ "@types/node": "^6.0.56", "@types/optimist": "0.0.29", "@types/resolve": "0.0.4", - "@types/underscore": "^1.7.36", - "@types/underscore.string": "0.0.30", "chai": "^3.5.0", "js-yaml": "^3.7.0", "mocha": "^3.2.0", @@ -71,5 +68,8 @@ "tslint-test-config-non-relative": "file:test/external/tslint-test-config-non-relative", "typescript": "2.1.4" }, - "license": "Apache-2.0" + "license": "Apache-2.0", + "engines": { + "node": ">=4.2.6" + } } diff --git a/scripts/tsconfig.json b/scripts/tsconfig.json index a0ebb235e95..982fca24da4 100644 --- a/scripts/tsconfig.json +++ b/scripts/tsconfig.json @@ -6,6 +6,7 @@ "noUnusedParameters": true, "noUnusedLocals": true, "sourceMap": true, - "target": "es5" + "target": "es5", + "lib": ["es6"] } } diff --git a/src/enableDisableRules.ts b/src/enableDisableRules.ts index 5b20d65cbe6..04a5d3d2924 100644 --- a/src/enableDisableRules.ts +++ b/src/enableDisableRules.ts @@ -47,10 +47,11 @@ export class EnableDisableRulesWalker extends SkippableTokenAwareRuleWalker { scanAllTokens(scan, (scanner: ts.Scanner) => { const startPos = scanner.getStartPos(); - if (this.tokensToSkipStartEndMap[startPos] != null) { + const skip = this.getSkipEndFromStart(startPos); + if (skip !== undefined) { // tokens to skip are places where the scanner gets confused about what the token is, without the proper context // (specifically, regex, identifiers, and templates). So skip those tokens. - scanner.setTextPos(this.tokensToSkipStartEndMap[startPos]); + scanner.setTextPos(skip); return; } @@ -117,7 +118,7 @@ export class EnableDisableRulesWalker extends SkippableTokenAwareRuleWalker { rulesList = commentTextParts[1].split(/\s+/).slice(1); // remove empty items and potential comment end. - rulesList = rulesList.filter((item) => !!item && item.indexOf("*/") === -1); + rulesList = rulesList.filter((item) => !!item && !item.includes("*/")); // potentially there were no items, so default to `all`. rulesList = rulesList.length > 0 ? rulesList : ["all"]; diff --git a/src/formatterLoader.ts b/src/formatterLoader.ts index 1e4256347da..32f7b09cdbc 100644 --- a/src/formatterLoader.ts +++ b/src/formatterLoader.ts @@ -17,7 +17,7 @@ import * as fs from "fs"; import * as path from "path"; -import {camelize} from "underscore.string"; +import {camelize} from "./utils"; const moduleDirectory = path.dirname(module.filename); const CORE_FORMATTERS_DIRECTORY = path.resolve(moduleDirectory, ".", "formatters"); diff --git a/src/formatters/msbuildFormatter.ts b/src/formatters/msbuildFormatter.ts index 40f92f80d83..8af372d9602 100644 --- a/src/formatters/msbuildFormatter.ts +++ b/src/formatters/msbuildFormatter.ts @@ -15,20 +15,18 @@ * limitations under the License. */ -import {camelize} from "underscore.string"; - import {AbstractFormatter} from "../language/formatter/abstractFormatter"; import {IFormatterMetadata} from "../language/formatter/formatter"; import {RuleFailure} from "../language/rule/rule"; -import * as Utils from "../utils"; +import {camelize, dedent} from "../utils"; export class Formatter extends AbstractFormatter { /* tslint:disable:object-literal-sort-keys */ public static metadata: IFormatterMetadata = { formatterName: "msbuild", description: "Formats errors for consumption by msbuild.", - descriptionDetails: Utils.dedent` + descriptionDetails: dedent` The output is compatible with both msbuild and Visual Studio. All failures have the 'warning' severity.`, sample: "myFile.ts(1,14): warning: Missing semicolon", diff --git a/src/formatters/proseFormatter.ts b/src/formatters/proseFormatter.ts index 7128b4ca357..997412c388f 100644 --- a/src/formatters/proseFormatter.ts +++ b/src/formatters/proseFormatter.ts @@ -36,17 +36,12 @@ export class Formatter extends AbstractFormatter { const fixLines: string[] = []; if (fixes) { - const perFileFixes: { [fileName: string]: number } = {}; + const perFileFixes = new Map(); for (const fix of fixes) { - if (perFileFixes[fix.getFileName()] == null) { - perFileFixes[fix.getFileName()] = 1; - } else { - perFileFixes[fix.getFileName()]++; - } + perFileFixes.set(fix.getFileName(), (perFileFixes.get(fix.getFileName()) || 0) + 1); } - Object.keys(perFileFixes).forEach((fixedFile: string) => { - const fixCount = perFileFixes[fixedFile]; + perFileFixes.forEach((fixCount, fixedFile) => { fixLines.push(`Fixed ${fixCount} error(s) in ${fixedFile}`); }); fixLines.push(""); // add a blank line between fixes and failures diff --git a/src/language/languageServiceHost.ts b/src/language/languageServiceHost.ts index 3f9a70f2d16..bab1df1c031 100644 --- a/src/language/languageServiceHost.ts +++ b/src/language/languageServiceHost.ts @@ -24,31 +24,32 @@ interface LanguageServiceEditableHost extends ts.LanguageServiceHost { } export function wrapProgram(program: ts.Program): ts.LanguageService { - const files: {[name: string]: string} = {}; - const fileVersions: {[name: string]: number} = {}; + const files = new Map(); // file name -> content + const fileVersions = new Map(); const host: LanguageServiceEditableHost = { getCompilationSettings: () => program.getCompilerOptions(), getCurrentDirectory: () => program.getCurrentDirectory(), getDefaultLibFileName: () => "lib.d.ts", getScriptFileNames: () => program.getSourceFiles().map((sf) => sf.fileName), getScriptSnapshot: (name: string) => { - if (files.hasOwnProperty(name)) { - return ts.ScriptSnapshot.fromString(files[name]); + const file = files.get(name); + if (file !== undefined) { + return ts.ScriptSnapshot.fromString(file); } if (!program.getSourceFile(name)) { return undefined; } return ts.ScriptSnapshot.fromString(program.getSourceFile(name).getFullText()); }, - getScriptVersion: (name: string) => fileVersions.hasOwnProperty(name) ? fileVersions[name] + "" : "1", + getScriptVersion: (name: string) => { + const version = fileVersions.get(name); + return version === undefined ? "1" : String(version); + }, log: () => { /* */ }, editFile(fileName: string, newContent: string) { - files[fileName] = newContent; - if (fileVersions.hasOwnProperty(fileName)) { - fileVersions[fileName]++; - } else { - fileVersions[fileName] = 0; - } + files.set(fileName, newContent); + const prevVersion = fileVersions.get(fileName); + fileVersions.set(fileName, prevVersion === undefined ? 0 : prevVersion + 1); }, }; const langSvc = ts.createLanguageService(host, ts.createDocumentRegistry()); diff --git a/src/language/utils.ts b/src/language/utils.ts index 52bbbb4dc14..b79bff5b45f 100644 --- a/src/language/utils.ts +++ b/src/language/utils.ts @@ -117,22 +117,12 @@ export function getBindingElementVariableDeclaration(node: ts.BindingElement): t return currentParent as ts.VariableDeclaration; } -/** Shim of Array.find */ -function find(a: T[], predicate: (value: T) => boolean): T | undefined { - for (const value of a) { - if (predicate(value)) { - return value; - } - } - return undefined; -} - /** * Finds a child of a given node with a given kind. * Note: This uses `node.getChildren()`, which does extra parsing work to include tokens. */ export function childOfKind(node: ts.Node, kind: ts.SyntaxKind): ts.Node | undefined { - return find(node.getChildren(), (child) => child.kind === kind); + return node.getChildren().find((child) => child.kind === kind); } /** diff --git a/src/language/walker/skippableTokenAwareRuleWalker.ts b/src/language/walker/skippableTokenAwareRuleWalker.ts index e3a6e6857ce..5f1a3d28a7c 100644 --- a/src/language/walker/skippableTokenAwareRuleWalker.ts +++ b/src/language/walker/skippableTokenAwareRuleWalker.ts @@ -17,16 +17,10 @@ import * as ts from "typescript"; -import {IOptions} from "../rule/rule"; import {RuleWalker} from "./ruleWalker"; export class SkippableTokenAwareRuleWalker extends RuleWalker { - protected tokensToSkipStartEndMap: {[start: number]: number}; - - constructor(sourceFile: ts.SourceFile, options: IOptions) { - super(sourceFile, options); - this.tokensToSkipStartEndMap = {}; - } + private tokensToSkipStartEndMap = new Map(); protected visitRegularExpressionLiteral(node: ts.Node) { this.addTokenToSkipFromNode(node); @@ -44,9 +38,15 @@ export class SkippableTokenAwareRuleWalker extends RuleWalker { } protected addTokenToSkipFromNode(node: ts.Node) { - if (node.getStart() < node.getEnd()) { + const start = node.getStart(); + const end = node.getEnd(); + if (start < end) { // only add to the map nodes whose end comes after their start, to prevent infinite loops - this.tokensToSkipStartEndMap[node.getStart()] = node.getEnd(); + this.tokensToSkipStartEndMap.set(start, end); } } + + protected getSkipEndFromStart(start: number): number | undefined { + return this.tokensToSkipStartEndMap.get(start); + } } diff --git a/src/ruleLoader.ts b/src/ruleLoader.ts index 2ba56032387..9ca7859370a 100644 --- a/src/ruleLoader.ts +++ b/src/ruleLoader.ts @@ -17,11 +17,10 @@ import * as fs from "fs"; import * as path from "path"; -import {camelize} from "underscore.string"; import {getRulesDirectories} from "./configuration"; import {IDisabledInterval, IRule} from "./language/rule/rule"; -import {dedent} from "./utils"; +import {camelize, dedent} from "./utils"; const moduleDirectory = path.dirname(module.filename); const CORE_RULES_DIRECTORY = path.resolve(moduleDirectory, ".", "rules"); @@ -151,7 +150,7 @@ function buildDisabledIntervalsFromSwitches(ruleSpecificList: IEnableDisablePosi while (i < ruleSpecificList.length) { const startPosition = ruleSpecificList[i].position; - // rule enabled state is always alternating therefore we can use position of next switch as end of disabled interval + // rule enabled state is always alternating therefore we can use position of next switch as end of disabled interval // set endPosition as Infinity in case when last switch for rule in a file is disabled const endPosition = ruleSpecificList[i + 1] ? ruleSpecificList[i + 1].position : Infinity; diff --git a/src/rules/adjacentOverloadSignaturesRule.ts b/src/rules/adjacentOverloadSignaturesRule.ts index 72954eb0caf..eb896aefee6 100644 --- a/src/rules/adjacentOverloadSignaturesRule.ts +++ b/src/rules/adjacentOverloadSignaturesRule.ts @@ -90,15 +90,15 @@ class AdjacentOverloadSignaturesWalker extends Lint.RuleWalker { /** 'getOverloadName' may return undefined for nodes that cannot be overloads, e.g. a `const` declaration. */ private checkOverloadsAdjacent(overloads: T[], getOverload: (node: T) => Overload | undefined) { let lastKey: string | undefined = undefined; - const seen: { [key: string]: true } = Object.create(null); + const seen = new Set(); for (const node of overloads) { const overload = getOverload(node); if (overload) { const { name, key } = overload; - if (key in seen && lastKey !== key) { + if (seen.has(key) && lastKey !== key) { this.addFailureAtNode(node, Rule.FAILURE_STRING_FACTORY(name)); } - seen[key] = true; + seen.add(key); lastKey = key; } else { lastKey = undefined; diff --git a/src/rules/commentFormatRule.ts b/src/rules/commentFormatRule.ts index e65a16b3fc3..b058c185af3 100644 --- a/src/rules/commentFormatRule.ts +++ b/src/rules/commentFormatRule.ts @@ -64,11 +64,11 @@ class CommentWalker extends Lint.SkippableTokenAwareRuleWalker { public visitSourceFile(node: ts.SourceFile) { super.visitSourceFile(node); Lint.scanAllTokens(ts.createScanner(ts.ScriptTarget.ES5, false, ts.LanguageVariant.Standard, node.text), (scanner: ts.Scanner) => { - const startPos = scanner.getStartPos(); - if (this.tokensToSkipStartEndMap[startPos] != null) { + const skip = this.getSkipEndFromStart(scanner.getStartPos()); + if (skip !== undefined) { // tokens to skip are places where the scanner gets confused about what the token is, without the proper context // (specifically, regex, identifiers, and templates). So skip those tokens. - scanner.setTextPos(this.tokensToSkipStartEndMap[startPos]); + scanner.setTextPos(skip); return; } diff --git a/src/rules/completedDocsRule.ts b/src/rules/completedDocsRule.ts index a1bdcc667c6..3d4aa5db9c0 100644 --- a/src/rules/completedDocsRule.ts +++ b/src/rules/completedDocsRule.ts @@ -70,12 +70,10 @@ export class Rule extends Lint.Rules.TypedRule { } export class CompletedDocsWalker extends Lint.ProgramAwareRuleWalker { - private nodesToCheck: { [i: string]: boolean } = {}; + private nodesToCheck: Set; public setNodesToCheck(nodesToCheck: string[]): void { - for (const nodeType of nodesToCheck) { - this.nodesToCheck[nodeType] = true; - } + this.nodesToCheck = new Set(nodesToCheck); } public visitClassDeclaration(node: ts.ClassDeclaration): void { @@ -99,7 +97,7 @@ export class CompletedDocsWalker extends Lint.ProgramAwareRuleWalker { } private checkComments(node: ts.Declaration, nodeToCheck: string): void { - if (!this.nodesToCheck[nodeToCheck] || node.name === undefined) { + if (!this.nodesToCheck.has(nodeToCheck) || node.name === undefined) { return; } diff --git a/src/rules/jsdocFormatRule.ts b/src/rules/jsdocFormatRule.ts index 784b8923fc8..5322276b1ab 100644 --- a/src/rules/jsdocFormatRule.ts +++ b/src/rules/jsdocFormatRule.ts @@ -52,11 +52,11 @@ class JsdocWalker extends Lint.SkippableTokenAwareRuleWalker { public visitSourceFile(node: ts.SourceFile) { super.visitSourceFile(node); Lint.scanAllTokens(ts.createScanner(ts.ScriptTarget.ES5, false, ts.LanguageVariant.Standard, node.text), (scanner: ts.Scanner) => { - const startPos = scanner.getStartPos(); - if (this.tokensToSkipStartEndMap[startPos] != null) { + const skip = this.getSkipEndFromStart(scanner.getStartPos()); + if (skip !== undefined) { // tokens to skip are places where the scanner gets confused about what the token is, without the proper context // (specifically, regex, identifiers, and templates). So skip those tokens. - scanner.setTextPos(this.tokensToSkipStartEndMap[startPos]); + scanner.setTextPos(skip); return; } diff --git a/src/rules/noMagicNumbersRule.ts b/src/rules/noMagicNumbersRule.ts index bbbad8e7885..26d9b07a679 100644 --- a/src/rules/noMagicNumbersRule.ts +++ b/src/rules/noMagicNumbersRule.ts @@ -47,18 +47,18 @@ export class Rule extends Lint.Rules.AbstractRule { public static FAILURE_STRING = "'magic numbers' are not allowed"; - public static ALLOWED_NODES = { - [ts.SyntaxKind.ExportAssignment]: true, - [ts.SyntaxKind.FirstAssignment]: true, - [ts.SyntaxKind.LastAssignment]: true, - [ts.SyntaxKind.PropertyAssignment]: true, - [ts.SyntaxKind.ShorthandPropertyAssignment]: true, - [ts.SyntaxKind.VariableDeclaration]: true, - [ts.SyntaxKind.VariableDeclarationList]: true, - [ts.SyntaxKind.EnumMember]: true, - [ts.SyntaxKind.PropertyDeclaration]: true, - [ts.SyntaxKind.Parameter]: true, - }; + public static ALLOWED_NODES = new Set([ + ts.SyntaxKind.ExportAssignment, + ts.SyntaxKind.FirstAssignment, + ts.SyntaxKind.LastAssignment, + ts.SyntaxKind.PropertyAssignment, + ts.SyntaxKind.ShorthandPropertyAssignment, + ts.SyntaxKind.VariableDeclaration, + ts.SyntaxKind.VariableDeclarationList, + ts.SyntaxKind.EnumMember, + ts.SyntaxKind.PropertyDeclaration, + ts.SyntaxKind.Parameter, + ]); public static DEFAULT_ALLOWED = [ -1, 0, 1 ]; @@ -69,25 +69,21 @@ export class Rule extends Lint.Rules.AbstractRule { class NoMagicNumbersWalker extends Lint.RuleWalker { // lookup object for allowed magic numbers - private allowed: { [prop: string]: boolean } = {}; + private allowed: Set; constructor(sourceFile: ts.SourceFile, options: IOptions) { super(sourceFile, options); const configOptions = this.getOptions(); const allowedNumbers: number[] = configOptions.length > 0 ? configOptions : Rule.DEFAULT_ALLOWED; - - allowedNumbers.forEach((value) => { - this.allowed[value] = true; - }); + this.allowed = new Set(allowedNumbers.map(String)); } public visitNode(node: ts.Node) { const isUnary = this.isUnaryNumericExpression(node); - if (node.kind === ts.SyntaxKind.NumericLiteral && node.parent !== undefined && !Rule.ALLOWED_NODES[node.parent.kind] || isUnary) { - const text = node.getText(); - if (!this.allowed[text]) { - this.addFailureAtNode(node, Rule.FAILURE_STRING); - } + const isNumber = node.kind === ts.SyntaxKind.NumericLiteral && !Rule.ALLOWED_NODES.has(node.parent!.kind); + const isMagicNumber = (isNumber || isUnary) && !this.allowed.has(node.getText()); + if (isMagicNumber) { + this.addFailureAtNode(node, Rule.FAILURE_STRING); } if (!isUnary) { super.visitNode(node); diff --git a/src/rules/noTrailingWhitespaceRule.ts b/src/rules/noTrailingWhitespaceRule.ts index 2e25a31f96e..ddc15ef3e70 100644 --- a/src/rules/noTrailingWhitespaceRule.ts +++ b/src/rules/noTrailingWhitespaceRule.ts @@ -46,11 +46,11 @@ class NoTrailingWhitespaceWalker extends Lint.SkippableTokenAwareRuleWalker { let lastSeenWasWhitespace = false; let lastSeenWhitespacePosition = 0; Lint.scanAllTokens(ts.createScanner(ts.ScriptTarget.ES5, false, ts.LanguageVariant.Standard, node.text), (scanner: ts.Scanner) => { - const startPos = scanner.getStartPos(); - if (this.tokensToSkipStartEndMap[startPos] != null) { + const skip = this.getSkipEndFromStart(scanner.getStartPos()); + if (skip !== undefined) { // tokens to skip are places where the scanner gets confused about what the token is, without the proper context // (specifically, regex, identifiers, and templates). So skip those tokens. - scanner.setTextPos(this.tokensToSkipStartEndMap[startPos]); + scanner.setTextPos(skip); lastSeenWasWhitespace = false; return; } diff --git a/src/rules/noUseBeforeDeclareRule.ts b/src/rules/noUseBeforeDeclareRule.ts index 8a07b56a5b1..1dcdbbff6f7 100644 --- a/src/rules/noUseBeforeDeclareRule.ts +++ b/src/rules/noUseBeforeDeclareRule.ts @@ -43,9 +43,7 @@ export class Rule extends Lint.Rules.AbstractRule { } } -interface VisitedVariables { - [varName: string]: boolean; -} +type VisitedVariables = Set; class NoUseBeforeDeclareWalker extends Lint.ScopeAwareRuleWalker { private importedPropertiesPositions: number[] = []; @@ -55,7 +53,7 @@ class NoUseBeforeDeclareWalker extends Lint.ScopeAwareRuleWalker(); } public visitBindingElement(node: ts.BindingElement) { @@ -112,11 +110,11 @@ class NoUseBeforeDeclareWalker extends Lint.ScopeAwareRuleWalker { } public onBlockScopeEnd() { - const seenLetStatements: { [startPosition: string]: boolean } = {}; + const seenLetStatements = new Set(); for (const usage of this.getCurrentBlockScope().getConstCandiates()) { let fix: Lint.Fix | undefined; - if (!usage.reassignedSibling && !seenLetStatements[usage.letStatement.getStart().toString()]) { + if (!usage.reassignedSibling && !seenLetStatements.has(usage.letStatement)) { // only fix if all variables in the `let` statement can use `const` fix = this.createFix(this.createReplacement(usage.letStatement.getStart(), "let".length, "const")); - seenLetStatements[usage.letStatement.getStart().toString()] = true; + seenLetStatements.add(usage.letStatement); } this.addFailureAtNode(usage.identifier, Rule.FAILURE_STRING_FACTORY(usage.identifier.text), fix); } @@ -203,34 +203,34 @@ interface IConstCandidate { reassignedSibling: boolean; } +interface UsageInfo { + letStatement: ts.VariableDeclarationList; + identifier: ts.Identifier; + usageCount: number; +} + class ScopeInfo { public currentVariableDeclaration: ts.VariableDeclaration; - private identifierUsages: { - [varName: string]: { - letStatement: ts.VariableDeclarationList, - identifier: ts.Identifier, - usageCount: number, - }, - } = {}; + private identifierUsages = new Map(); // variable names grouped by common `let` statements - private sharedLetSets: {[letStartIndex: string]: string[]} = {}; + private sharedLetSets = new Map(); public addVariable(identifier: ts.Identifier, letStatement: ts.VariableDeclarationList) { - this.identifierUsages[identifier.text] = { letStatement, identifier, usageCount: 0 }; - const letSetKey = letStatement.getStart().toString(); - if (this.sharedLetSets[letSetKey] == null) { - this.sharedLetSets[letSetKey] = []; + this.identifierUsages.set(identifier.text, { letStatement, identifier, usageCount: 0 }); + let shared = this.sharedLetSets.get(letStatement); + if (shared === undefined) { + shared = []; + this.sharedLetSets.set(letStatement, shared); } - this.sharedLetSets[letSetKey].push(identifier.text); + shared.push(identifier.text); } public getConstCandiates() { const constCandidates: IConstCandidate[] = []; - for (const letSetKey of Object.keys(this.sharedLetSets)) { - const variableNames = this.sharedLetSets[letSetKey]; - const anyReassigned = variableNames.some((key) => this.identifierUsages[key].usageCount > 0); + this.sharedLetSets.forEach((variableNames) => { + const anyReassigned = variableNames.some((key) => this.identifierUsages.get(key)!.usageCount > 0); for (const variableName of variableNames) { - const usage = this.identifierUsages[variableName]; + const usage = this.identifierUsages.get(variableName)!; if (usage.usageCount === 0) { constCandidates.push({ identifier: usage.identifier, @@ -239,13 +239,14 @@ class ScopeInfo { }); } } - } + }); return constCandidates; } public incrementVariableUsage(varName: string) { - if (this.identifierUsages[varName] != null) { - this.identifierUsages[varName].usageCount++; + const usages = this.identifierUsages.get(varName); + if (usages !== undefined) { + usages.usageCount++; return true; } return false; diff --git a/src/rules/preferForOfRule.ts b/src/rules/preferForOfRule.ts index c937ad9d7f5..398c9d3bee8 100644 --- a/src/rules/preferForOfRule.ts +++ b/src/rules/preferForOfRule.ts @@ -47,17 +47,13 @@ interface IIncrementorState { } // a map of incrementors and whether or not they are only used to index into an array reference in the for loop -interface IncrementorMap { - [name: string]: IIncrementorState; -} +type IncrementorMap = Map; -class PreferForOfWalker extends Lint.BlockScopeAwareRuleWalker<{}, IncrementorMap> { - public createScope() { - return {}; - } +class PreferForOfWalker extends Lint.BlockScopeAwareRuleWalker { + public createScope() {} // tslint:disable-line:no-empty public createBlockScope() { - return {}; + return new Map(); } protected visitForStatement(node: ts.ForStatement) { @@ -69,31 +65,31 @@ class PreferForOfWalker extends Lint.BlockScopeAwareRuleWalker<{}, IncrementorMa indexVariableName = indexVariable.getText(); // store `for` loop state - currentBlockScope[indexVariableName] = { + currentBlockScope.set(indexVariableName, { arrayToken: arrayToken as ts.Identifier, forLoopEndPosition: node.incrementor.end + 1, onlyArrayReadAccess: true, - }; + }); } super.visitForStatement(node); if (indexVariableName != null) { - const incrementorState = currentBlockScope[indexVariableName]; + const incrementorState = currentBlockScope.get(indexVariableName)!; if (incrementorState.onlyArrayReadAccess) { this.addFailureFromStartToEnd(node.getStart(), incrementorState.forLoopEndPosition, Rule.FAILURE_STRING); } // remove current `for` loop state - delete currentBlockScope[indexVariableName]; + currentBlockScope.delete(indexVariableName); } } protected visitIdentifier(node: ts.Identifier) { - const incrementorScope = this.findBlockScope((scope) => scope[node.text] != null); + const incrementorScope = this.findBlockScope((scope) => scope.has(node.text)); if (incrementorScope != null) { - const incrementorState = incrementorScope[node.text]; + const incrementorState = incrementorScope.get(node.text); // check if the identifier is an iterator and is currently in the `for` loop body if (incrementorState != null && incrementorState.arrayToken != null && incrementorState.forLoopEndPosition < node.getStart()) { @@ -201,8 +197,6 @@ class PreferForOfWalker extends Lint.BlockScopeAwareRuleWalker<{}, IncrementorMa } } } - } else { - return false; } return false; } diff --git a/src/rules/unifiedSignaturesRule.ts b/src/rules/unifiedSignaturesRule.ts index 66753d08e23..68cf3937cca 100644 --- a/src/rules/unifiedSignaturesRule.ts +++ b/src/rules/unifiedSignaturesRule.ts @@ -208,11 +208,11 @@ function getIsTypeParameter(typeParameters?: ts.TypeParameterDeclaration[]): IsT return () => false; } - const set: { [key: string]: true } = Object.create(null); + const set = new Set(); for (const t of typeParameters) { - set[t.getText()] = true; + set.add(t.getText()); } - return (typeName: string) => set[typeName]; + return (typeName: string) => set.has(typeName); } /** True if any of the outer type parameters are used in a signature. */ @@ -235,9 +235,7 @@ function signatureUsesTypeParameter(sig: ts.SignatureDeclaration, isTypeParamete * Does not rely on overloads being adjacent. This is similar to code in adjacentOverloadSignaturesRule.ts, but not the same. */ function collectOverloads(nodes: T[], getOverload: GetOverload): ts.SignatureDeclaration[][] { - const map: { [key: string]: ts.SignatureDeclaration[] } = Object.create(null); - // Array of values in the map. - const res: ts.SignatureDeclaration[][] = []; + const map = new Map(); for (const sig of nodes) { const overload = getOverload(sig); if (!overload) { @@ -245,16 +243,14 @@ function collectOverloads(nodes: T[], getOverload: GetOverload): ts.Signat } const { signature, key } = overload; - let overloads = map[key]; + const overloads = map.get(key); if (overloads) { overloads.push(signature); } else { - overloads = [signature]; - res.push(overloads); - map[key] = overloads; + map.set(key, [signature]); } } - return res; + return Array.from(map.values()); } function parametersAreEqual(a: ts.ParameterDeclaration, b: ts.ParameterDeclaration): boolean { diff --git a/src/rules/whitespaceRule.ts b/src/rules/whitespaceRule.ts index cdabe97eb5e..775c5893a92 100644 --- a/src/rules/whitespaceRule.ts +++ b/src/rules/whitespaceRule.ts @@ -91,10 +91,11 @@ class WhitespaceWalker extends Lint.SkippableTokenAwareRuleWalker { prevTokenShouldBeFollowedByWhitespace = false; } - if (this.tokensToSkipStartEndMap[startPos] != null) { + const skip = this.getSkipEndFromStart(startPos); + if (skip !== undefined) { // tokens to skip are places where the scanner gets confused about what the token is, without the proper context // (specifically, regex, identifiers, and templates). So skip those tokens. - scanner.setTextPos(this.tokensToSkipStartEndMap[startPos]); + scanner.setTextPos(skip); return; } diff --git a/src/test/parse.ts b/src/test/parse.ts index 0525707bd4c..5bf1272bc37 100644 --- a/src/test/parse.ts +++ b/src/test/parse.ts @@ -51,15 +51,20 @@ export function parseErrorsFromMarkup(text: string): LintError[] { } const messageSubstitutionLines = lines.filter((l) => l instanceof MessageSubstitutionLine) as MessageSubstitutionLine[]; - const messageSubstitutions: { [key: string]: string } = {}; - for (const line of messageSubstitutionLines) { - messageSubstitutions[line.key] = line.message; - } + const messageSubstitutions = new Map(messageSubstitutionLines.map(({ key, message }) => + [key, message] as [string, string])); // errorLineForCodeLine[5] contains all the ErrorLine objects associated with the 5th line of code, for example const errorLinesForCodeLines = createCodeLineNoToErrorsMap(lines); const lintErrors: LintError[] = []; + function addError(errorLine: EndErrorLine, errorStartPos: { line: number, col: number }, lineNo: number) { + lintErrors.push({ + startPos: errorStartPos, + endPos: { line: lineNo, col: errorLine.endCol }, + message: messageSubstitutions.get(errorLine.message) || errorLine.message, + }); + } // for each line of code... errorLinesForCodeLines.forEach((errorLinesForLineOfCode, lineNo) => { @@ -70,11 +75,7 @@ export function parseErrorsFromMarkup(text: string): LintError[] { // if the error starts and ends on this line, add it now to list of errors if (errorLine instanceof EndErrorLine) { - lintErrors.push({ - startPos: errorStartPos, - endPos: { line: lineNo, col: errorLine.endCol }, - message: messageSubstitutions[errorLine.message] || errorLine.message, - }); + addError(errorLine, errorStartPos, lineNo); // if the error is the start of a multiline error } else if (errorLine instanceof MultilineErrorLine) { @@ -90,11 +91,7 @@ export function parseErrorsFromMarkup(text: string): LintError[] { // if end of multiline error, add it it list of errors if (nextErrorLine instanceof EndErrorLine) { - lintErrors.push({ - startPos: errorStartPos, - endPos: { line: nextLineNo, col: nextErrorLine.endCol }, - message: messageSubstitutions[nextErrorLine.message] || nextErrorLine.message, - }); + addError(nextErrorLine, errorStartPos, nextLineNo); break; } } diff --git a/src/tsconfig.json b/src/tsconfig.json index bba9bd7b433..b85f41cf5f2 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -10,6 +10,7 @@ "declaration": true, "sourceMap": false, "target": "es5", + "lib": ["es6"], "outDir": "../lib" } } diff --git a/src/utils.ts b/src/utils.ts index 6dbf524e026..7460eb6464f 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -39,6 +39,14 @@ export function objectify(arg: any): any { } } +/** + * Replace hyphens in a rule name by upper-casing the letter after them. + * E.g. "foo-bar" -> "fooBar" + */ +export function camelize(stringWithHyphens: string): string { + return stringWithHyphens.replace(/-(.)/g, (_, nextLetter) => nextLetter.toUpperCase()); +} + /** * Removes leading indents from a template string without removing all leading whitespace */ diff --git a/test/tsconfig.json b/test/tsconfig.json index de519b752ed..685d2f2fb98 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -9,6 +9,7 @@ "strictNullChecks": true, "sourceMap": true, "target": "es5", + "lib": ["es6"], "outDir": "../build" }, "include": [ From 9979f9d77f602728bc6825803ea7db873d64c961 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Sat, 7 Jan 2017 23:30:16 -0800 Subject: [PATCH 007/131] Clean up `no-shadowed-variable` (#1970) --- src/rules/noShadowedVariableRule.ts | 31 ++++++++++------------------- 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/src/rules/noShadowedVariableRule.ts b/src/rules/noShadowedVariableRule.ts index 20c16f2b1d4..377ecb12720 100644 --- a/src/rules/noShadowedVariableRule.ts +++ b/src/rules/noShadowedVariableRule.ts @@ -42,27 +42,22 @@ export class Rule extends Lint.Rules.AbstractRule { } } -class NoShadowedVariableWalker extends Lint.BlockScopeAwareRuleWalker { +class NoShadowedVariableWalker extends Lint.BlockScopeAwareRuleWalker, Set> { public createScope() { - return new ScopeInfo(); + return new Set(); } public createBlockScope() { - return new ScopeInfo(); + return new Set(); } public visitBindingElement(node: ts.BindingElement) { const isSingleVariable = node.name.kind === ts.SyntaxKind.Identifier; - const variableDeclaration = Lint.getBindingElementVariableDeclaration(node); - if (isSingleVariable) { const name = node.name as ts.Identifier; - if (variableDeclaration) { - const isBlockScopedVariable = Lint.isBlockScopedVariable(variableDeclaration); - this.handleSingleVariableIdentifier(name, isBlockScopedVariable); - } else { - this.handleSingleVariableIdentifier(name, false); - } + const variableDeclaration = Lint.getBindingElementVariableDeclaration(node); + const isBlockScopedVariable = variableDeclaration !== null && Lint.isBlockScopedVariable(variableDeclaration); + this.handleSingleVariableIdentifier(name, isBlockScopedVariable); } super.visitBindingElement(node); @@ -132,23 +127,23 @@ class NoShadowedVariableWalker extends Lint.BlockScopeAwareRuleWalker= 0; + return this.getCurrentScope().has(varName); } private inCurrentBlockScope(varName: string) { - return this.getCurrentBlockScope().variableNames.indexOf(varName) >= 0; + return this.getCurrentBlockScope().has(varName); } private inPreviousBlockScope(varName: string) { return this.getAllBlockScopes().some((scopeInfo) => { - return scopeInfo !== this.getCurrentBlockScope() && scopeInfo.variableNames.indexOf(varName) >= 0 ; + return scopeInfo !== this.getCurrentBlockScope() && scopeInfo.has(varName); }); } @@ -157,7 +152,3 @@ class NoShadowedVariableWalker extends Lint.BlockScopeAwareRuleWalker Date: Sun, 8 Jan 2017 11:15:49 +0200 Subject: [PATCH 008/131] Add exceptions support for comment-format rule (fixes #562) (#1757) --- src/rules/commentFormatRule.ts | 111 ++++++++++++++++-- src/utils.ts | 7 ++ .../exceptions-pattern/test.js.lint | 38 ++++++ .../exceptions-pattern/test.ts.lint | 36 ++++++ .../exceptions-pattern/tslint.json | 8 ++ .../exceptions-words/test.js.lint | 37 ++++++ .../exceptions-words/test.ts.lint | 37 ++++++ .../exceptions-words/tslint.json | 8 ++ test/utilsTests.ts | 13 +- 9 files changed, 285 insertions(+), 10 deletions(-) create mode 100644 test/rules/comment-format/exceptions-pattern/test.js.lint create mode 100644 test/rules/comment-format/exceptions-pattern/test.ts.lint create mode 100644 test/rules/comment-format/exceptions-pattern/tslint.json create mode 100644 test/rules/comment-format/exceptions-words/test.js.lint create mode 100644 test/rules/comment-format/exceptions-words/test.ts.lint create mode 100644 test/rules/comment-format/exceptions-words/tslint.json diff --git a/src/rules/commentFormatRule.ts b/src/rules/commentFormatRule.ts index b058c185af3..199083d966a 100644 --- a/src/rules/commentFormatRule.ts +++ b/src/rules/commentFormatRule.ts @@ -18,6 +18,14 @@ import * as ts from "typescript"; import * as Lint from "../index"; +import { escapeRegExp } from "../utils"; + +interface IExceptionsObject { + ignoreWords?: string[]; + ignorePattern?: string; +} + +type ExceptionsRegExp = RegExp | null; const OPTION_SPACE = "check-space"; const OPTION_LOWERCASE = "check-lowercase"; @@ -35,17 +43,53 @@ export class Rule extends Lint.Rules.AbstractRule { * \`"check-space"\` requires that all single-line comments must begin with a space, as in \`// comment\` * note that comments starting with \`///\` are also allowed, for things such as \`///\` * \`"check-lowercase"\` requires that the first non-whitespace character of a comment must be lowercase, if applicable. - * \`"check-uppercase"\` requires that the first non-whitespace character of a comment must be uppercase, if applicable.`, + * \`"check-uppercase"\` requires that the first non-whitespace character of a comment must be uppercase, if applicable. + + Exceptions to \`"check-lowercase"\` or \`"check-uppercase"\` can be managed with object that may be passed as last argument. + + One of two options can be provided in this object: + + * \`"ignoreWords"\` - array of strings - words that will be ignored at the beginning of the comment. + * \`"ignorePattern"\` - string - RegExp pattern that will be ignored at the beginning of the comment. + `, options: { type: "array", items: { - type: "string", - enum: ["check-space", "check-lowercase", "check-uppercase"], + anyOf: [ + { + type: "string", + enum: [ + "check-space", + "check-lowercase", + "check-uppercase", + ], + }, + { + type: "object", + properties: { + ignoreWords: { + type: "array", + items: { + type: "string", + }, + }, + ignorePattern: { + type: "string", + }, + }, + minProperties: 1, + maxProperties: 1, + }, + ], }, minLength: 1, - maxLength: 3, + maxLength: 4, }, - optionExamples: ['[true, "check-space", "check-lowercase"]'], + optionExamples: [ + '[true, "check-space", "check-uppercase"]', + '[true, "check-lowercase", {"ignoreWords": ["TODO", "HACK"]}]', + '[true, "check-lowercase", {"ignorePattern": "STD\\w{2,3}\\b"}]', + ], type: "style", typescriptOnly: false, }; @@ -54,6 +98,8 @@ export class Rule extends Lint.Rules.AbstractRule { public static LOWERCASE_FAILURE = "comment must start with lowercase letter"; public static UPPERCASE_FAILURE = "comment must start with uppercase letter"; public static LEADING_SPACE_FAILURE = "comment must start with a space"; + public static IGNORE_WORDS_FAILURE_FACTORY = (words: string[]): string => ` or the word(s): ${words.join(", ")}`; + public static IGNORE_PATTERN_FAILURE_FACTORY = (pattern: string): string => ` or its start must match the regex pattern "${pattern}"`; public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { return this.applyWithWalker(new CommentWalker(sourceFile, this.getOptions())); @@ -61,6 +107,15 @@ export class Rule extends Lint.Rules.AbstractRule { } class CommentWalker extends Lint.SkippableTokenAwareRuleWalker { + private exceptionsRegExp: ExceptionsRegExp; + private failureIgnorePart: string = ""; + + constructor(sourceFile: ts.SourceFile, options: Lint.IOptions) { + super(sourceFile, options); + + this.exceptionsRegExp = this.composeExceptionsRegExp(); + } + public visitSourceFile(node: ts.SourceFile) { super.visitSourceFile(node); Lint.scanAllTokens(ts.createScanner(ts.ScriptTarget.ES5, false, ts.LanguageVariant.Standard, node.text), (scanner: ts.Scanner) => { @@ -82,19 +137,57 @@ class CommentWalker extends Lint.SkippableTokenAwareRuleWalker { } } if (this.hasOption(OPTION_LOWERCASE)) { - if (!startsWithLowercase(commentText)) { - this.addFailureAt(startPosition, width, Rule.LOWERCASE_FAILURE); + if (!startsWithLowercase(commentText) && !this.startsWithException(commentText)) { + this.addFailureAt(startPosition, width, Rule.LOWERCASE_FAILURE + this.failureIgnorePart); } } if (this.hasOption(OPTION_UPPERCASE)) { - if (!startsWithUppercase(commentText) && !isEnableDisableFlag(commentText)) { - this.addFailureAt(startPosition, width, Rule.UPPERCASE_FAILURE); + if (!startsWithUppercase(commentText) && !isEnableDisableFlag(commentText) && !this.startsWithException(commentText)) { + this.addFailureAt(startPosition, width, Rule.UPPERCASE_FAILURE + this.failureIgnorePart); } } } }); } + private startsWithException(commentText: string): boolean { + if (this.exceptionsRegExp == null) { + return false; + } + + return this.exceptionsRegExp.test(commentText); + } + + private composeExceptionsRegExp(): ExceptionsRegExp { + const optionsList = this.getOptions() as Array; + const exceptionsObject = optionsList[optionsList.length - 1]; + + // early return if last element is string instead of exceptions object + if (typeof exceptionsObject === "string" || !exceptionsObject) { + return null; + } + + if (exceptionsObject.ignorePattern) { + this.failureIgnorePart = Rule.IGNORE_PATTERN_FAILURE_FACTORY(exceptionsObject.ignorePattern); + // regex is "start of string"//"any amount of whitespace" followed by user provided ignore pattern + return new RegExp(`^//\\s*(${exceptionsObject.ignorePattern})`); + } + + if (exceptionsObject.ignoreWords) { + this.failureIgnorePart = Rule.IGNORE_WORDS_FAILURE_FACTORY(exceptionsObject.ignoreWords); + // Converts all exceptions values to strings, trim whitespace, escapes RegExp special characters and combines into alternation + const wordsPattern = exceptionsObject.ignoreWords + .map(String) + .map((str) => str.trim()) + .map(escapeRegExp) + .join("|"); + + // regex is "start of string"//"any amount of whitespace"("any word from ignore list") followed by non alphanumeric character + return new RegExp(`^//\\s*(${wordsPattern})\\b`); + } + + return null; + } } function startsWith(commentText: string, changeCase: (str: string) => string) { diff --git a/src/utils.ts b/src/utils.ts index 7460eb6464f..d0643016314 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -100,3 +100,10 @@ export function stripComments(content: string): string { }); return result; }; + +/** + * Escapes all special characters in RegExp pattern to avoid broken regular expressions and ensure proper matches + */ +export function escapeRegExp(re: string): string { + return re.replace(/[.+*?|^$[\]{}()\\]/g, "\\$&"); +} diff --git a/test/rules/comment-format/exceptions-pattern/test.js.lint b/test/rules/comment-format/exceptions-pattern/test.js.lint new file mode 100644 index 00000000000..34b4a0818fa --- /dev/null +++ b/test/rules/comment-format/exceptions-pattern/test.js.lint @@ -0,0 +1,38 @@ +class Clazz { // This comment is correct + /* block comment + * adada + */ + public funcxion() { // this comment has a lowercase letter starting it + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [upper] + //this comment is on its own line, and starts with a lowercase _and_ no space + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [upper] + console.log("test"); //This comment has no space + } + /// +} + +//#region test +//#endregion + +`${location.protocol}//${location.hostname}` + +// tslint should show error here + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [upper] + +// tslint: not a rule flag + ~~~~~~~~~~~~~~~~~~~~~~~~ [upper] + +class Invalid {} + +// tslint:disable-next-line:no-unused-expression +class Valid {} + +// todo write more tests + ~~~~~~~~~~~~~~~~~~~~~~ [upper] + +// STDIN for input +// STDOUT for output +// stderr for errors + + +[upper]: comment must start with uppercase letter or its start must match the regex pattern "std(in|out|err)\b" diff --git a/test/rules/comment-format/exceptions-pattern/test.ts.lint b/test/rules/comment-format/exceptions-pattern/test.ts.lint new file mode 100644 index 00000000000..42adb252e09 --- /dev/null +++ b/test/rules/comment-format/exceptions-pattern/test.ts.lint @@ -0,0 +1,36 @@ +class Clazz { // this comment is correct + /* block comment + * adada + */ + public funcxion() { // This comment has a capital letter starting it + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [lower] + //This comment is on its own line, and starts with a capital _and_ no space + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [lower] + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [space] + console.log("test"); //this comment has no space + ~~~~~~~~~~~~~~~~~~~~~~~~~ [space] + } + /// +} + +//#region test +//#endregion + +`${location.protocol}//${location.hostname}` + +//noinspection JSUnusedGlobalSymbols +const unusedVar = 'unneeded value'; + +// TODO: Write more tests + ~~~~~~~~~~~~~~~~~~~~~~~ [lower] +// HACKING is not an exception + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [lower] + +// STDIN for input +// STDOUT for output +// stderr for errors + + + +[lower]: comment must start with lowercase letter or its start must match the regex pattern "STD\w{2,3}" +[space]: comment must start with a space diff --git a/test/rules/comment-format/exceptions-pattern/tslint.json b/test/rules/comment-format/exceptions-pattern/tslint.json new file mode 100644 index 00000000000..012c339dec3 --- /dev/null +++ b/test/rules/comment-format/exceptions-pattern/tslint.json @@ -0,0 +1,8 @@ +{ + "rules": { + "comment-format": [true, "check-space", "check-lowercase", {"ignorePattern": "STD\\w{2,3}"}] + }, + "jsRules": { + "comment-format": [true, "check-uppercase", {"ignorePattern": "std(in|out|err)\\b"}] + } +} diff --git a/test/rules/comment-format/exceptions-words/test.js.lint b/test/rules/comment-format/exceptions-words/test.js.lint new file mode 100644 index 00000000000..309e97d9e1d --- /dev/null +++ b/test/rules/comment-format/exceptions-words/test.js.lint @@ -0,0 +1,37 @@ +class Clazz { // This comment is correct + /* block comment + * adada + */ + public funcxion() { // this comment has a lowercase letter starting it + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [upper] + //this comment is on its own line, and starts with a lowercase _and_ no space + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [upper] + console.log("test"); //This comment has no space + } + /// +} + +//#region test +//#endregion + +`${location.protocol}//${location.hostname}` + +// tslint should show error here + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [upper] + +// tslint: not a rule flag + ~~~~~~~~~~~~~~~~~~~~~~~~ [upper] + +class Invalid {} + +// tslint:disable-next-line:no-unused-expression +class Valid {} + +// todo write more tests + +// STDIN for input +// STDOUT for output +// stderr for errors + ~~~~~~~~~~~~~~~~~~ [upper] + +[upper]: comment must start with uppercase letter or the word(s): todo diff --git a/test/rules/comment-format/exceptions-words/test.ts.lint b/test/rules/comment-format/exceptions-words/test.ts.lint new file mode 100644 index 00000000000..aa7103ff2a4 --- /dev/null +++ b/test/rules/comment-format/exceptions-words/test.ts.lint @@ -0,0 +1,37 @@ +class Clazz { // this comment is correct + /* block comment + * adada + */ + public funcxion() { // This comment has a capital letter starting it + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [lower] + //This comment is on its own line, and starts with a capital _and_ no space + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [lower] + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [space] + console.log("test"); //this comment has no space + ~~~~~~~~~~~~~~~~~~~~~~~~~ [space] + } + /// +} + +//#region test +//#endregion + +`${location.protocol}//${location.hostname}` + +//noinspection JSUnusedGlobalSymbols +const unusedVar = 'unneeded value'; + +// TODO: Write more tests + +// HACKING is not an exception + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [lower] + +// STDIN for input + ~~~~~~~~~~~~~~~~ [lower] +// STDOUT for output + ~~~~~~~~~~~~~~~~~~ [lower] +// stderr for errors + + +[lower]: comment must start with lowercase letter or the word(s): TODO, HACK +[space]: comment must start with a space diff --git a/test/rules/comment-format/exceptions-words/tslint.json b/test/rules/comment-format/exceptions-words/tslint.json new file mode 100644 index 00000000000..91aa3c20aac --- /dev/null +++ b/test/rules/comment-format/exceptions-words/tslint.json @@ -0,0 +1,8 @@ +{ + "rules": { + "comment-format": [true, "check-space", "check-lowercase", {"ignoreWords": ["TODO", "HACK"]}] + }, + "jsRules": { + "comment-format": [true, "check-uppercase", {"ignoreWords": ["todo"]}] + } +} diff --git a/test/utilsTests.ts b/test/utilsTests.ts index 52206e3d151..37cfbcee5e3 100644 --- a/test/utilsTests.ts +++ b/test/utilsTests.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import {arrayify, dedent, objectify} from "../src/utils"; +import {arrayify, dedent, escapeRegExp, objectify} from "../src/utils"; describe("Utils", () => { it("arrayify", () => { @@ -46,4 +46,15 @@ describe("Utils", () => { assert.equal(dedent` `, " "); assert.equal(dedent``, ""); }); + + it("escapeRegExp", () => { + const plus = escapeRegExp("(a+|d)?b[ci]{2,}"); + const plusRe = new RegExp(plus); + + // contains substring that matches regular expression pattern + assert.equal(plusRe.test("regexpaaaabcicmatch"), false); + + // properly matches exact string with special characters + assert.equal(plusRe.test("string(a+|d)?b[ci]{2,}match"), true); + }); }); From 25de3f9499b05ee7f8dbc2aaf7bf16aa9e4eec3d Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Sun, 8 Jan 2017 19:25:51 -0800 Subject: [PATCH 009/131] Improve error message (#2010) --- src/test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test.ts b/src/test.ts index b35b5e8debf..d55952af6f8 100644 --- a/src/test.ts +++ b/src/test.ts @@ -92,7 +92,7 @@ export function runTest(testDirectory: string, rulesDirectory?: string | string[ const text = fs.readFileSync(path.resolve(path.dirname(fileToLint), filenameToGet), {encoding: "utf-8"}); return ts.createSourceFile(filenameToGet, text, target, true); } - throw new Error("couldn't not get source file"); + throw new Error(`Couldn't get source file '${filenameToGet}'`); }, readFile: (x: string) => x, useCaseSensitiveFileNames: () => true, From a6d3384771c9459990a88dd0b8d50b3694350514 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Tue, 10 Jan 2017 18:04:40 -0800 Subject: [PATCH 010/131] Add `no-boolean-literal-compare` rule (#2013) --- docs/_data/rules.json | 51 +++++-- docs/rules/comment-format/index.html | 68 +++++++-- docs/rules/no-boolean-compare/index.html | 14 ++ .../no-boolean-literal-compare/index.html | 15 ++ src/language/walker/ruleWalker.ts | 4 + src/rules/noBooleanLiteralCompareRule.ts | 131 ++++++++++++++++++ .../no-boolean-literal-compare/test.ts.fix | 30 ++++ .../no-boolean-literal-compare/test.ts.lint | 45 ++++++ .../no-boolean-literal-compare/tsconfig.json | 5 + .../no-boolean-literal-compare/tslint.json | 8 ++ 10 files changed, 349 insertions(+), 22 deletions(-) create mode 100644 docs/rules/no-boolean-compare/index.html create mode 100644 docs/rules/no-boolean-literal-compare/index.html create mode 100644 src/rules/noBooleanLiteralCompareRule.ts create mode 100644 test/rules/no-boolean-literal-compare/test.ts.fix create mode 100644 test/rules/no-boolean-literal-compare/test.ts.lint create mode 100644 test/rules/no-boolean-literal-compare/tsconfig.json create mode 100644 test/rules/no-boolean-literal-compare/tslint.json diff --git a/docs/_data/rules.json b/docs/_data/rules.json index fa358c66214..7e799d38986 100644 --- a/docs/_data/rules.json +++ b/docs/_data/rules.json @@ -139,22 +139,44 @@ "ruleName": "comment-format", "description": "Enforces formatting rules for single-line comments.", "rationale": "Helps maintain a consistent, readable style in your codebase.", - "optionsDescription": "\nThree arguments may be optionally provided:\n\n* `\"check-space\"` requires that all single-line comments must begin with a space, as in `// comment`\n * note that comments starting with `///` are also allowed, for things such as `///`\n* `\"check-lowercase\"` requires that the first non-whitespace character of a comment must be lowercase, if applicable.\n* `\"check-uppercase\"` requires that the first non-whitespace character of a comment must be uppercase, if applicable.", + "optionsDescription": "\nThree arguments may be optionally provided:\n\n* `\"check-space\"` requires that all single-line comments must begin with a space, as in `// comment`\n * note that comments starting with `///` are also allowed, for things such as `///`\n* `\"check-lowercase\"` requires that the first non-whitespace character of a comment must be lowercase, if applicable.\n* `\"check-uppercase\"` requires that the first non-whitespace character of a comment must be uppercase, if applicable.\n\nExceptions to `\"check-lowercase\"` or `\"check-uppercase\"` can be managed with object that may be passed as last argument.\n\nOne of two options can be provided in this object:\n \n * `\"ignoreWords\"` - array of strings - words that will be ignored at the beginning of the comment.\n * `\"ignorePattern\"` - string - RegExp pattern that will be ignored at the beginning of the comment.\n", "options": { "type": "array", "items": { - "type": "string", - "enum": [ - "check-space", - "check-lowercase", - "check-uppercase" + "anyOf": [ + { + "type": "string", + "enum": [ + "check-space", + "check-lowercase", + "check-uppercase" + ] + }, + { + "type": "object", + "properties": { + "ignoreWords": { + "type": "array", + "items": { + "type": "string" + } + }, + "ignorePattern": { + "type": "string" + } + }, + "minProperties": 1, + "maxProperties": 1 + } ] }, "minLength": 1, - "maxLength": 3 + "maxLength": 4 }, "optionExamples": [ - "[true, \"check-space\", \"check-lowercase\"]" + "[true, \"check-space\", \"check-uppercase\"]", + "[true, \"check-lowercase\", {\"ignoreWords\": [\"TODO\", \"HACK\"]}]", + "[true, \"check-lowercase\", {\"ignorePattern\": \"STD\\w{2,3}\\b\"}]" ], "type": "style", "typescriptOnly": false @@ -559,6 +581,19 @@ "type": "functionality", "typescriptOnly": false }, + { + "ruleName": "no-boolean-literal-compare", + "description": "Warns on comparison to a boolean literal, as in `x === true`.", + "hasFix": true, + "optionsDescription": "Not configurable.", + "options": null, + "optionExamples": [ + "true" + ], + "type": "style", + "typescriptOnly": true, + "requiresTypeInfo": true + }, { "ruleName": "no-conditional-assignment", "description": "Disallows any type of assignment in conditionals.", diff --git a/docs/rules/comment-format/index.html b/docs/rules/comment-format/index.html index cbc4b7813a0..65450916bb9 100644 --- a/docs/rules/comment-format/index.html +++ b/docs/rules/comment-format/index.html @@ -2,7 +2,7 @@ ruleName: comment-format description: Enforces formatting rules for single-line comments. rationale: 'Helps maintain a consistent, readable style in your codebase.' -optionsDescription: |- +optionsDescription: | Three arguments may be optionally provided: @@ -10,18 +10,38 @@ * note that comments starting with `///` are also allowed, for things such as `///` * `"check-lowercase"` requires that the first non-whitespace character of a comment must be lowercase, if applicable. * `"check-uppercase"` requires that the first non-whitespace character of a comment must be uppercase, if applicable. + + Exceptions to `"check-lowercase"` or `"check-uppercase"` can be managed with object that may be passed as last argument. + + One of two options can be provided in this object: + + * `"ignoreWords"` - array of strings - words that will be ignored at the beginning of the comment. + * `"ignorePattern"` - string - RegExp pattern that will be ignored at the beginning of the comment. options: type: array items: - type: string - enum: - - check-space - - check-lowercase - - check-uppercase + anyOf: + - type: string + enum: + - check-space + - check-lowercase + - check-uppercase + - type: object + properties: + ignoreWords: + type: array + items: + type: string + ignorePattern: + type: string + minProperties: 1 + maxProperties: 1 minLength: 1 - maxLength: 3 + maxLength: 4 optionExamples: - - '[true, "check-space", "check-lowercase"]' + - '[true, "check-space", "check-uppercase"]' + - '[true, "check-lowercase", {"ignoreWords": ["TODO", "HACK"]}]' + - '[true, "check-lowercase", {"ignorePattern": "STD\w{2,3}\b"}]' type: style typescriptOnly: false layout: rule @@ -30,14 +50,34 @@ { "type": "array", "items": { - "type": "string", - "enum": [ - "check-space", - "check-lowercase", - "check-uppercase" + "anyOf": [ + { + "type": "string", + "enum": [ + "check-space", + "check-lowercase", + "check-uppercase" + ] + }, + { + "type": "object", + "properties": { + "ignoreWords": { + "type": "array", + "items": { + "type": "string" + } + }, + "ignorePattern": { + "type": "string" + } + }, + "minProperties": 1, + "maxProperties": 1 + } ] }, "minLength": 1, - "maxLength": 3 + "maxLength": 4 } --- \ No newline at end of file diff --git a/docs/rules/no-boolean-compare/index.html b/docs/rules/no-boolean-compare/index.html new file mode 100644 index 00000000000..1c1a89423b2 --- /dev/null +++ b/docs/rules/no-boolean-compare/index.html @@ -0,0 +1,14 @@ +--- +ruleName: no-boolean-compare +description: 'Warns on comparison to a boolean literal, as in `x === true`.' +hasFix: true +optionsDescription: Not configurable. +options: null +optionExamples: + - 'true' +type: style +typescriptOnly: true +layout: rule +title: 'Rule: no-boolean-compare' +optionsJSON: 'null' +--- \ No newline at end of file diff --git a/docs/rules/no-boolean-literal-compare/index.html b/docs/rules/no-boolean-literal-compare/index.html new file mode 100644 index 00000000000..74860ef768f --- /dev/null +++ b/docs/rules/no-boolean-literal-compare/index.html @@ -0,0 +1,15 @@ +--- +ruleName: no-boolean-literal-compare +description: 'Warns on comparison to a boolean literal, as in `x === true`.' +hasFix: true +optionsDescription: Not configurable. +options: null +optionExamples: + - 'true' +type: style +typescriptOnly: true +requiresTypeInfo: true +layout: rule +title: 'Rule: no-boolean-literal-compare' +optionsJSON: 'null' +--- \ No newline at end of file diff --git a/src/language/walker/ruleWalker.ts b/src/language/walker/ruleWalker.ts index 83d469fac8e..40bc173e482 100644 --- a/src/language/walker/ruleWalker.ts +++ b/src/language/walker/ruleWalker.ts @@ -105,6 +105,10 @@ export class RuleWalker extends SyntaxWalker implements IWalker { return this.createReplacement(start, length, ""); } + public deleteFromTo(start: number, end: number): Replacement { + return this.createReplacement(start, end - start, ""); + } + public getRuleName(): string { return this.ruleName; } diff --git a/src/rules/noBooleanLiteralCompareRule.ts b/src/rules/noBooleanLiteralCompareRule.ts new file mode 100644 index 00000000000..73a776c3053 --- /dev/null +++ b/src/rules/noBooleanLiteralCompareRule.ts @@ -0,0 +1,131 @@ +/** + * @license + * Copyright 2017 Palantir Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as ts from "typescript"; + +import * as Lint from "../index"; + +export class Rule extends Lint.Rules.TypedRule { + /* tslint:disable:object-literal-sort-keys */ + public static metadata: Lint.IRuleMetadata = { + ruleName: "no-boolean-literal-compare", + description: "Warns on comparison to a boolean literal, as in `x === true`.", + hasFix: true, + optionsDescription: "Not configurable.", + options: null, + optionExamples: ["true"], + type: "style", + typescriptOnly: true, + requiresTypeInfo: true, + }; + /* tslint:enable:object-literal-sort-keys */ + + public static FAILURE_STRING(negate: boolean) { + return `This expression is unnecessarily compared to a boolean. Just ${negate ? "negate it" : "use it directly"}.`; + } + + public applyWithProgram(sourceFile: ts.SourceFile, langSvc: ts.LanguageService): Lint.RuleFailure[] { + return this.applyWithWalker(new Walker(sourceFile, this.getOptions(), langSvc.getProgram())); + } +} + +class Walker extends Lint.ProgramAwareRuleWalker { + public visitBinaryExpression(node: ts.BinaryExpression) { + this.check(node); + super.visitBinaryExpression(node); + } + + private check(node: ts.BinaryExpression) { + const comparison = deconstructComparison(node); + if (comparison === undefined) { + return; + } + + const { negate, expression } = comparison; + const type = this.getTypeChecker().getTypeAtLocation(expression); + if (!Lint.isTypeFlagSet(type, ts.TypeFlags.Boolean)) { + return; + } + + const deleted = node.left === expression + ? this.deleteFromTo(node.left.end, node.end) + : this.deleteFromTo(node.getStart(), node.right.getStart()); + const replacements = [deleted]; + if (negate) { + if (needsParenthesesForNegate(expression)) { + replacements.push(this.appendText(node.getStart(), "!(")); + replacements.push(this.appendText(node.getEnd(), ")")); + } else { + replacements.push(this.appendText(node.getStart(), "!")); + } + } + + this.addFailureAtNode(expression, Rule.FAILURE_STRING(negate), this.createFix(...replacements)); + } +} + +function needsParenthesesForNegate(node: ts.Expression) { + switch (node.kind) { + case ts.SyntaxKind.AsExpression: + case ts.SyntaxKind.BinaryExpression: + return true; + default: + return false; + } +} + +function deconstructComparison(node: ts.BinaryExpression): { negate: boolean, expression: ts.Expression } | undefined { + const { left, operatorToken, right } = node; + const operator = operatorKind(operatorToken); + if (operator === undefined) { + return undefined; + } + + const leftValue = booleanFromExpression(left); + if (leftValue !== undefined) { + return { negate: leftValue !== operator, expression: right }; + } + const rightValue = booleanFromExpression(right); + if (rightValue !== undefined) { + return { negate: rightValue !== operator, expression: left }; + } + return undefined; +} + +function operatorKind(operatorToken: ts.BinaryOperatorToken): boolean | undefined { + switch (operatorToken.kind) { + case ts.SyntaxKind.EqualsEqualsToken: + case ts.SyntaxKind.EqualsEqualsEqualsToken: + return true; + case ts.SyntaxKind.ExclamationEqualsToken: + case ts.SyntaxKind.ExclamationEqualsEqualsToken: + return false; + default: + return undefined; + } +} + +function booleanFromExpression(node: ts.Expression): boolean | undefined { + switch (node.kind) { + case ts.SyntaxKind.TrueKeyword: + return true; + case ts.SyntaxKind.FalseKeyword: + return false; + default: + return undefined; + } +} diff --git a/test/rules/no-boolean-literal-compare/test.ts.fix b/test/rules/no-boolean-literal-compare/test.ts.fix new file mode 100644 index 00000000000..30bb48dc97e --- /dev/null +++ b/test/rules/no-boolean-literal-compare/test.ts.fix @@ -0,0 +1,30 @@ +declare const x: boolean; + +x; +x; + +!x; +!x; + +!x; +!x; + +x; +x; + +x; + +declare const y: boolean | undefined; +y === true; + +declare function f(): boolean; +!f(); + +declare const a: number, b: number; + +!(a as any as boolean); + +!(a < b); + +!!x; + diff --git a/test/rules/no-boolean-literal-compare/test.ts.lint b/test/rules/no-boolean-literal-compare/test.ts.lint new file mode 100644 index 00000000000..9e54c2c6e92 --- /dev/null +++ b/test/rules/no-boolean-literal-compare/test.ts.lint @@ -0,0 +1,45 @@ +declare const x: boolean; + +x === true; +~ [T] +true === x; + ~ [T] + +x === false; +~ [F] +false === x; + ~ [F] + +x !== true; +~ [F] +true !== x; + ~ [F] + +x !== false; +~ [T] +false !== x; + ~ [T] + +x == true; +~ [T] + +declare const y: boolean | undefined; +y === true; + +declare function f(): boolean; +f() === false; +~~~ [F] + +declare const a: number, b: number; + +a as any as boolean === false; +~~~~~~~~~~~~~~~~~~~ [F] + +a < b === false; +~~~~~ [F] + +!x === false; +~~ [F] + +[T]: This expression is unnecessarily compared to a boolean. Just use it directly. +[F]: This expression is unnecessarily compared to a boolean. Just negate it. diff --git a/test/rules/no-boolean-literal-compare/tsconfig.json b/test/rules/no-boolean-literal-compare/tsconfig.json new file mode 100644 index 00000000000..1cc3bc85ee9 --- /dev/null +++ b/test/rules/no-boolean-literal-compare/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "strictNullChecks": true + } +} \ No newline at end of file diff --git a/test/rules/no-boolean-literal-compare/tslint.json b/test/rules/no-boolean-literal-compare/tslint.json new file mode 100644 index 00000000000..79f31adfee3 --- /dev/null +++ b/test/rules/no-boolean-literal-compare/tslint.json @@ -0,0 +1,8 @@ +{ + "linterOptions": { + "typeCheck": true + }, + "rules": { + "no-boolean-literal-compare": true + } +} \ No newline at end of file From 74db5f9e668725d891f67ed39c4dedd506a01f75 Mon Sep 17 00:00:00 2001 From: Noah Chen Date: Thu, 12 Jan 2017 15:43:07 -0500 Subject: [PATCH 011/131] Update contributing docs with how to run a specific test and debug in VSCode (#2025) --- docs/develop/contributing/index.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/docs/develop/contributing/index.md b/docs/develop/contributing/index.md index 648756513fe..6983f7cef14 100644 --- a/docs/develop/contributing/index.md +++ b/docs/develop/contributing/index.md @@ -19,3 +19,26 @@ The [`next` branch of the TSLint repo](https://github.com/palantir/tslint/tree/n compiler as a `devDependency`. This allows you to develop the linter and its rules against the latest features of the language. Releases from this branch are published to NPM with the `next` dist-tag, so you can get the latest dev version of TSLint via `npm install tslint@next`. + +#### Running a specific test + +You can test a specific test by using the `--test` command line parameter followed by your test directory. For example: +``` +// global tslint +// point to a dir that has tslint.json and .lint files +tslint --test test/rules/semicolon/always + +// locally built tslint +./bin/tslint --test test/rules/semicolon/always +``` + +#### Debugging in Visual Studio Code + +Configuration files to work with Visual Studio Code are included when you check out the source code. These files live in the `.vscode` directory. To run TSLint in the debugger, switch to Debug view and use the dropdown at the top of the Debug pane to select the launch configuration (specified in `.vscode/launch.json`). Press `F5` to debug. You should be able to set breakpoints and debug as usual. + +The current debug configurations are: + +- Debug CLI: Used to debug TSLint using command line arguments. Modify the `args` array in `.vscode/launch.json` to add arguments. +- Debug Mocha Tests: Runs non-rule tests +- Debug Rule Tests: Runs rule tests (under `test/rules`) +- Deubg Document Generation: Debug the `scripts/buildDocs.ts` script. From f0660da40c1d66f810fc7e638fb55652d3c49562 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Thu, 12 Jan 2017 17:49:45 -0800 Subject: [PATCH 012/131] Use @types/update-notifier (#2027) --- package.json | 1 + src/updateNotifier.ts | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index d2c4fcfe6d5..c02636f7e09 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "@types/node": "^6.0.56", "@types/optimist": "0.0.29", "@types/resolve": "0.0.4", + "@types/update-notifier": "^1.0.0", "chai": "^3.5.0", "js-yaml": "^3.7.0", "mocha": "^3.2.0", diff --git a/src/updateNotifier.ts b/src/updateNotifier.ts index 3cce32b491b..314d42bdd18 100644 --- a/src/updateNotifier.ts +++ b/src/updateNotifier.ts @@ -16,9 +16,7 @@ */ import { dedent } from "./utils"; - -// tslint:disable-next-line no-var-requires -const updateNotifier = require("update-notifier"); +import updateNotifier = require("update-notifier"); export function updateNotifierCheck(): void { try { From 4193859e1358ded82060f38e71ce223150cbc854 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Thu, 12 Jan 2017 17:51:58 -0800 Subject: [PATCH 013/131] Deprecate `Rule.createFailure` and `Rule.addFailure` (#1952) --- src/language/walker/ruleWalker.ts | 2 ++ src/rules/noConsecutiveBlankLinesRule.ts | 19 +++++++++++------ src/rules/noUnusedVariableRule.ts | 26 ++++++++++++++--------- src/rules/spaceBeforeFunctionParenRule.ts | 23 ++++++-------------- 4 files changed, 37 insertions(+), 33 deletions(-) diff --git a/src/language/walker/ruleWalker.ts b/src/language/walker/ruleWalker.ts index 40bc173e482..d3a5d9c62b0 100644 --- a/src/language/walker/ruleWalker.ts +++ b/src/language/walker/ruleWalker.ts @@ -68,12 +68,14 @@ export class RuleWalker extends SyntaxWalker implements IWalker { return; // TODO remove this method in next major version } + /** @deprecated Prefer `addFailureAt` and its variants. */ public createFailure(start: number, width: number, failure: string, fix?: Fix): RuleFailure { const from = (start > this.limit) ? this.limit : start; const to = ((start + width) > this.limit) ? this.limit : (start + width); return new RuleFailure(this.sourceFile, from, to, failure, this.ruleName, fix); } + /** @deprecated Prefer `addFailureAt` and its variants. */ public addFailure(failure: RuleFailure) { this.failures.push(failure); } diff --git a/src/rules/noConsecutiveBlankLinesRule.ts b/src/rules/noConsecutiveBlankLinesRule.ts index 9ef8218e214..c763d0ba267 100644 --- a/src/rules/noConsecutiveBlankLinesRule.ts +++ b/src/rules/noConsecutiveBlankLinesRule.ts @@ -90,12 +90,19 @@ class NoConsecutiveBlankLinesWalker extends Lint.RuleWalker { line > lastVal + 1 ? sequences.push([line]) : sequences[sequences.length - 1].push(line); lastVal = line; } - sequences - .filter((arr) => arr.length > allowedBlanks) - .map((arr) => arr[0]) - .map((startLineNum: number) => this.createFailure(lineStarts[startLineNum + 1], 1, failureMessage)) - .filter((failure) => !Lint.doesIntersect(failure, templateIntervals)) - .forEach((failure) => this.addFailure(failure)); + + for (const arr of sequences) { + if (arr.length <= allowedBlanks) { + continue; + } + + const startLineNum = arr[0]; + const pos = lineStarts[startLineNum + 1]; + const isInTemplate = templateIntervals.some((interval) => pos >= interval.startPosition && pos < interval.endPosition); + if (!isInTemplate) { + this.addFailureAt(pos, 1, failureMessage); + } + } } private getTemplateIntervals(sourceFile: ts.SourceFile): Lint.IDisabledInterval[] { diff --git a/src/rules/noUnusedVariableRule.ts b/src/rules/noUnusedVariableRule.ts index ad87d2f7dfd..dfa3c74c260 100644 --- a/src/rules/noUnusedVariableRule.ts +++ b/src/rules/noUnusedVariableRule.ts @@ -84,6 +84,13 @@ export class Rule extends Lint.Rules.AbstractRule { } } +interface Failure { + start: number; + width: number; + message: string; + fix?: Lint.Fix; +} + class NoUnusedVariablesWalker extends Lint.RuleWalker { private skipBindingElement: boolean; private skipParameterDeclaration: boolean; @@ -93,7 +100,7 @@ class NoUnusedVariablesWalker extends Lint.RuleWalker { private ignorePattern: RegExp; private isReactUsed: boolean; private reactImport: ts.NamespaceImport; - private possibleFailures: Lint.RuleFailure[] = []; + private possibleFailures: Failure[] = []; constructor(sourceFile: ts.SourceFile, options: Lint.IOptions, private languageService: ts.LanguageService) { @@ -140,15 +147,15 @@ class NoUnusedVariablesWalker extends Lint.RuleWalker { if (!this.isIgnored(nameText)) { const start = this.reactImport.name.getStart(); const msg = Rule.FAILURE_STRING_FACTORY(Rule.FAILURE_TYPE_IMPORT, nameText); - this.possibleFailures.push(this.createFailure(start, nameText.length, msg)); + this.possibleFailures.push({ start, width: nameText.length, message: msg }); } } let someFixBrokeIt = false; // Performance optimization: type-check the whole file before verifying individual fixes - if (this.possibleFailures.some((f) => f.hasFix())) { + if (this.possibleFailures.some((f) => f.fix !== undefined)) { const newText = Lint.Fix.applyAll(this.getSourceFile().getFullText(), - this.possibleFailures.map((f) => f.getFix()).filter((f) => !!f) as Lint.Fix[]); + this.possibleFailures.map((f) => f.fix!).filter((f) => f !== undefined)); // If we have the program, we can verify that the fix doesn't introduce failures if (Lint.checkEdit(this.languageService, this.getSourceFile(), newText).length > 0) { @@ -158,13 +165,12 @@ class NoUnusedVariablesWalker extends Lint.RuleWalker { } this.possibleFailures.forEach((f) => { - const fix = f.getFix(); - if (!someFixBrokeIt || fix === undefined) { - this.addFailure(f); + if (!someFixBrokeIt || f.fix === undefined) { + this.addFailureAt(f.start, f.width, f.message, f.fix); } else { - const newText = fix.apply(this.getSourceFile().getFullText()); + const newText = f.fix.apply(this.getSourceFile().getFullText()); if (Lint.checkEdit(this.languageService, this.getSourceFile(), newText).length === 0) { - this.addFailure(f); + this.addFailureAt(f.start, f.width, f.message, f.fix); } } }); @@ -450,7 +456,7 @@ class NoUnusedVariablesWalker extends Lint.RuleWalker { if (replacements && replacements.length) { fix = new Lint.Fix(Rule.metadata.ruleName, replacements); } - this.possibleFailures.push(this.createFailure(position, name.length, Rule.FAILURE_STRING_FACTORY(type, name), fix)); + this.possibleFailures.push({ start: position, width: name.length, message: Rule.FAILURE_STRING_FACTORY(type, name), fix }); } private isIgnored(name: string) { diff --git a/src/rules/spaceBeforeFunctionParenRule.ts b/src/rules/spaceBeforeFunctionParenRule.ts index 894bdc5e7cc..be68799a4f5 100644 --- a/src/rules/spaceBeforeFunctionParenRule.ts +++ b/src/rules/spaceBeforeFunctionParenRule.ts @@ -156,16 +156,15 @@ class FunctionWalker extends Lint.RuleWalker { } const hasSpace = this.isSpaceAt(openParen.getStart() - 1); - let failure: Lint.RuleFailure | undefined = undefined; if (hasSpace && option === "never") { - failure = this.createInvalidWhitespaceFailure(openParen.getStart() - 1); + const pos = openParen.getStart() - 1; + const fix = new Lint.Fix(Rule.metadata.ruleName, [ this.deleteText(pos, 1) ]); + this.addFailureAt(pos, 1, Rule.INVALID_WHITESPACE_ERROR, fix); } else if (!hasSpace && option === "always") { - failure = this.createMissingWhitespaceFailure(openParen.getStart()); - } - - if (failure !== undefined) { - this.addFailure(failure); + const pos = openParen.getStart(); + const fix = new Lint.Fix(Rule.metadata.ruleName, [ this.appendText(pos, " ") ]); + this.addFailureAt(pos, 1, Rule.MISSING_WHITESPACE_ERROR, fix); } } @@ -189,14 +188,4 @@ class FunctionWalker extends Lint.RuleWalker { const openParen = Lint.childOfKind(node, ts.SyntaxKind.OpenParenToken); this.evaluateRuleAt(openParen, option); } - - private createInvalidWhitespaceFailure(pos: number): Lint.RuleFailure { - const fix = this.createFix(this.deleteText(pos, 1)); - return this.createFailure(pos, 1, Rule.INVALID_WHITESPACE_ERROR, fix); - } - - private createMissingWhitespaceFailure(pos: number): Lint.RuleFailure { - const fix = this.createFix(this.appendText(pos, " ")); - return this.createFailure(pos, 1, Rule.MISSING_WHITESPACE_ERROR, fix); - } } From e6ca41432521a3e5df2780c50478d10f1964a96d Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Thu, 12 Jan 2017 19:09:29 -0800 Subject: [PATCH 014/131] Rewrite member-ordering rule (#1957) --- src/rules/memberOrderingRule.ts | 355 ++++++++---------- .../rules/member-ordering/method/test.ts.lint | 16 + 2 files changed, 172 insertions(+), 199 deletions(-) diff --git a/src/rules/memberOrderingRule.ts b/src/rules/memberOrderingRule.ts index 8d558c9b2d8..fa699086a88 100644 --- a/src/rules/memberOrderingRule.ts +++ b/src/rules/memberOrderingRule.ts @@ -135,175 +135,127 @@ export class Rule extends Lint.Rules.AbstractRule { } } -/* start code supporting old options (i.e. "public-before-private") */ -interface IModifiers { - isMethod: boolean; - isPrivate: boolean; - isInstance: boolean; -} - -function getModifiers(isMethod: boolean, modifiers?: ts.ModifiersArray): IModifiers { - return { - isInstance: !Lint.hasModifier(modifiers, ts.SyntaxKind.StaticKeyword), - isMethod, - isPrivate: Lint.hasModifier(modifiers, ts.SyntaxKind.PrivateKeyword), - }; -} - -function toString(modifiers: IModifiers): string { - return [ - modifiers.isPrivate ? "private" : "public", - modifiers.isInstance ? "instance" : "static", - "member", - modifiers.isMethod ? "function" : "variable", - ].join(" "); -} -/* end old code */ - -/* start new code */ -enum AccessLevel { - PRIVATE, - PROTECTED, - PUBLIC, -} - -enum Membership { - INSTANCE, - STATIC, -} - -enum Kind { - FIELD, - METHOD, -} - -interface INodeAndModifiers { - accessLevel: AccessLevel; - isConstructor: boolean; - kind: Kind; - membership: Membership; - node: ts.Node; -} - -function getNodeAndModifiers(node: ts.Node, isMethod: boolean, isConstructor = false): INodeAndModifiers { - const { modifiers } = node; - const accessLevel = Lint.hasModifier(modifiers, ts.SyntaxKind.PrivateKeyword) ? AccessLevel.PRIVATE - : Lint.hasModifier(modifiers, ts.SyntaxKind.ProtectedKeyword) ? AccessLevel.PROTECTED - : AccessLevel.PUBLIC; - const kind = isMethod ? Kind.METHOD : Kind.FIELD; - const membership = Lint.hasModifier(modifiers, ts.SyntaxKind.StaticKeyword) ? Membership.STATIC : Membership.INSTANCE; - return { - accessLevel, - isConstructor, - kind, - membership, - node, - }; -} +export class MemberOrderingWalker extends Lint.RuleWalker { + private readonly order: string[] | undefined; -function getNodeOption({accessLevel, isConstructor, kind, membership}: INodeAndModifiers) { - if (isConstructor) { - return "constructor"; + constructor(sourceFile: ts.SourceFile, options: Lint.IOptions) { + super(sourceFile, options); + this.order = this.getOrder(); } - return [ - AccessLevel[accessLevel].toLowerCase(), - Membership[membership].toLowerCase(), - Kind[kind].toLowerCase(), - ].join("-"); -} -/* end new code */ - -export class MemberOrderingWalker extends Lint.RuleWalker { - private previousMember: IModifiers; - private memberStack: INodeAndModifiers[][] = []; - private hasOrderOption = this.getHasOrderOption(); - public visitClassDeclaration(node: ts.ClassDeclaration) { - this.resetPreviousModifiers(); - - this.newMemberList(); + this.visitMembers(node.members); super.visitClassDeclaration(node); - this.checkMemberOrder(); } public visitClassExpression(node: ts.ClassExpression) { - this.resetPreviousModifiers(); - - this.newMemberList(); + this.visitMembers(node.members); super.visitClassExpression(node); - this.checkMemberOrder(); } public visitInterfaceDeclaration(node: ts.InterfaceDeclaration) { - this.resetPreviousModifiers(); - - this.newMemberList(); + this.visitMembers(node.members); super.visitInterfaceDeclaration(node); - this.checkMemberOrder(); } - public visitMethodDeclaration(node: ts.MethodDeclaration) { - this.checkModifiersAndSetPrevious(node, getModifiers(true, node.modifiers)); - this.pushMember(getNodeAndModifiers(node, true)); - super.visitMethodDeclaration(node); + public visitTypeLiteral(node: ts.TypeLiteralNode) { + this.visitMembers(node.members); + super.visitTypeLiteral(node); } - public visitMethodSignature(node: ts.SignatureDeclaration) { - this.checkModifiersAndSetPrevious(node, getModifiers(true, node.modifiers)); - this.pushMember(getNodeAndModifiers(node, true)); - super.visitMethodSignature(node); - } + private getOrder(): string[] | undefined { + const allOptions = this.getOptions(); + if (allOptions == null || allOptions.length === 0) { + return undefined; + } - public visitConstructorDeclaration(node: ts.ConstructorDeclaration) { - this.checkModifiersAndSetPrevious(node, getModifiers(true, node.modifiers)); - this.pushMember(getNodeAndModifiers(node, true, true)); - super.visitConstructorDeclaration(node); + const firstOption = allOptions[0]; + if (firstOption == null || typeof firstOption !== "object") { + return undefined; + } + + const orderOption = firstOption[OPTION_ORDER]; + if (Array.isArray(orderOption)) { + return orderOption; + } else if (typeof orderOption === "string") { + return PRESET_ORDERS[orderOption] || PRESET_ORDERS["default"]; + } else { + return undefined; + } } - public visitPropertyDeclaration(node: ts.PropertyDeclaration) { - const { initializer } = node; - const isFunction = initializer != null - && (initializer.kind === ts.SyntaxKind.ArrowFunction || initializer.kind === ts.SyntaxKind.FunctionExpression); - this.checkModifiersAndSetPrevious(node, getModifiers(isFunction, node.modifiers)); - this.pushMember(getNodeAndModifiers(node, isFunction)); - super.visitPropertyDeclaration(node); + private visitMembers(members: Member[]) { + if (this.order === undefined) { + this.checkUsingOldOptions(members); + } else { + this.checkUsingNewOptions(members); + } } - public visitPropertySignature(node: ts.PropertyDeclaration) { - this.checkModifiersAndSetPrevious(node, getModifiers(false, node.modifiers)); - this.pushMember(getNodeAndModifiers(node, false)); - super.visitPropertySignature(node); + /* start new code */ + private checkUsingNewOptions(members: Member[]) { + let prevRank = -1; + for (const member of members) { + const rank = this.memberRank(member); + if (rank === -1) { + // no explicit ordering for this kind of node specified, so continue + continue; + } + + if (rank < prevRank) { + const nodeType = this.rankName(rank); + const prevNodeType = this.rankName(prevRank); + const lowerRank = this.findLowerRank(members, rank); + const locationHint = lowerRank !== -1 + ? `after ${this.rankName(lowerRank)}s` + : "at the beginning of the class/interface"; + const errorLine1 = `Declaration of ${nodeType} not allowed after declaration of ${prevNodeType}. ` + + `Instead, this should come ${locationHint}.`; + this.addFailureAtNode(member, errorLine1); + } else { + // keep track of last good node + prevRank = rank; + } + } } - public visitTypeLiteral(_node: ts.TypeLiteralNode) { - // don't call super from here -- we want to skip the property declarations in type literals + /** Finds the highest existing rank lower than `targetRank`. */ + private findLowerRank(members: Member[], targetRank: Rank): Rank | -1 { + let max: Rank | -1 = -1; + for (const member of members) { + const rank = this.memberRank(member); + if (rank !== -1 && rank < targetRank) { + max = Math.max(max, rank); + } + } + return max; } - public visitObjectLiteralExpression(_node: ts.ObjectLiteralExpression) { - // again, don't call super here - object literals can have methods, - // and we don't wan't to check these + private memberRank(member: Member): Rank | -1 { + const optionName = getOptionName(member); + return optionName === undefined ? -1 : this.order!.indexOf(optionName); } - /* start old code */ - private resetPreviousModifiers() { - this.previousMember = { - isInstance: false, - isMethod: false, - isPrivate: false, - }; + private rankName(rank: Rank): string { + return this.order![rank].replace(/-/g, " "); } + /* end new code */ - private checkModifiersAndSetPrevious(node: ts.Node, currentMember: IModifiers) { - if (!this.canAppearAfter(this.previousMember, currentMember)) { - this.addFailureAtNode(node, - `Declaration of ${toString(currentMember)} not allowed to appear after declaration of ${toString(this.previousMember)}`); + /* start old code */ + private checkUsingOldOptions(members: Member[]) { + let previousModifiers: IModifiers | undefined; + for (const member of members) { + const modifiers = getModifiers(member); + if (previousModifiers !== undefined && !this.canAppearAfter(previousModifiers, modifiers)) { + this.addFailureAtNode(member, + `Declaration of ${toString(modifiers)} not allowed to appear after declaration of ${toString(previousModifiers)}`); + } + previousModifiers = modifiers; } - this.previousMember = currentMember; } - private canAppearAfter(previousMember: IModifiers, currentMember: IModifiers) { - if (previousMember == null || currentMember == null) { + private canAppearAfter(previousMember: IModifiers | undefined, currentMember: IModifiers) { + if (previousMember === undefined) { return true; } @@ -322,75 +274,80 @@ export class MemberOrderingWalker extends Lint.RuleWalker { return true; } /* end old code */ +} - /* start new code */ - private newMemberList() { - if (this.hasOrderOption) { - this.memberStack.push([]); - } - } +/* start code supporting old options (i.e. "public-before-private") */ +interface IModifiers { + isMethod: boolean; + isPrivate: boolean; + isInstance: boolean; +} - private pushMember(node: INodeAndModifiers) { - if (this.hasOrderOption) { - this.memberStack[this.memberStack.length - 1].push(node); - } - } +function getModifiers(member: Member): IModifiers { + return { + isInstance: !Lint.hasModifier(member.modifiers, ts.SyntaxKind.StaticKeyword), + isMethod: isMethodOrConstructor(member), + isPrivate: Lint.hasModifier(member.modifiers, ts.SyntaxKind.PrivateKeyword), + }; +} - private checkMemberOrder() { - if (this.hasOrderOption) { - const memberList = this.memberStack.pop(); - const order = this.getOrder(); - if (memberList !== undefined && order !== null) { - const memberRank = memberList.map((n) => order.indexOf(getNodeOption(n))); - - let prevRank = -1; - memberRank.forEach((rank, i) => { - // no explicit ordering for this kind of node specified, so continue - if (rank === -1) { return; } - - // node should have come before last node, so add a failure - if (rank < prevRank) { - // generate a nice and clear error message - const node = memberList[i].node; - const nodeType = order[rank].split("-").join(" "); - const prevNodeType = order[prevRank].split("-").join(" "); - - const lowerRanks = memberRank.filter((r) => r < rank && r !== -1).sort(); - const locationHint = lowerRanks.length > 0 - ? `after ${order[lowerRanks[lowerRanks.length - 1]].split("-").join(" ")}s` - : "at the beginning of the class/interface"; - - const errorLine1 = `Declaration of ${nodeType} not allowed after declaration of ${prevNodeType}. ` + - `Instead, this should come ${locationHint}.`; - this.addFailureAtNode(node, errorLine1); - } else { - // keep track of last good node - prevRank = rank; - } - }); - } - } +function toString(modifiers: IModifiers): string { + return [ + modifiers.isPrivate ? "private" : "public", + modifiers.isInstance ? "instance" : "static", + "member", + modifiers.isMethod ? "function" : "variable", + ].join(" "); +} +/* end old code */ + +/* start new code */ +const enum MemberKind { Method, Constructor, Field, Ignore } +function getMemberKind(member: Member): MemberKind { + switch (member.kind) { + case ts.SyntaxKind.Constructor: + case ts.SyntaxKind.ConstructSignature: + return MemberKind.Constructor; + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.MethodSignature: + return MemberKind.Method; + case ts.SyntaxKind.PropertyDeclaration: + case ts.SyntaxKind.PropertySignature: + const { initializer } = member as ts.PropertyDeclaration; + const isFunction = initializer !== undefined && + (initializer.kind === ts.SyntaxKind.ArrowFunction || initializer.kind === ts.SyntaxKind.FunctionExpression); + return isFunction ? MemberKind.Method : MemberKind.Field; + default: + return MemberKind.Ignore; } +} - private getHasOrderOption() { - const allOptions = this.getOptions(); - if (allOptions == null || allOptions.length === 0) { - return false; - } +function isMethodOrConstructor(member: Member) { + const kind = getMemberKind(member); + return kind === MemberKind.Method || kind === MemberKind.Constructor; +} - const firstOption = allOptions[0]; - return firstOption != null && typeof firstOption === "object" && firstOption[OPTION_ORDER] != null; +/** Returns e.g. "public-static-field". */ +function getOptionName(member: Member): string | undefined { + const memberKind = getMemberKind(member); + switch (memberKind) { + case MemberKind.Constructor: + return "constructor"; + case MemberKind.Ignore: + return undefined; + default: + const accessLevel = hasModifier(ts.SyntaxKind.PrivateKeyword) ? "private" + : hasModifier(ts.SyntaxKind.ProtectedKeyword) ? "protected" + : "public"; + const membership = hasModifier(ts.SyntaxKind.StaticKeyword) ? "static" : "instance"; + const kind = memberKind === MemberKind.Method ? "method" : "field"; + return `${accessLevel}-${membership}-${kind}`; } - - // assumes this.hasOrderOption() === true - private getOrder(): string[] | null { - const orderOption = this.getOptions()[0][OPTION_ORDER]; - if (Array.isArray(orderOption)) { - return orderOption; - } else if (typeof orderOption === "string") { - return PRESET_ORDERS[orderOption] || PRESET_ORDERS["default"]; - } - return null; + function hasModifier(kind: ts.SyntaxKind) { + return Lint.hasModifier(member.modifiers, kind); } - /* end new code */ } + +type Member = ts.TypeElement | ts.ClassElement; +type Rank = number; +/* end new code */ diff --git a/test/rules/member-ordering/method/test.ts.lint b/test/rules/member-ordering/method/test.ts.lint index 1e552a3cbef..044904e4395 100644 --- a/test/rules/member-ordering/method/test.ts.lint +++ b/test/rules/member-ordering/method/test.ts.lint @@ -52,5 +52,21 @@ class Constructor2 { ~~~~~~~~~~~~~~~~~ [0] } +// Works for type literal, just like interface +type T = { + x(): void; + y: number; + ~~~~~~~~~~ [0] +} + +// Works for class inside object literal +const o = { + foo: class C { + x(): void; + y: number; + ~~~~~~~~~~ [0] + } +} + [0]: Declaration of public instance member variable not allowed to appear after declaration of public instance member function From 970c96bd09a6639f7576a952c92af259ff52941f Mon Sep 17 00:00:00 2001 From: Noah Chen Date: Thu, 12 Jan 2017 23:13:38 -0500 Subject: [PATCH 015/131] Check array length before accessing in `prefer-for-of` (#2022) --- src/rules/preferForOfRule.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rules/preferForOfRule.ts b/src/rules/preferForOfRule.ts index 398c9d3bee8..d4e546df2c9 100644 --- a/src/rules/preferForOfRule.ts +++ b/src/rules/preferForOfRule.ts @@ -122,7 +122,7 @@ class PreferForOfWalker extends Lint.BlockScopeAwareRuleWalker Date: Thu, 12 Jan 2017 20:19:34 -0800 Subject: [PATCH 016/131] Added "check-preblock" option to whitespace rule (#2002) --- src/rules/whitespaceRule.ts | 22 ++++-- test/rules/whitespace/all/test.js.lint | 26 +++++++ test/rules/whitespace/all/test.ts.fix | 94 +++++++++++++++++++++++++ test/rules/whitespace/all/test.ts.lint | 32 ++++++++- test/rules/whitespace/all/tslint.json | 2 + test/rules/whitespace/none/test.ts.lint | 23 ++++++ 6 files changed, 193 insertions(+), 6 deletions(-) create mode 100644 test/rules/whitespace/all/test.ts.fix diff --git a/src/rules/whitespaceRule.ts b/src/rules/whitespaceRule.ts index 775c5893a92..2dd286ceeb1 100644 --- a/src/rules/whitespaceRule.ts +++ b/src/rules/whitespaceRule.ts @@ -26,6 +26,7 @@ const OPTION_MODULE = "check-module"; const OPTION_SEPARATOR = "check-separator"; const OPTION_TYPE = "check-type"; const OPTION_TYPECAST = "check-typecast"; +const OPTION_PREBLOCK = "check-preblock"; export class Rule extends Lint.Rules.AbstractRule { /* tslint:disable:object-literal-sort-keys */ @@ -42,13 +43,14 @@ export class Rule extends Lint.Rules.AbstractRule { * \`"check-module"\` checks for whitespace in import & export statements. * \`"check-separator"\` checks for whitespace after separator tokens (\`,\`/\`;\`). * \`"check-type"\` checks for whitespace before a variable type specification. - * \`"check-typecast"\` checks for whitespace between a typecast and its target.`, + * \`"check-typecast"\` checks for whitespace between a typecast and its target. + * \`"check-preblock"\` checks for whitespace before the opening brace of a block`, options: { type: "array", items: { type: "string", enum: ["check-branch", "check-decl", "check-operator", "check-module", - "check-separator", "check-type", "check-typecast"], + "check-separator", "check-type", "check-typecast", "check-preblock"], }, minLength: 0, maxLength: 7, @@ -87,7 +89,7 @@ class WhitespaceWalker extends Lint.SkippableTokenAwareRuleWalker { if (tokenKind === ts.SyntaxKind.WhitespaceTrivia || tokenKind === ts.SyntaxKind.NewLineTrivia) { prevTokenShouldBeFollowedByWhitespace = false; } else if (prevTokenShouldBeFollowedByWhitespace) { - this.addFailureAt(startPos, 1, Rule.FAILURE_STRING); + this.addMissingWhitespaceErrorAt(startPos); prevTokenShouldBeFollowedByWhitespace = false; } @@ -154,6 +156,13 @@ class WhitespaceWalker extends Lint.SkippableTokenAwareRuleWalker { super.visitBinaryExpression(node); } + protected visitBlock(block: ts.Block) { + if (this.hasOption(OPTION_PREBLOCK)) { + this.checkForTrailingWhitespace(block.getFullStart()); + } + super.visitBlock(block); + } + // check for spaces between ternary operator symbols public visitConditionalExpression(node: ts.ConditionalExpression) { if (this.hasOption(OPTION_OPERATOR)) { @@ -261,7 +270,12 @@ class WhitespaceWalker extends Lint.SkippableTokenAwareRuleWalker { if (nextTokenType !== ts.SyntaxKind.WhitespaceTrivia && nextTokenType !== ts.SyntaxKind.NewLineTrivia && nextTokenType !== ts.SyntaxKind.EndOfFileToken) { - this.addFailureAt(position, 1, Rule.FAILURE_STRING); + this.addMissingWhitespaceErrorAt(position); } } + + private addMissingWhitespaceErrorAt(position: number) { + const fix = this.createFix(this.appendText(position, " ")); + this.addFailureAt(position, 1, Rule.FAILURE_STRING, fix); + } } diff --git a/test/rules/whitespace/all/test.js.lint b/test/rules/whitespace/all/test.js.lint index 314a6cd2fe8..c4cc76957c6 100644 --- a/test/rules/whitespace/all/test.js.lint +++ b/test/rules/whitespace/all/test.js.lint @@ -86,3 +86,29 @@ export function each(obj, iterator, context) { export {each as forEach}; import "libE"; + +function foobar(){} + ~ [missing whitespace] + +function foorbar() +{} + +if (){ + ~ [missing whitespace] + // +} else{} + ~ [missing whitespace] + +if () +{} +else +{} + +/* some comment */{ + // some code with a preceding comment +} + +{ + const foo = 123; + // code that just wants to be encapsulated in a block scope +} diff --git a/test/rules/whitespace/all/test.ts.fix b/test/rules/whitespace/all/test.ts.fix new file mode 100644 index 00000000000..091a21af295 --- /dev/null +++ b/test/rules/whitespace/all/test.ts.fix @@ -0,0 +1,94 @@ +import ast = AST; +module M { + export var ast = AST; + + var x: number; + + var y = (x === 10) ? 1 : 2; + + var zz = (y === 4); + + var z = y; + + var a, b; + + switch (x) { + case 1: break; + default: break; + } + + for (x = 1; x < 2; ++x) { + goto: console.log("hi"); + } + + while (i < 1) { + ++i; + } + + var q; + q.forEach(() => 3); + q.forEach(() => { + return 3; + }); + + var r: () => string; + var s: new () => string; + var a = "10"; + var a = "10"; +} + +var a; + +export = a; + +a.then(() => { + return 1; +}).if(() => { + return 1; +}); + +var name = "something"; +var test = ` + +
Date: Thu, 12 Jan 2017 21:17:38 -0800 Subject: [PATCH 017/131] unified-signatures: Don't count a function/method declaration as an overload if it has a body. (#2017) --- src/rules/unifiedSignaturesRule.ts | 5 ++++- test/rules/unified-signatures/test.ts.lint | 20 ++++++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/rules/unifiedSignaturesRule.ts b/src/rules/unifiedSignaturesRule.ts index 68cf3937cca..5d8c8a36ede 100644 --- a/src/rules/unifiedSignaturesRule.ts +++ b/src/rules/unifiedSignaturesRule.ts @@ -80,6 +80,9 @@ class Walker extends Lint.RuleWalker { this.checkOverloads(statements, (statement) => { if (statement.kind === ts.SyntaxKind.FunctionDeclaration) { const fn = statement as ts.FunctionDeclaration; + if (fn.body) { + return undefined; + } return fn.name && { signature: fn, key: fn.name.text }; } else { return undefined; @@ -90,7 +93,7 @@ class Walker extends Lint.RuleWalker { private checkMembers(members: Array, typeParameters?: ts.TypeParameterDeclaration[]) { this.checkOverloads(members, getOverloadName, typeParameters); function getOverloadName(member: ts.TypeElement | ts.ClassElement) { - if (!isSignatureDeclaration(member)) { + if (!isSignatureDeclaration(member) || (member as ts.MethodDeclaration).body) { return undefined; } const key = getOverloadKey(member); diff --git a/test/rules/unified-signatures/test.ts.lint b/test/rules/unified-signatures/test.ts.lint index bf711b4f207..7bebd293aa6 100644 --- a/test/rules/unified-signatures/test.ts.lint +++ b/test/rules/unified-signatures/test.ts.lint @@ -1,6 +1,22 @@ // Works in non-declaration file too. -function f(x: number): number; -function f(x: string): string; +function f(x: number): void; +function f(x: string): void; + ~~~~~~~~~ [These overloads can be combined into one signature taking `number | string`.] function f(x: any): any { return x; } + +// Body does *not* count as a signature. +function g(): void; +function g(a: number, b: number): void; +function g(a?: number, b?: number): void {} + +class C { + constructor(); + constructor(a: number, b: number); + constructor(a?: number, b?: number) {} + + a(): void; + a(a: number, b: number): void; + a(a?: number, b?: number): void {} +} From 29cd182fad73267d8269d3fec1533afca50a78d3 Mon Sep 17 00:00:00 2001 From: Alexander Rusakov Date: Fri, 13 Jan 2017 08:24:30 +0300 Subject: [PATCH 018/131] Update space-before-function-paren in latest.ts (#2001) (#2014) --- src/configs/latest.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/configs/latest.ts b/src/configs/latest.ts index 09dc87d75a1..94149b8a8cd 100644 --- a/src/configs/latest.ts +++ b/src/configs/latest.ts @@ -16,6 +16,7 @@ */ // tslint:disable object-literal-sort-keys +// tslint:disable:object-literal-key-quotes export const rules = { // added in v3.x "no-invalid-this": false, @@ -37,7 +38,13 @@ export const rules = { // added in v4.3 "import-spacing": true, - "space-before-function-paren": [true, "never"], + "space-before-function-paren": [true, { + "anonymous": "never", + "asyncArrow": "always", + "constructor": "never", + "method": "never", + "named": "never", + }], "typeof-compare": true, "unified-signatures": true, }; From 4228d1165caa255dcb11e35db1062d25a48dc44d Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Thu, 12 Jan 2017 21:31:22 -0800 Subject: [PATCH 019/131] Clean up quotemarkRule.ts (#2019) --- src/rules/quotemarkRule.ts | 69 +++++++++----------------------------- 1 file changed, 15 insertions(+), 54 deletions(-) diff --git a/src/rules/quotemarkRule.ts b/src/rules/quotemarkRule.ts index deb9e45e65b..934f9a6e475 100644 --- a/src/rules/quotemarkRule.ts +++ b/src/rules/quotemarkRule.ts @@ -19,11 +19,6 @@ import * as ts from "typescript"; import * as Lint from "../index"; -enum QuoteMark { - SINGLE_QUOTES, - DOUBLE_QUOTES, -} - export class Rule extends Lint.Rules.AbstractRule { /* tslint:disable:object-literal-sort-keys */ public static metadata: Lint.IRuleMetadata = { @@ -54,8 +49,9 @@ export class Rule extends Lint.Rules.AbstractRule { }; /* tslint:enable:object-literal-sort-keys */ - public static SINGLE_QUOTE_FAILURE = "\" should be '"; - public static DOUBLE_QUOTE_FAILURE = "' should be \""; + public static FAILURE_STRING(actual: string, expected: string) { + return `${actual} should be ${expected}`; + } public isEnabled(): boolean { if (super.isEnabled()) { @@ -73,60 +69,25 @@ export class Rule extends Lint.Rules.AbstractRule { } class QuotemarkWalker extends Lint.RuleWalker { - private quoteMark = QuoteMark.DOUBLE_QUOTES; - private jsxQuoteMark: QuoteMark; + private quoteMark: string; + private jsxQuoteMark: string; private avoidEscape: boolean; constructor(sourceFile: ts.SourceFile, options: Lint.IOptions) { super(sourceFile, options); - - const ruleArguments = this.getOptions(); - - if (ruleArguments.indexOf("single") > -1) { - this.quoteMark = QuoteMark.SINGLE_QUOTES; - } - - if (ruleArguments.indexOf("jsx-single") > -1) { - this.jsxQuoteMark = QuoteMark.SINGLE_QUOTES; - } else if (ruleArguments.indexOf("jsx-double") > -1) { - this.jsxQuoteMark = QuoteMark.DOUBLE_QUOTES; - } else { - this.jsxQuoteMark = this.quoteMark; - } - - this.avoidEscape = ruleArguments.indexOf("avoid-escape") > 0; + this.quoteMark = this.hasOption("single") ? "'" : '"'; + this.jsxQuoteMark = this.hasOption("jsx-single") ? "'" : this.hasOption("jsx-double") ? '"' : this.quoteMark; + this.avoidEscape = this.hasOption("avoid-escape"); } public visitStringLiteral(node: ts.StringLiteral) { - const inJsx = (node.parent !== undefined && node.parent.kind === ts.SyntaxKind.JsxAttribute); - const text = node.getText(); - const width = node.getWidth(); - const position = node.getStart(); - - const firstCharacter = text.charAt(0); - const lastCharacter = text.charAt(text.length - 1); - - const quoteMark = inJsx ? this.jsxQuoteMark : this.quoteMark; - const expectedQuoteMark = (quoteMark === QuoteMark.SINGLE_QUOTES) ? "'" : "\""; - - if (firstCharacter !== expectedQuoteMark || lastCharacter !== expectedQuoteMark) { - // allow the "other" quote mark to be used, but only to avoid having to escape - const includesOtherQuoteMark = text.slice(1, -1).indexOf(expectedQuoteMark) !== -1; - - if (!(this.avoidEscape && includesOtherQuoteMark)) { - const failureMessage = (quoteMark === QuoteMark.SINGLE_QUOTES) - ? Rule.SINGLE_QUOTE_FAILURE - : Rule.DOUBLE_QUOTE_FAILURE; - - const newText = expectedQuoteMark - + text.slice(1, -1).replace(new RegExp(expectedQuoteMark, "g"), `\\${expectedQuoteMark}`) - + expectedQuoteMark; - - const fix = this.createFix(this.createReplacement(position, width, newText)); - this.addFailureAt(position, width, failureMessage, fix); - } + const expectedQuoteMark = node.parent!.kind === ts.SyntaxKind.JsxAttribute ? this.jsxQuoteMark : this.quoteMark; + const actualQuoteMark = node.getText()[0]; + if (actualQuoteMark !== expectedQuoteMark && !(this.avoidEscape && node.text.includes(expectedQuoteMark))) { + const escapedText = node.text.replace(new RegExp(expectedQuoteMark, "g"), `\\${expectedQuoteMark}`); + const newText = expectedQuoteMark + escapedText + expectedQuoteMark; + this.addFailureAtNode(node, Rule.FAILURE_STRING(actualQuoteMark, expectedQuoteMark), + this.createFix(this.createReplacement(node.getStart(), node.getWidth(), newText))); } - - super.visitStringLiteral(node); } } From 14ee0f7acd9fb0a598bde91ddc5fbf919be0209b Mon Sep 17 00:00:00 2001 From: Martin Probst Date: Fri, 13 Jan 2017 18:01:30 +0100 Subject: [PATCH 020/131] Wordsmith the error message for no-inferrable-types a bit. (#2029) In particular, call out that only trivially inferred types of literals should be left out, not every type that's inferrable. --- src/rules/noInferrableTypesRule.ts | 2 +- test/rules/no-inferrable-types/default/test.ts.lint | 6 +++--- test/rules/no-inferrable-types/ignore-params/test.ts.lint | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/rules/noInferrableTypesRule.ts b/src/rules/noInferrableTypesRule.ts index 3c3fe756a83..617724d2a72 100644 --- a/src/rules/noInferrableTypesRule.ts +++ b/src/rules/noInferrableTypesRule.ts @@ -48,7 +48,7 @@ export class Rule extends Lint.Rules.AbstractRule { /* tslint:enable:object-literal-sort-keys */ public static FAILURE_STRING_FACTORY = (type: string) => { - return `LHS type (${type}) inferred by RHS expression, remove type annotation`; + return `Type ${type} trivially inferred from a ${type} literal, remove type annotation`; } public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { diff --git a/test/rules/no-inferrable-types/default/test.ts.lint b/test/rules/no-inferrable-types/default/test.ts.lint index 1dc436a1163..37553cfd3ba 100644 --- a/test/rules/no-inferrable-types/default/test.ts.lint +++ b/test/rules/no-inferrable-types/default/test.ts.lint @@ -26,6 +26,6 @@ function bar(a = 5, b = true, c = "bah") { } // not errors, types are not inferrable function baz(a: any = 5, b: any = true, c: any = "bah") { } -[number]: LHS type (number) inferred by RHS expression, remove type annotation -[boolean]: LHS type (boolean) inferred by RHS expression, remove type annotation -[string]: LHS type (string) inferred by RHS expression, remove type annotation +[number]: Type number trivially inferred from a number literal, remove type annotation +[boolean]: Type boolean trivially inferred from a boolean literal, remove type annotation +[string]: Type string trivially inferred from a string literal, remove type annotation diff --git a/test/rules/no-inferrable-types/ignore-params/test.ts.lint b/test/rules/no-inferrable-types/ignore-params/test.ts.lint index 8a57f8fdf69..3442fd4ebb8 100644 --- a/test/rules/no-inferrable-types/ignore-params/test.ts.lint +++ b/test/rules/no-inferrable-types/ignore-params/test.ts.lint @@ -27,6 +27,6 @@ function bar(a = 5, b = true, c = "bah") { } // not errors, types are not inferrable function baz(a: any = 5, b: any = true, c: any = "bah") { } -[number]: LHS type (number) inferred by RHS expression, remove type annotation -[boolean]: LHS type (boolean) inferred by RHS expression, remove type annotation -[string]: LHS type (string) inferred by RHS expression, remove type annotation +[number]: Type number trivially inferred from a number literal, remove type annotation +[boolean]: Type boolean trivially inferred from a boolean literal, remove type annotation +[string]: Type string trivially inferred from a string literal, remove type annotation From 294a3651ab0b7d63326a8d8b0161b1f4d6151934 Mon Sep 17 00:00:00 2001 From: Mohsen Azimi Date: Fri, 13 Jan 2017 20:03:17 -0800 Subject: [PATCH 021/131] Simplify default projectDirectory using path module (#2026) --- src/linter.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/linter.ts b/src/linter.ts index 85ab22caa09..322a2d0f915 100644 --- a/src/linter.ts +++ b/src/linter.ts @@ -16,6 +16,7 @@ */ import * as fs from "fs"; +import * as path from "path"; import * as ts from "typescript"; import { @@ -58,12 +59,7 @@ class Linter { */ public static createProgram(configFile: string, projectDirectory?: string): ts.Program { if (projectDirectory === undefined) { - const lastSeparator = configFile.lastIndexOf("/"); - if (lastSeparator < 0) { - projectDirectory = "."; - } else { - projectDirectory = configFile.substring(0, lastSeparator + 1); - } + projectDirectory = path.dirname(configFile); } const { config } = ts.readConfigFile(configFile, ts.sys.readFile); From b2be1eb6617197e7034f722016a21b578dcfd564 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Fri, 13 Jan 2017 20:04:12 -0800 Subject: [PATCH 022/131] Add `prefer-method-signature` rule (#2028) --- docs/_data/rules.json | 12 ++++ docs/rules/prefer-method-signature/index.html | 14 +++++ src/rules/preferMethodSignatureRule.ts | 59 +++++++++++++++++++ .../rules/prefer-method-signature/test.ts.fix | 14 +++++ .../prefer-method-signature/test.ts.lint | 18 ++++++ .../rules/prefer-method-signature/tslint.json | 5 ++ 6 files changed, 122 insertions(+) create mode 100644 docs/rules/prefer-method-signature/index.html create mode 100644 src/rules/preferMethodSignatureRule.ts create mode 100644 test/rules/prefer-method-signature/test.ts.fix create mode 100644 test/rules/prefer-method-signature/test.ts.lint create mode 100644 test/rules/prefer-method-signature/tslint.json diff --git a/docs/_data/rules.json b/docs/_data/rules.json index 7e799d38986..bdf69bd6a6b 100644 --- a/docs/_data/rules.json +++ b/docs/_data/rules.json @@ -1280,6 +1280,18 @@ "type": "typescript", "typescriptOnly": false }, + { + "ruleName": "prefer-method-signature", + "description": "Prefer `foo(): void` over `foo: () => void` in interfaces and types.", + "hasFix": true, + "optionsDescription": "Not configurable.", + "options": null, + "optionExamples": [ + "true" + ], + "type": "style", + "typescriptOnly": false + }, { "ruleName": "promise-function-async", "description": "Requires any function or method that returns a promise to be marked async.", diff --git a/docs/rules/prefer-method-signature/index.html b/docs/rules/prefer-method-signature/index.html new file mode 100644 index 00000000000..c5b89e93be1 --- /dev/null +++ b/docs/rules/prefer-method-signature/index.html @@ -0,0 +1,14 @@ +--- +ruleName: prefer-method-signature +description: 'Prefer `foo(): void` over `foo: () => void` in interfaces and types.' +hasFix: true +optionsDescription: Not configurable. +options: null +optionExamples: + - 'true' +type: style +typescriptOnly: false +layout: rule +title: 'Rule: prefer-method-signature' +optionsJSON: 'null' +--- \ No newline at end of file diff --git a/src/rules/preferMethodSignatureRule.ts b/src/rules/preferMethodSignatureRule.ts new file mode 100644 index 00000000000..9ab1c93410c --- /dev/null +++ b/src/rules/preferMethodSignatureRule.ts @@ -0,0 +1,59 @@ +/** + * @license + * Copyright 2017 Palantir Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as ts from "typescript"; + +import * as Lint from "../index"; + +export class Rule extends Lint.Rules.AbstractRule { + /* tslint:disable:object-literal-sort-keys */ + public static metadata: Lint.IRuleMetadata = { + ruleName: "prefer-method-signature", + description: "Prefer `foo(): void` over `foo: () => void` in interfaces and types.", + hasFix: true, + optionsDescription: "Not configurable.", + options: null, + optionExamples: ["true"], + type: "style", + typescriptOnly: false, + }; + /* tslint:enable:object-literal-sort-keys */ + + public static FAILURE_STRING = "Use a method signature instead of a property signature of function type."; + + public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { + return this.applyWithWalker(new Walker(sourceFile, this.getOptions())); + } +} + +class Walker extends Lint.RuleWalker { + public visitPropertySignature(node: ts.PropertyDeclaration) { + const { type } = node; + if (type !== undefined && type.kind === ts.SyntaxKind.FunctionType) { + this.addFailureAtNode(node.name, Rule.FAILURE_STRING, this.createMethodSignatureFix(node, type as ts.FunctionTypeNode)); + } + + super.visitPropertySignature(node); + } + + private createMethodSignatureFix(node: ts.PropertyDeclaration, type: ts.FunctionTypeNode): Lint.Fix | undefined { + return type.type && this.createFix( + this.deleteFromTo(Lint.childOfKind(node, ts.SyntaxKind.ColonToken)!.getStart(), type.getStart()), + this.deleteFromTo(Lint.childOfKind(type, ts.SyntaxKind.EqualsGreaterThanToken)!.getStart(), type.type.getStart()), + this.appendText(Lint.childOfKind(type, ts.SyntaxKind.CloseParenToken)!.end, ":")); + } +} diff --git a/test/rules/prefer-method-signature/test.ts.fix b/test/rules/prefer-method-signature/test.ts.fix new file mode 100644 index 00000000000..9b8183968c1 --- /dev/null +++ b/test/rules/prefer-method-signature/test.ts.fix @@ -0,0 +1,14 @@ +interface I { + foo(): void; + bar(): T; +} + +type T = { + foo?(): void; +} + +class C { + // OK in class + foo: () => void; +} + diff --git a/test/rules/prefer-method-signature/test.ts.lint b/test/rules/prefer-method-signature/test.ts.lint new file mode 100644 index 00000000000..26328e4632f --- /dev/null +++ b/test/rules/prefer-method-signature/test.ts.lint @@ -0,0 +1,18 @@ +interface I { + foo: () => void; + ~~~ [0] + bar: () => T; + ~~~ [0] +} + +type T = { + foo?: () => void; + ~~~ [0] +} + +class C { + // OK in class + foo: () => void; +} + +[0]: Use a method signature instead of a property signature of function type. diff --git a/test/rules/prefer-method-signature/tslint.json b/test/rules/prefer-method-signature/tslint.json new file mode 100644 index 00000000000..ca95816c0f3 --- /dev/null +++ b/test/rules/prefer-method-signature/tslint.json @@ -0,0 +1,5 @@ +{ + "rules": { + "prefer-method-signature": true + } +} From 96b84241a1c7fa151a3a7e832c8eba0b31ea0e19 Mon Sep 17 00:00:00 2001 From: Noah Chen Date: Sat, 14 Jan 2017 09:46:48 -0500 Subject: [PATCH 023/131] `no-magic-numbers`: negative numbers fail even if in allowed node (#2035) --- src/rules/noMagicNumbersRule.ts | 18 +++++++++--------- .../rules/no-magic-numbers/custom/test.ts.lint | 5 +++++ 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/rules/noMagicNumbersRule.ts b/src/rules/noMagicNumbersRule.ts index 26d9b07a679..4e61383c052 100644 --- a/src/rules/noMagicNumbersRule.ts +++ b/src/rules/noMagicNumbersRule.ts @@ -79,21 +79,21 @@ class NoMagicNumbersWalker extends Lint.RuleWalker { } public visitNode(node: ts.Node) { - const isUnary = this.isUnaryNumericExpression(node); - const isNumber = node.kind === ts.SyntaxKind.NumericLiteral && !Rule.ALLOWED_NODES.has(node.parent!.kind); - const isMagicNumber = (isNumber || isUnary) && !this.allowed.has(node.getText()); - if (isMagicNumber) { - this.addFailureAtNode(node, Rule.FAILURE_STRING); - } - if (!isUnary) { + if (node.kind === ts.SyntaxKind.NumericLiteral || this.isNegativeNumericExpression(node)) { + const isAllowedNode = Rule.ALLOWED_NODES.has(node.parent!.kind); + const isAllowedNumber = this.allowed.has(node.getText()); + if (!isAllowedNode && !isAllowedNumber) { + this.addFailureAtNode(node, Rule.FAILURE_STRING); + } + } else { super.visitNode(node); } } /** - * Checks if a node is an unary expression with on a numeric operand. + * Checks if a node is an negative unary expression with on a numeric operand. */ - private isUnaryNumericExpression(node: ts.Node): boolean { + private isNegativeNumericExpression(node: ts.Node): boolean { if (node.kind !== ts.SyntaxKind.PrefixUnaryExpression) { return false; } diff --git a/test/rules/no-magic-numbers/custom/test.ts.lint b/test/rules/no-magic-numbers/custom/test.ts.lint index 32b2173416e..3f5aa7ddc36 100644 --- a/test/rules/no-magic-numbers/custom/test.ts.lint +++ b/test/rules/no-magic-numbers/custom/test.ts.lint @@ -5,3 +5,8 @@ console.log(1338); ~~~~ ['magic numbers' are not allowed] console.log(-1338) ~~~~~ ['magic numbers' are not allowed] +export let x = 1337; +export let x = -1337; +export let x = 1337.7; +export let x = 1338; +export let x = -1338; From da603e67735db03493615e1f81794a9ffdef4c62 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Sun, 15 Jan 2017 09:40:43 -0800 Subject: [PATCH 024/131] no-magic-numbers: Make condition lazy (don't call `.getText()` for every number literal in the file) (#2042) --- docs/_data/rules.json | 5 +-- docs/rules/whitespace/index.html | 5 ++- src/rules/noMagicNumbersRule.ts | 31 ++++++++++--------- .../no-magic-numbers/custom/test.ts.lint | 1 + 4 files changed, 25 insertions(+), 17 deletions(-) diff --git a/docs/_data/rules.json b/docs/_data/rules.json index bdf69bd6a6b..ddea1801436 100644 --- a/docs/_data/rules.json +++ b/docs/_data/rules.json @@ -1720,7 +1720,7 @@ "ruleName": "whitespace", "description": "Enforces whitespace style conventions.", "rationale": "Helps maintain a readable, consistent style in your codebase.", - "optionsDescription": "\nSeven arguments may be optionally provided:\n\n* `\"check-branch\"` checks branching statements (`if`/`else`/`for`/`while`) are followed by whitespace.\n* `\"check-decl\"`checks that variable declarations have whitespace around the equals token.\n* `\"check-operator\"` checks for whitespace around operator tokens.\n* `\"check-module\"` checks for whitespace in import & export statements.\n* `\"check-separator\"` checks for whitespace after separator tokens (`,`/`;`).\n* `\"check-type\"` checks for whitespace before a variable type specification.\n* `\"check-typecast\"` checks for whitespace between a typecast and its target.", + "optionsDescription": "\nSeven arguments may be optionally provided:\n\n* `\"check-branch\"` checks branching statements (`if`/`else`/`for`/`while`) are followed by whitespace.\n* `\"check-decl\"`checks that variable declarations have whitespace around the equals token.\n* `\"check-operator\"` checks for whitespace around operator tokens.\n* `\"check-module\"` checks for whitespace in import & export statements.\n* `\"check-separator\"` checks for whitespace after separator tokens (`,`/`;`).\n* `\"check-type\"` checks for whitespace before a variable type specification.\n* `\"check-typecast\"` checks for whitespace between a typecast and its target.\n* `\"check-preblock\"` checks for whitespace before the opening brace of a block", "options": { "type": "array", "items": { @@ -1732,7 +1732,8 @@ "check-module", "check-separator", "check-type", - "check-typecast" + "check-typecast", + "check-preblock" ] }, "minLength": 0, diff --git a/docs/rules/whitespace/index.html b/docs/rules/whitespace/index.html index 259fd6a8e02..97d6f7bfb29 100644 --- a/docs/rules/whitespace/index.html +++ b/docs/rules/whitespace/index.html @@ -13,6 +13,7 @@ * `"check-separator"` checks for whitespace after separator tokens (`,`/`;`). * `"check-type"` checks for whitespace before a variable type specification. * `"check-typecast"` checks for whitespace between a typecast and its target. + * `"check-preblock"` checks for whitespace before the opening brace of a block options: type: array items: @@ -25,6 +26,7 @@ - check-separator - check-type - check-typecast + - check-preblock minLength: 0 maxLength: 7 optionExamples: @@ -45,7 +47,8 @@ "check-module", "check-separator", "check-type", - "check-typecast" + "check-typecast", + "check-preblock" ] }, "minLength": 0, diff --git a/src/rules/noMagicNumbersRule.ts b/src/rules/noMagicNumbersRule.ts index 4e61383c052..bfc37e2fad5 100644 --- a/src/rules/noMagicNumbersRule.ts +++ b/src/rules/noMagicNumbersRule.ts @@ -68,7 +68,7 @@ export class Rule extends Lint.Rules.AbstractRule { } class NoMagicNumbersWalker extends Lint.RuleWalker { - // lookup object for allowed magic numbers + // allowed magic numbers private allowed: Set; constructor(sourceFile: ts.SourceFile, options: IOptions) { super(sourceFile, options); @@ -79,25 +79,28 @@ class NoMagicNumbersWalker extends Lint.RuleWalker { } public visitNode(node: ts.Node) { - if (node.kind === ts.SyntaxKind.NumericLiteral || this.isNegativeNumericExpression(node)) { - const isAllowedNode = Rule.ALLOWED_NODES.has(node.parent!.kind); - const isAllowedNumber = this.allowed.has(node.getText()); - if (!isAllowedNode && !isAllowedNumber) { + const num = getLiteralNumber(node); + if (num !== undefined) { + if (!Rule.ALLOWED_NODES.has(node.parent!.kind) && !this.allowed.has(num)) { this.addFailureAtNode(node, Rule.FAILURE_STRING); } } else { super.visitNode(node); } } +} - /** - * Checks if a node is an negative unary expression with on a numeric operand. - */ - private isNegativeNumericExpression(node: ts.Node): boolean { - if (node.kind !== ts.SyntaxKind.PrefixUnaryExpression) { - return false; - } - const unaryNode = (node as ts.PrefixUnaryExpression); - return unaryNode.operator === ts.SyntaxKind.MinusToken && unaryNode.operand.kind === ts.SyntaxKind.NumericLiteral; +/** If node is a number literal, return a string representation of that number. */ +function getLiteralNumber(node: ts.Node): string | undefined { + if (node.kind === ts.SyntaxKind.NumericLiteral) { + return (node as ts.NumericLiteral).text; + } + if (node.kind !== ts.SyntaxKind.PrefixUnaryExpression) { + return undefined; + } + const { operator, operand } = node as ts.PrefixUnaryExpression; + if (operator === ts.SyntaxKind.MinusToken && operand.kind === ts.SyntaxKind.NumericLiteral) { + return "-" + (operand as ts.NumericLiteral).text; } + return undefined; } diff --git a/test/rules/no-magic-numbers/custom/test.ts.lint b/test/rules/no-magic-numbers/custom/test.ts.lint index 3f5aa7ddc36..f164c29de8d 100644 --- a/test/rules/no-magic-numbers/custom/test.ts.lint +++ b/test/rules/no-magic-numbers/custom/test.ts.lint @@ -1,5 +1,6 @@ console.log(1337); console.log(-1337); +console.log(- 1337); console.log(1337.7); console.log(1338); ~~~~ ['magic numbers' are not allowed] From fe104728ff4c3138823aba4fbae6f8b0e48485ca Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Sun, 15 Jan 2017 19:11:09 -0800 Subject: [PATCH 025/131] Use native string repeat (#2048) --- src/test/lines.ts | 12 +++++------- src/test/utils.ts | 19 ------------------- test/rule-tester/utilsTests.ts | 28 ---------------------------- 3 files changed, 5 insertions(+), 54 deletions(-) delete mode 100644 src/test/utils.ts delete mode 100644 test/rule-tester/utilsTests.ts diff --git a/src/test/lines.ts b/src/test/lines.ts index 09faafaab06..0f9aeae6422 100644 --- a/src/test/lines.ts +++ b/src/test/lines.ts @@ -14,8 +14,6 @@ * limitations under the License. */ -import {replicateStr} from "./utils"; - // Use classes here instead of interfaces because we want runtime type data export class Line { } export class CodeLine extends Line { constructor(public contents: string) { super(); } } @@ -27,7 +25,7 @@ export class EndErrorLine extends ErrorLine { constructor(startCol: number, public endCol: number, public message: string) { super(startCol); } } -// example matches (between the quotes): +// example matches (between the quotes): // " ~~~~~~~~" const multilineErrorRegex = /^\s*(~+|~nil)$/; // " ~~~~~~~~~ [some error message]" @@ -80,7 +78,7 @@ export function printLine(line: Line, code?: string): string | null { throw new Error("Must supply argument for code parameter when line is an ErrorLine"); } - const leadingSpaces = replicateStr(" ", line.startCol); + const leadingSpaces = " ".repeat(line.startCol); if (line instanceof MultilineErrorLine) { // special case for when the line of code is simply a newline. // use "~nil" to indicate the error continues on that line @@ -88,11 +86,11 @@ export function printLine(line: Line, code?: string): string | null { return ZERO_LENGTH_ERROR; } - const tildes = replicateStr("~", code.length - leadingSpaces.length); + const tildes = "~".repeat(code.length - leadingSpaces.length); return `${leadingSpaces}${tildes}`; } else if (line instanceof EndErrorLine) { - let tildes = replicateStr("~", line.endCol - line.startCol); - let endSpaces = replicateStr(" ", code.length - line.endCol); + let tildes = "~".repeat(line.endCol - line.startCol); + let endSpaces = " ".repeat(code.length - line.endCol); if (tildes.length === 0) { tildes = ZERO_LENGTH_ERROR; // because we add "~nil" we need four less spaces than normal at the end diff --git a/src/test/utils.ts b/src/test/utils.ts deleted file mode 100644 index a7caec955fc..00000000000 --- a/src/test/utils.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright 2016 Palantir Technologies, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export function replicateStr(str: string, numTimes: number) { - return Array(numTimes + 1).join(str); -} diff --git a/test/rule-tester/utilsTests.ts b/test/rule-tester/utilsTests.ts deleted file mode 100644 index e283a1f8b31..00000000000 --- a/test/rule-tester/utilsTests.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2016 Palantir Technologies, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as utils from "../../src/test/utils"; - -describe("Rule Test Utils", () => { - describe("replicateStr", () => { - it("should duplicate strings correctly", () => { - assert.strictEqual("xxxxx", utils.replicateStr("x", 5)); - assert.strictEqual("", utils.replicateStr("abc", 0)); - assert.strictEqual("abcabcabc", utils.replicateStr("abc", 3)); - assert.strictEqual("one", utils.replicateStr("one", 1)); - }); - }); -}); From a8fbbe24852ee8e9d18e7660847233129e31ffc9 Mon Sep 17 00:00:00 2001 From: Klaus Meinhardt Date: Mon, 16 Jan 2017 17:15:46 +0100 Subject: [PATCH 026/131] Introduce alternative to SkippableTokenAwareRuleWalker and scanAllTokens (#2036) Scanning the whole source file is very fragile, because the grammar is not context free. You had to skip certain ranges, that were known to cause trouble. For example regex literals or template expressions. This approach is also very fragile, as it didn't cover all cases and has to adopt new grammar as the language adds it. The new function hands the scanning and parsing off to the parser and only iterates over the tokens of all AST nodes. Skipping ranges is no longer necessary. Also added deprecation comment to scanAllTokens. --- src/enableDisableRules.ts | 39 ++-- src/language/utils.ts | 167 ++++++++++++++++++ src/rules/commentFormatRule.ts | 19 +- src/rules/jsdocFormatRule.ts | 19 +- src/rules/noTrailingWhitespaceRule.ts | 22 +-- src/rules/whitespaceRule.ts | 35 +--- .../_integration/enable-disable/test.tsx.lint | 69 ++++++++ test/rules/_integration/react/test.tsx.lint | 8 +- test/rules/comment-format/lower/test.tsx.lint | 12 ++ test/rules/whitespace/all/test.ts.fix | 6 + test/rules/whitespace/all/test.ts.lint | 13 ++ 11 files changed, 306 insertions(+), 103 deletions(-) create mode 100644 test/rules/_integration/enable-disable/test.tsx.lint create mode 100644 test/rules/comment-format/lower/test.tsx.lint diff --git a/src/enableDisableRules.ts b/src/enableDisableRules.ts index 04a5d3d2924..6c819f7ff44 100644 --- a/src/enableDisableRules.ts +++ b/src/enableDisableRules.ts @@ -19,11 +19,11 @@ import * as ts from "typescript"; import {AbstractRule} from "./language/rule/abstractRule"; import {IOptions} from "./language/rule/rule"; -import {scanAllTokens} from "./language/utils"; -import {SkippableTokenAwareRuleWalker} from "./language/walker/skippableTokenAwareRuleWalker"; +import {forEachComment, TokenPosition} from "./language/utils"; +import {RuleWalker} from "./language/walker/ruleWalker"; import {IEnableDisablePosition} from "./ruleLoader"; -export class EnableDisableRulesWalker extends SkippableTokenAwareRuleWalker { +export class EnableDisableRulesWalker extends RuleWalker { public enableDisableRuleMap: {[rulename: string]: IEnableDisablePosition[]} = {}; constructor(sourceFile: ts.SourceFile, options: IOptions, rules: {[name: string]: any}) { @@ -42,25 +42,8 @@ export class EnableDisableRulesWalker extends SkippableTokenAwareRuleWalker { } public visitSourceFile(node: ts.SourceFile) { - super.visitSourceFile(node); - const scan = ts.createScanner(ts.ScriptTarget.ES5, false, ts.LanguageVariant.Standard, node.text); - - scanAllTokens(scan, (scanner: ts.Scanner) => { - const startPos = scanner.getStartPos(); - const skip = this.getSkipEndFromStart(startPos); - if (skip !== undefined) { - // tokens to skip are places where the scanner gets confused about what the token is, without the proper context - // (specifically, regex, identifiers, and templates). So skip those tokens. - scanner.setTextPos(skip); - return; - } - - if (scanner.getToken() === ts.SyntaxKind.MultiLineCommentTrivia || - scanner.getToken() === ts.SyntaxKind.SingleLineCommentTrivia) { - const commentText = scanner.getTokenText(); - const startPosition = scanner.getTokenPos(); - this.handlePossibleTslintSwitch(commentText, startPosition, node, scanner); - } + forEachComment(node, (fullText, _kind, pos) => { + return this.handlePossibleTslintSwitch(fullText.substring(pos.tokenStart, pos.end), node, pos); }); } @@ -97,7 +80,7 @@ export class EnableDisableRulesWalker extends SkippableTokenAwareRuleWalker { return ruleStateMap[ruleStateMap.length - 1].isEnabled; } - private handlePossibleTslintSwitch(commentText: string, startingPosition: number, node: ts.SourceFile, scanner: ts.Scanner) { + private handlePossibleTslintSwitch(commentText: string, node: ts.SourceFile, pos: TokenPosition) { // regex is: start of string followed by "/*" or "//" followed by any amount of whitespace followed by "tslint:" if (commentText.match(/^(\/\*|\/\/)\s*tslint:/)) { const commentTextParts = commentText.split(":"); @@ -150,18 +133,18 @@ export class EnableDisableRulesWalker extends SkippableTokenAwareRuleWalker { if (isCurrentLine) { // start at the beginning of the current line - start = this.getStartOfLinePosition(node, startingPosition); + start = this.getStartOfLinePosition(node, pos.tokenStart); // end at the beginning of the next line - end = scanner.getTextPos() + 1; + end = pos.end + 1; } else if (isNextLine) { // start at the current position - start = startingPosition; + start = pos.tokenStart; // end at the beginning of the line following the next line - end = this.getStartOfLinePosition(node, startingPosition, 2); + end = this.getStartOfLinePosition(node, pos.tokenStart, 2); } else { // disable rule for the rest of the file // start at the current position, but skip end position - start = startingPosition; + start = pos.tokenStart; end = undefined; } diff --git a/src/language/utils.ts b/src/language/utils.ts index b79bff5b45f..5a2d23ff54c 100644 --- a/src/language/utils.ts +++ b/src/language/utils.ts @@ -61,6 +61,7 @@ export function doesIntersect(failure: RuleFailure, disabledIntervals: IDisabled }); } +/** @deprecated use forEachToken instead */ export function scanAllTokens(scanner: ts.Scanner, callback: (s: ts.Scanner) => void) { let lastStartPos = -1; while (scanner.scan() !== ts.SyntaxKind.EndOfFileToken) { @@ -237,3 +238,169 @@ export function isBlockScopeBoundary(node: ts.Node): boolean { && (node.parent.kind === ts.SyntaxKind.TryStatement || node.parent.kind === ts.SyntaxKind.IfStatement); } + +export interface TokenPosition { + /** The start of the token including all trivia before it */ + fullStart: number; + /** The start of the token */ + tokenStart: number; + /** The end of the token */ + end: number; +} +export type ForEachTokenCallback = (fullText: string, kind: ts.SyntaxKind, pos: TokenPosition, parent: ts.Node) => void; +export type ForEachCommentCallback = (fullText: string, kind: ts.SyntaxKind, pos: TokenPosition) => void; +export type FilterCallback = (node: ts.Node) => boolean; + +/** + * Iterate over all tokens of `node` + * + * @description JsDoc comments are treated like regular comments and only visited if `skipTrivia` === false. + * + * @param node The node whose tokens should be visited + * @param skipTrivia If set to false all trivia preceeding `node` or any of its children is included + * @param cb Is called for every token of `node`. It gets the full text of the SourceFile and the position of the token within that text. + * @param filter If provided, will be called for every Node and Token found. If it returns false `cb` will not be called for this subtree. + */ +export function forEachToken(node: ts.Node, skipTrivia: boolean, cb: ForEachTokenCallback, filter?: FilterCallback) { + // this function will most likely be called with SourceFile anyways, so there is no need for an additional parameter + const sourceFile = node.getSourceFile(); + const fullText = sourceFile.text; + const iterateFn = filter === undefined ? iterateChildren : iterateWithFilter; + const handleTrivia = skipTrivia ? undefined : createTriviaHandler(sourceFile, cb); + + iterateFn(node); + + // this function is used to save the if condition for the common case where no filter is provided + function iterateWithFilter(child: ts.Node): void { + if (filter!(child)) { + return iterateChildren(child); + } + } + + function iterateChildren(child: ts.Node): void { + if (child.kind < ts.SyntaxKind.FirstNode) { + // we found a token, tokens have no children, stop recursing here + return callback(child); + } + /* Exclude everything contained in JsDoc, it will be handled with the other trivia anyway. + * When we would handle JsDoc tokens like regular ones, we would scan some trivia multiple times. + * Even worse, we would scan for trivia inside the JsDoc comment, which yields unexpected results.*/ + if (child.kind !== ts.SyntaxKind.JSDocComment) { + // recurse into Node's children to find tokens + return child.getChildren(sourceFile).forEach(iterateFn); + } + } + + function callback(token: ts.Node) { + const tokenStart = token.getStart(sourceFile); + if (!skipTrivia && tokenStart !== token.pos) { + // we only have to handle trivia before each token, because there is nothing after EndOfFileToken + handleTrivia!(token.pos, tokenStart, token); + } + return cb(fullText, token.kind, {tokenStart, fullStart: token.pos, end: token.end}, token.parent!); + } +} + +function createTriviaHandler(sourceFile: ts.SourceFile, cb: ForEachTokenCallback) { + const fullText = sourceFile.text; + const scanner = ts.createScanner(sourceFile.languageVersion, false, sourceFile.languageVariant, fullText); + /** + * Scan the specified range to get all trivia tokens. + * This includes trailing trivia of the last token and the leading trivia of the current token + */ + function handleTrivia(start: number, end: number, token: ts.Node) { + const parent = token.parent!; + // prevent false positives by not scanning inside JsxText + if (!canHaveLeadingTrivia(token.kind, parent)) { + return; + } + scanner.setTextPos(start); + let position: number; + // we only get here if start !== end, so we can scan at least one time + do { + const kind = scanner.scan(); + position = scanner.getTextPos(); + cb(fullText, kind, {tokenStart: scanner.getTokenPos(), end: position, fullStart: start}, parent); + } while (position < end); + } + + return handleTrivia; +} + +/** Iterate over all comments owned by `node` or its children */ +export function forEachComment(node: ts.Node, cb: ForEachCommentCallback) { + /* Visit all tokens and skip trivia. + Comment ranges between tokens are parsed without the need of a scanner. + forEachToken also does intentionally not pay attention to the correct comment ownership of nodes as it always + scans all trivia before each token, which could include trailing comments of the previous token. + Comment onwership is done right in this function*/ + return forEachToken(node, true, (fullText, tokenKind, pos, parent) => { + // don't search for comments inside JsxText + if (canHaveLeadingTrivia(tokenKind, parent)) { + // Comments before the first token (pos.fullStart === 0) are all considered leading comments, so no need for special treatment + ts.forEachLeadingCommentRange(fullText, pos.fullStart, commentCallback); + } + if (canHaveTrailingTrivia(tokenKind, parent)) { + ts.forEachTrailingCommentRange(fullText, pos.end, commentCallback); + } + function commentCallback(tokenStart: number, end: number, kind: ts.SyntaxKind) { + cb(fullText, kind, {tokenStart, end, fullStart: pos.fullStart}); + } + }); +} + +/** Exclude leading positions that would lead to scanning for trivia inside JsxText */ +function canHaveLeadingTrivia(tokenKind: ts.SyntaxKind, parent: ts.Node): boolean { + if (tokenKind === ts.SyntaxKind.JsxText) { + return false; // there is no trivia before JsxText + } + if (tokenKind === ts.SyntaxKind.OpenBraceToken) { + // before a JsxExpression inside a JsxElement's body can only be other JsxChild, but no trivia + return parent.kind !== ts.SyntaxKind.JsxExpression || parent.parent!.kind !== ts.SyntaxKind.JsxElement; + } + if (tokenKind === ts.SyntaxKind.LessThanToken) { + if (parent.kind === ts.SyntaxKind.JsxClosingElement) { + return false; // would be inside the element body + } + if (parent.kind === ts.SyntaxKind.JsxOpeningElement || parent.kind === ts.SyntaxKind.JsxSelfClosingElement) { + // there can only be leading trivia if we are at the end of the top level element + return parent.parent!.parent!.kind !== ts.SyntaxKind.JsxElement; + } + } + return true; +} + +/** Exclude trailing positions that would lead to scanning for trivia inside JsxText */ +function canHaveTrailingTrivia(tokenKind: ts.SyntaxKind, parent: ts.Node): boolean { + if (tokenKind === ts.SyntaxKind.JsxText) { + return false; // there is no trivia after JsxText + } + if (tokenKind === ts.SyntaxKind.CloseBraceToken) { + // after a JsxExpression inside a JsxElement's body can only be other JsxChild, but no trivia + return parent.kind !== ts.SyntaxKind.JsxExpression || parent.parent!.kind !== ts.SyntaxKind.JsxElement; + } + if (tokenKind === ts.SyntaxKind.GreaterThanToken) { + if (parent.kind === ts.SyntaxKind.JsxOpeningElement) { + return false; // would be inside the element + } + if (parent.kind === ts.SyntaxKind.JsxClosingElement || parent.kind === ts.SyntaxKind.JsxSelfClosingElement) { + // there can only be trailing trivia if we are at the end of the top level element + return parent.parent!.parent!.kind !== ts.SyntaxKind.JsxElement; + } + } + return true; +} + +/** + * Checks if there are any comments between `position` and the next non-trivia token + * + * @param text The text to scan + * @param position The position inside `text` where to start scanning. Make sure that this is a valid start position. + * This value is typically obtained from `node.getFullStart()` or `node.getEnd()` + */ +export function hasCommentAfterPosition(text: string, position: number): boolean { + const cb = () => true; + return ts.forEachTrailingCommentRange(text, position, cb) || + ts.forEachLeadingCommentRange(text, position, cb) || + false; // return boolean instead of undefined if no comment is found +} diff --git a/src/rules/commentFormatRule.ts b/src/rules/commentFormatRule.ts index 199083d966a..688728b03ec 100644 --- a/src/rules/commentFormatRule.ts +++ b/src/rules/commentFormatRule.ts @@ -106,7 +106,7 @@ export class Rule extends Lint.Rules.AbstractRule { } } -class CommentWalker extends Lint.SkippableTokenAwareRuleWalker { +class CommentWalker extends Lint.RuleWalker { private exceptionsRegExp: ExceptionsRegExp; private failureIgnorePart: string = ""; @@ -117,19 +117,10 @@ class CommentWalker extends Lint.SkippableTokenAwareRuleWalker { } public visitSourceFile(node: ts.SourceFile) { - super.visitSourceFile(node); - Lint.scanAllTokens(ts.createScanner(ts.ScriptTarget.ES5, false, ts.LanguageVariant.Standard, node.text), (scanner: ts.Scanner) => { - const skip = this.getSkipEndFromStart(scanner.getStartPos()); - if (skip !== undefined) { - // tokens to skip are places where the scanner gets confused about what the token is, without the proper context - // (specifically, regex, identifiers, and templates). So skip those tokens. - scanner.setTextPos(skip); - return; - } - - if (scanner.getToken() === ts.SyntaxKind.SingleLineCommentTrivia) { - const commentText = scanner.getTokenText(); - const startPosition = scanner.getTokenPos() + 2; + Lint.forEachComment(node, (fullText, kind, pos) => { + if (kind === ts.SyntaxKind.SingleLineCommentTrivia) { + const commentText = fullText.substring(pos.tokenStart, pos.end); + const startPosition = pos.tokenStart + 2; const width = commentText.length - 2; if (this.hasOption(OPTION_SPACE)) { if (!startsWithSpace(commentText)) { diff --git a/src/rules/jsdocFormatRule.ts b/src/rules/jsdocFormatRule.ts index 5322276b1ab..f6c9b0ebe24 100644 --- a/src/rules/jsdocFormatRule.ts +++ b/src/rules/jsdocFormatRule.ts @@ -48,22 +48,11 @@ export class Rule extends Lint.Rules.AbstractRule { } } -class JsdocWalker extends Lint.SkippableTokenAwareRuleWalker { +class JsdocWalker extends Lint.RuleWalker { public visitSourceFile(node: ts.SourceFile) { - super.visitSourceFile(node); - Lint.scanAllTokens(ts.createScanner(ts.ScriptTarget.ES5, false, ts.LanguageVariant.Standard, node.text), (scanner: ts.Scanner) => { - const skip = this.getSkipEndFromStart(scanner.getStartPos()); - if (skip !== undefined) { - // tokens to skip are places where the scanner gets confused about what the token is, without the proper context - // (specifically, regex, identifiers, and templates). So skip those tokens. - scanner.setTextPos(skip); - return; - } - - if (scanner.getToken() === ts.SyntaxKind.MultiLineCommentTrivia) { - const commentText = scanner.getTokenText(); - const startPosition = scanner.getTokenPos(); - this.findFailuresForJsdocComment(commentText, startPosition); + Lint.forEachComment(node, (fullText, kind, pos) => { + if (kind === ts.SyntaxKind.MultiLineCommentTrivia) { + this.findFailuresForJsdocComment(fullText.substring(pos.tokenStart, pos.end), pos.tokenStart); } }); } diff --git a/src/rules/noTrailingWhitespaceRule.ts b/src/rules/noTrailingWhitespaceRule.ts index ddc15ef3e70..ca5a52985c7 100644 --- a/src/rules/noTrailingWhitespaceRule.ts +++ b/src/rules/noTrailingWhitespaceRule.ts @@ -40,29 +40,19 @@ export class Rule extends Lint.Rules.AbstractRule { } } -class NoTrailingWhitespaceWalker extends Lint.SkippableTokenAwareRuleWalker { +class NoTrailingWhitespaceWalker extends Lint.RuleWalker { public visitSourceFile(node: ts.SourceFile) { - super.visitSourceFile(node); let lastSeenWasWhitespace = false; let lastSeenWhitespacePosition = 0; - Lint.scanAllTokens(ts.createScanner(ts.ScriptTarget.ES5, false, ts.LanguageVariant.Standard, node.text), (scanner: ts.Scanner) => { - const skip = this.getSkipEndFromStart(scanner.getStartPos()); - if (skip !== undefined) { - // tokens to skip are places where the scanner gets confused about what the token is, without the proper context - // (specifically, regex, identifiers, and templates). So skip those tokens. - scanner.setTextPos(skip); - lastSeenWasWhitespace = false; - return; - } - - if (scanner.getToken() === ts.SyntaxKind.NewLineTrivia) { + Lint.forEachToken(node, false, (_text, kind, pos) => { + if (kind === ts.SyntaxKind.NewLineTrivia) { if (lastSeenWasWhitespace) { - this.addFailureFromStartToEnd(lastSeenWhitespacePosition, scanner.getStartPos(), Rule.FAILURE_STRING); + this.addFailureFromStartToEnd(lastSeenWhitespacePosition, pos.tokenStart, Rule.FAILURE_STRING); } lastSeenWasWhitespace = false; - } else if (scanner.getToken() === ts.SyntaxKind.WhitespaceTrivia) { + } else if (kind === ts.SyntaxKind.WhitespaceTrivia) { lastSeenWasWhitespace = true; - lastSeenWhitespacePosition = scanner.getStartPos(); + lastSeenWhitespacePosition = pos.tokenStart; } else { lastSeenWasWhitespace = false; } diff --git a/src/rules/whitespaceRule.ts b/src/rules/whitespaceRule.ts index 2dd286ceeb1..da72b04eb22 100644 --- a/src/rules/whitespaceRule.ts +++ b/src/rules/whitespaceRule.ts @@ -35,7 +35,7 @@ export class Rule extends Lint.Rules.AbstractRule { description: "Enforces whitespace style conventions.", rationale: "Helps maintain a readable, consistent style in your codebase.", optionsDescription: Lint.Utils.dedent` - Seven arguments may be optionally provided: + Eight arguments may be optionally provided: * \`"check-branch"\` checks branching statements (\`if\`/\`else\`/\`for\`/\`while\`) are followed by whitespace. * \`"check-decl"\`checks that variable declarations have whitespace around the equals token. @@ -68,7 +68,7 @@ export class Rule extends Lint.Rules.AbstractRule { } } -class WhitespaceWalker extends Lint.SkippableTokenAwareRuleWalker { +class WhitespaceWalker extends Lint.RuleWalker { private scanner: ts.Scanner; constructor(sourceFile: ts.SourceFile, options: Lint.IOptions) { @@ -80,27 +80,14 @@ class WhitespaceWalker extends Lint.SkippableTokenAwareRuleWalker { super.visitSourceFile(node); let prevTokenShouldBeFollowedByWhitespace = false; - this.scanner.setTextPos(0); - - Lint.scanAllTokens(this.scanner, (scanner: ts.Scanner) => { - const startPos = scanner.getStartPos(); - const tokenKind = scanner.getToken(); - + Lint.forEachToken(node, false, (_text, tokenKind, pos, parent) => { if (tokenKind === ts.SyntaxKind.WhitespaceTrivia || tokenKind === ts.SyntaxKind.NewLineTrivia) { prevTokenShouldBeFollowedByWhitespace = false; + return; } else if (prevTokenShouldBeFollowedByWhitespace) { - this.addMissingWhitespaceErrorAt(startPos); + this.addMissingWhitespaceErrorAt(pos.tokenStart); prevTokenShouldBeFollowedByWhitespace = false; } - - const skip = this.getSkipEndFromStart(startPos); - if (skip !== undefined) { - // tokens to skip are places where the scanner gets confused about what the token is, without the proper context - // (specifically, regex, identifiers, and templates). So skip those tokens. - scanner.setTextPos(skip); - return; - } - // check for trailing space after the given tokens switch (tokenKind) { case ts.SyntaxKind.CatchKeyword: @@ -120,7 +107,7 @@ class WhitespaceWalker extends Lint.SkippableTokenAwareRuleWalker { } break; case ts.SyntaxKind.EqualsToken: - if (this.hasOption(OPTION_DECL)) { + if (this.hasOption(OPTION_DECL) && parent.kind !== ts.SyntaxKind.JsxAttribute) { prevTokenShouldBeFollowedByWhitespace = true; } break; @@ -219,16 +206,6 @@ class WhitespaceWalker extends Lint.SkippableTokenAwareRuleWalker { super.visitImportEqualsDeclaration(node); } - public visitJsxElement(node: ts.JsxElement) { - this.addTokenToSkipFromNode(node); - super.visitJsxElement(node); - } - - public visitJsxSelfClosingElement(node: ts.JsxSelfClosingElement) { - this.addTokenToSkipFromNode(node); - super.visitJsxSelfClosingElement(node); - } - public visitTypeAssertionExpression(node: ts.TypeAssertion) { if (this.hasOption(OPTION_TYPECAST)) { const position = node.expression.getFullStart(); diff --git a/test/rules/_integration/enable-disable/test.tsx.lint b/test/rules/_integration/enable-disable/test.tsx.lint new file mode 100644 index 00000000000..e6bab97957b --- /dev/null +++ b/test/rules/_integration/enable-disable/test.tsx.lint @@ -0,0 +1,69 @@ +const span = ( + + /* tslint:disable */ + { + x + 'test' + ~~~~~~ [' should be "] + } + text + +); + +const span = ( + + // tslint:disable-next-line + {x + 'test'} + ~~~~~~ [' should be "] + text + +); + +const span = ( + + {x + 'comment is ignored with text'} // tslint:disable-line + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [' should be "] + works with text + +); + +const span = ( + + {x + 'comment is ignored without text'} // tslint:disable-line + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [' should be "] + +); + +const span = ( + + /* tslint:disable */ {x + 'ignores comments before element if not root'} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [' should be "] + +); + +const span = ( + + {x + 'ignores comments after element if not root'} /* tslint:disable-line */ + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [' should be "] + +); + +const span = ( + /* tslint:disable-next-line */ + {x + 'ignores trailing comments of root element'} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [' should be "] + +); + +const span = ( + + {{x + 'works for comments inside JsxExpression'} /* tslint:disable-line */} + +); + +const span = ( + + {/* tslint:disable-next-line */} + {x + 'works for comment-only expressions'} + text + +); diff --git a/test/rules/_integration/react/test.tsx.lint b/test/rules/_integration/react/test.tsx.lint index 74101c98b3e..cd7b705f2b2 100644 --- a/test/rules/_integration/react/test.tsx.lint +++ b/test/rules/_integration/react/test.tsx.lint @@ -25,7 +25,13 @@ export class FooComponent extends React.Component { public render() { return ( -
this.onClick()}> +
this.onClick()} class={true===false?"foo":"bar"}> + ~ [missing whitespace] + ~ [missing whitespace] + ~ [missing whitespace] + ~ [missing whitespace] + ~ [missing whitespace] + ~ [missing whitespace] {this.state.bar.map((s) => {s})}
diff --git a/test/rules/comment-format/lower/test.tsx.lint b/test/rules/comment-format/lower/test.tsx.lint new file mode 100644 index 00000000000..7a7e5388409 --- /dev/null +++ b/test/rules/comment-format/lower/test.tsx.lint @@ -0,0 +1,12 @@ +const a = ( + + https://github.com/ { + //invalid comment + ~~~~~~~~~~~~~~~ [space] + content + }, text + +); + + +[space]: comment must start with a space \ No newline at end of file diff --git a/test/rules/whitespace/all/test.ts.fix b/test/rules/whitespace/all/test.ts.fix index 091a21af295..43c7c581fbd 100644 --- a/test/rules/whitespace/all/test.ts.fix +++ b/test/rules/whitespace/all/test.ts.fix @@ -53,6 +53,12 @@ var test = `
Date: Mon, 16 Jan 2017 17:17:56 +0100 Subject: [PATCH 027/131] Rewrite importBlacklistRule (#1975) --- src/rules/importBlacklistRule.ts | 68 ++++++++++-------------- test/rules/import-blacklist/test.js.lint | 3 ++ test/rules/import-blacklist/test.ts.lint | 6 +++ 3 files changed, 38 insertions(+), 39 deletions(-) diff --git a/src/rules/importBlacklistRule.ts b/src/rules/importBlacklistRule.ts index 7c6dfd53c34..73350c779cf 100644 --- a/src/rules/importBlacklistRule.ts +++ b/src/rules/importBlacklistRule.ts @@ -49,62 +49,52 @@ export class Rule extends Lint.Rules.AbstractRule { } public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { - const options = this.getOptions(); - return this.applyWithWalker( - new NoRequireFullLibraryWalker(sourceFile, options, options.ruleArguments), - ); + return this.applyWithWalker(new ImportBlacklistWalker(sourceFile, this.getOptions())); } } -class NoRequireFullLibraryWalker extends Lint.RuleWalker { - private blacklist: string[]; - constructor(sourceFile: ts.SourceFile, options: Lint.IOptions, blacklist: string[]) { - super(sourceFile, options); - this.blacklist = blacklist; - } - +class ImportBlacklistWalker extends Lint.RuleWalker { public visitCallExpression(node: ts.CallExpression) { - if ( - node.expression.getText() === "require" && - node.arguments && - node.arguments[0] && - this.isModuleBlacklisted(node.arguments[0].getText()) - ) { - this.reportFailure(node.arguments[0]); + if (node.expression.kind === ts.SyntaxKind.Identifier && + (node.expression as ts.Identifier).text === "require" && + node.arguments !== undefined && + node.arguments.length === 1) { + + this.checkForBannedImport(node.arguments[0]); } super.visitCallExpression(node); } public visitImportEqualsDeclaration(node: ts.ImportEqualsDeclaration) { - const moduleReference = node.moduleReference as ts.ExternalModuleReference; - // If it's an import require and not an import alias - if (moduleReference.expression) { - if (this.isModuleBlacklisted(moduleReference.expression.getText())) { - this.reportFailure(moduleReference.expression); - } + if (isExternalModuleReference(node.moduleReference) && + node.moduleReference.expression !== undefined) { + // If it's an import require and not an import alias + this.checkForBannedImport(node.moduleReference.expression); } super.visitImportEqualsDeclaration(node); } public visitImportDeclaration(node: ts.ImportDeclaration) { - if (this.isModuleBlacklisted(node.moduleSpecifier.getText())) { - this.reportFailure(node.moduleSpecifier); - } + this.checkForBannedImport(node.moduleSpecifier); super.visitImportDeclaration(node); } - private isModuleBlacklisted(text: string): boolean { - return this.blacklist.some((entry) => { - return text.substring(1, text.length - 1) === entry; - }); + private checkForBannedImport(expression: ts.Expression) { + if (isStringLiteral(expression) && this.hasOption(expression.text)) { + this.addFailureFromStartToEnd( + expression.getStart(this.getSourceFile()) + 1, + expression.getEnd() - 1, + Rule.FAILURE_STRING, + ); + } } +} - private reportFailure(node: ts.Expression): void { - this.addFailureAt( - // take quotes into account - node.getStart() + 1, - node.getWidth() - 2, - Rule.FAILURE_STRING, - ); - } +function isStringLiteral(node: ts.Node): node is ts.LiteralExpression { + return node.kind === ts.SyntaxKind.StringLiteral || + node.kind === ts.SyntaxKind.NoSubstitutionTemplateLiteral; +} + +function isExternalModuleReference(node: ts.ModuleReference): node is ts.ExternalModuleReference { + return node.kind === ts.SyntaxKind.ExternalModuleReference; } diff --git a/test/rules/import-blacklist/test.js.lint b/test/rules/import-blacklist/test.js.lint index 2c155eb4bb3..6cfd234fd76 100644 --- a/test/rules/import-blacklist/test.js.lint +++ b/test/rules/import-blacklist/test.js.lint @@ -4,4 +4,7 @@ const forOwn = require('lodash').forOwn; ~~~~~~ [0] const forOwn = require('lodash/forOwn'); +// non-static imports cannot be checked +const _ = require(lodash); + [0]: This import is blacklisted, import a submodule instead diff --git a/test/rules/import-blacklist/test.ts.lint b/test/rules/import-blacklist/test.ts.lint index 25bac38c85c..2a2be028fd4 100644 --- a/test/rules/import-blacklist/test.ts.lint +++ b/test/rules/import-blacklist/test.ts.lint @@ -6,4 +6,10 @@ import forOwn = require('lodash'); ~~~~~~ [0] import forOwn = require('lodash/forOwn'); +// non-static imports cannot be checked +import {Observable} from rxjs; +import forOwn = require(lodash); + +import * as notBlacklisted from "not-blacklisted"; + [0]: This import is blacklisted, import a submodule instead From 441b2e58f8e990b8f330f609264c3a22cb998df4 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Mon, 16 Jan 2017 08:26:18 -0800 Subject: [PATCH 028/131] Add `misused-new` rule (#1963) --- docs/_data/rules.json | 11 +++ .../rules/no-interface-constructor/index.html | 14 ++++ docs/rules/no-misused-new/index.html | 13 +++ src/rules/noMisusedNewRule.ts | 81 +++++++++++++++++++ test/rules/no-misused-new/test.ts.lint | 32 ++++++++ test/rules/no-misused-new/tslint.json | 5 ++ 6 files changed, 156 insertions(+) create mode 100644 docs/rules/no-interface-constructor/index.html create mode 100644 docs/rules/no-misused-new/index.html create mode 100644 src/rules/noMisusedNewRule.ts create mode 100644 test/rules/no-misused-new/test.ts.lint create mode 100644 test/rules/no-misused-new/tslint.json diff --git a/docs/_data/rules.json b/docs/_data/rules.json index ddea1801436..89ca336a49d 100644 --- a/docs/_data/rules.json +++ b/docs/_data/rules.json @@ -838,6 +838,17 @@ "type": "maintainability", "typescriptOnly": true }, + { + "ruleName": "no-misused-new", + "description": "Warns on apparent attempts to define constructors for interfaces or `new` for classes.", + "optionsDescription": "Not configurable.", + "options": null, + "optionExamples": [ + "true" + ], + "type": "functionality", + "typescriptOnly": true + }, { "ruleName": "no-namespace", "description": "Disallows use of internal `module`s and `namespace`s.", diff --git a/docs/rules/no-interface-constructor/index.html b/docs/rules/no-interface-constructor/index.html new file mode 100644 index 00000000000..b2f5778f784 --- /dev/null +++ b/docs/rules/no-interface-constructor/index.html @@ -0,0 +1,14 @@ +--- +ruleName: no-interface-constructor +description: Warns on apparent attempts to define constructors for interfaces. +rationale: '`interface I { new(): I }` declares a type where for `x: I`, `new x()` is also of type `I`.' +optionsDescription: Not configurable. +options: null +optionExamples: + - 'true' +type: functionality +typescriptOnly: true +layout: rule +title: 'Rule: no-interface-constructor' +optionsJSON: 'null' +--- \ No newline at end of file diff --git a/docs/rules/no-misused-new/index.html b/docs/rules/no-misused-new/index.html new file mode 100644 index 00000000000..771e8ed5754 --- /dev/null +++ b/docs/rules/no-misused-new/index.html @@ -0,0 +1,13 @@ +--- +ruleName: no-misused-new +description: Warns on apparent attempts to define constructors for interfaces or `new` for classes. +optionsDescription: Not configurable. +options: null +optionExamples: + - 'true' +type: functionality +typescriptOnly: true +layout: rule +title: 'Rule: no-misused-new' +optionsJSON: 'null' +--- \ No newline at end of file diff --git a/src/rules/noMisusedNewRule.ts b/src/rules/noMisusedNewRule.ts new file mode 100644 index 00000000000..e5bdad0f269 --- /dev/null +++ b/src/rules/noMisusedNewRule.ts @@ -0,0 +1,81 @@ +/** + * @license + * Copyright 2017 Palantir Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as ts from "typescript"; + +import * as Lint from "../index"; + +export class Rule extends Lint.Rules.AbstractRule { + /* tslint:disable:object-literal-sort-keys */ + public static metadata: Lint.IRuleMetadata = { + ruleName: "no-misused-new", + description: "Warns on apparent attempts to define constructors for interfaces or `new` for classes.", + optionsDescription: "Not configurable.", + options: null, + optionExamples: ["true"], + type: "functionality", + typescriptOnly: true, + }; + /* tslint:enable:object-literal-sort-keys */ + + public static FAILURE_STRING_INTERFACE = "Interfaces cannot be constructed, only classes. Did you mean `declare class`?"; + public static FAILURE_STRING_CLASS = '`new` in a class is a method named "new". Did you mean `constructor`?'; + + public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { + return this.applyWithWalker(new Walker(sourceFile, this.getOptions())); + } +} + +class Walker extends Lint.RuleWalker { + public visitMethodSignature(node: ts.MethodSignature) { + if (nameIs(node.name, "constructor")) { + this.addFailureAtNode(node, Rule.FAILURE_STRING_INTERFACE); + } + } + + public visitMethodDeclaration(node: ts.MethodDeclaration) { + if (node.body === undefined && nameIs(node.name, "new") && + returnTypeMatchesParent(node.parent as ts.ClassLikeDeclaration, node)) { + this.addFailureAtNode(node, Rule.FAILURE_STRING_CLASS); + } + } + + public visitConstructSignature(node: ts.ConstructSignatureDeclaration) { + if (returnTypeMatchesParent(node.parent as ts.InterfaceDeclaration, node)) { + this.addFailureAtNode(node, Rule.FAILURE_STRING_INTERFACE); + } + } +} + +function nameIs(name: ts.PropertyName, text: string): boolean { + return name.kind === ts.SyntaxKind.Identifier && name.text === text; +} + +function returnTypeMatchesParent(parent: { name?: ts.Identifier }, decl: ts.SignatureDeclaration): boolean { + if (parent.name === undefined) { + return false; + } + + const name = parent.name.text; + const type = decl.type; + if (type === undefined || type.kind !== ts.SyntaxKind.TypeReference) { + return false; + } + + const typeName = (type as ts.TypeReferenceNode).typeName; + return typeName.kind === ts.SyntaxKind.Identifier && (typeName as ts.Identifier).text === name; +} diff --git a/test/rules/no-misused-new/test.ts.lint b/test/rules/no-misused-new/test.ts.lint new file mode 100644 index 00000000000..f1ed5ca1120 --- /dev/null +++ b/test/rules/no-misused-new/test.ts.lint @@ -0,0 +1,32 @@ +interface I { + new(): I; + ~~~~~~~~~ [0] + // OK if return type is not the interface. + new(): {}; + constructor(): void; + ~~~~~~~~~~~~~~~~~~~~ [0] +} + +// Works for generic type. +interface G { + new(): G; + ~~~~~~~~~~~~~~~ [0] +} + +type T = { + // `new` OK in type literal (we don't know the type name) + new(): T; + // `constructor` still flagged. + constructor(): void; + ~~~~~~~~~~~~~~~~~~~~ [0] +} + +class C { + new(): C; + ~~~~~~~~~ [1] + // OK if there's a body + new() {} +} + +[0]: Interfaces cannot be constructed, only classes. Did you mean `declare class`? +[1]: `new` in a class is a method named "new". Did you mean `constructor`? diff --git a/test/rules/no-misused-new/tslint.json b/test/rules/no-misused-new/tslint.json new file mode 100644 index 00000000000..87f36324069 --- /dev/null +++ b/test/rules/no-misused-new/tslint.json @@ -0,0 +1,5 @@ +{ + "rules": { + "no-misused-new": true + } +} From 141002081c7b3ab78637397a449d4772dfad7549 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Mon, 16 Jan 2017 08:26:52 -0800 Subject: [PATCH 029/131] Add `no-unnecessary-qualifier` rule (#2008) --- docs/_data/rules.json | 13 ++ .../rules/no-unnecessary-qualifier/index.html | 15 ++ src/language/utils.ts | 23 +-- src/rules/arrowReturnShorthandRule.ts | 10 +- src/rules/noUnnecessaryQualifierRule.ts | 142 ++++++++++++++++++ src/rules/unifiedSignaturesRule.ts | 8 +- src/utils.ts | 7 + test/rules/no-unnecessary-qualifier/b.ts | 1 + .../test-global.ts.fix | 5 + .../test-global.ts.lint | 6 + .../no-unnecessary-qualifier/test.ts.fix | 41 +++++ .../no-unnecessary-qualifier/test.ts.lint | 54 +++++++ .../no-unnecessary-qualifier/tsconfig.json | 5 + .../no-unnecessary-qualifier/tslint.json | 8 + 14 files changed, 314 insertions(+), 24 deletions(-) create mode 100644 docs/rules/no-unnecessary-qualifier/index.html create mode 100644 src/rules/noUnnecessaryQualifierRule.ts create mode 100644 test/rules/no-unnecessary-qualifier/b.ts create mode 100644 test/rules/no-unnecessary-qualifier/test-global.ts.fix create mode 100644 test/rules/no-unnecessary-qualifier/test-global.ts.lint create mode 100644 test/rules/no-unnecessary-qualifier/test.ts.fix create mode 100644 test/rules/no-unnecessary-qualifier/test.ts.lint create mode 100644 test/rules/no-unnecessary-qualifier/tsconfig.json create mode 100644 test/rules/no-unnecessary-qualifier/tslint.json diff --git a/docs/_data/rules.json b/docs/_data/rules.json index 89ca336a49d..c8e76a99a14 100644 --- a/docs/_data/rules.json +++ b/docs/_data/rules.json @@ -979,6 +979,19 @@ "type": "maintainability", "typescriptOnly": false }, + { + "ruleName": "no-unnecessary-qualifier", + "description": "Warns when a namespace qualifier (`A.x`) is unnecessary.", + "hasFix": true, + "optionsDescription": "Not configurable.", + "options": null, + "optionExamples": [ + "true" + ], + "type": "style", + "typescriptOnly": true, + "requiresTypeInfo": true + }, { "ruleName": "no-unsafe-finally", "description": "\nDisallows control flow statements, such as `return`, `continue`,\n`break` and `throws` in finally blocks.", diff --git a/docs/rules/no-unnecessary-qualifier/index.html b/docs/rules/no-unnecessary-qualifier/index.html new file mode 100644 index 00000000000..342ca422939 --- /dev/null +++ b/docs/rules/no-unnecessary-qualifier/index.html @@ -0,0 +1,15 @@ +--- +ruleName: no-unnecessary-qualifier +description: Warns when a namespace qualifier (`A.x`) is unnecessary. +hasFix: true +optionsDescription: Not configurable. +options: null +optionExamples: + - 'true' +type: style +typescriptOnly: true +requiresTypeInfo: true +layout: rule +title: 'Rule: no-unnecessary-qualifier' +optionsJSON: 'null' +--- \ No newline at end of file diff --git a/src/language/utils.ts b/src/language/utils.ts index 5a2d23ff54c..151a35db694 100644 --- a/src/language/utils.ts +++ b/src/language/utils.ts @@ -147,36 +147,40 @@ export function isAssignment(node: ts.Node) { * Bitwise check for node flags. */ export function isNodeFlagSet(node: ts.Node, flagToCheck: ts.NodeFlags): boolean { - /* tslint:disable:no-bitwise */ + // tslint:disable-next-line:no-bitwise return (node.flags & flagToCheck) !== 0; - /* tslint:enable:no-bitwise */ } /** * Bitwise check for combined node flags. */ export function isCombinedNodeFlagSet(node: ts.Node, flagToCheck: ts.NodeFlags): boolean { - /* tslint:disable:no-bitwise */ + // tslint:disable-next-line:no-bitwise return (ts.getCombinedNodeFlags(node) & flagToCheck) !== 0; - /* tslint:enable:no-bitwise */ } /** * Bitwise check for combined modifier flags. */ export function isCombinedModifierFlagSet(node: ts.Node, flagToCheck: ts.ModifierFlags): boolean { - /* tslint:disable:no-bitwise */ + // tslint:disable-next-line:no-bitwise return (ts.getCombinedModifierFlags(node) & flagToCheck) !== 0; - /* tslint:enable:no-bitwise */ } /** * Bitwise check for type flags. */ export function isTypeFlagSet(type: ts.Type, flagToCheck: ts.TypeFlags): boolean { - /* tslint:disable:no-bitwise */ + // tslint:disable-next-line:no-bitwise return (type.flags & flagToCheck) !== 0; - /* tslint:enable:no-bitwise */ +} + +/** + * Bitwise check for symbol flags. + */ +export function isSymbolFlagSet(symbol: ts.Symbol, flagToCheck: ts.SymbolFlags): boolean { + // tslint:disable-next-line:no-bitwise + return (symbol.flags & flagToCheck) !== 0; } /** @@ -184,9 +188,8 @@ export function isTypeFlagSet(type: ts.Type, flagToCheck: ts.TypeFlags): boolean * Does not work with TypeScript 2.0.x */ export function isObjectFlagSet(objectType: ts.ObjectType, flagToCheck: ts.ObjectFlags): boolean { - /* tslint:disable:no-bitwise */ + // tslint:disable-next-line:no-bitwise return (objectType.objectFlags & flagToCheck) !== 0; - /* tslint:enable:no-bitwise */ } /** diff --git a/src/rules/arrowReturnShorthandRule.ts b/src/rules/arrowReturnShorthandRule.ts index 58bb7e869d6..4a765d5fe25 100644 --- a/src/rules/arrowReturnShorthandRule.ts +++ b/src/rules/arrowReturnShorthandRule.ts @@ -85,11 +85,11 @@ class Walker extends Lint.RuleWalker { this.appendText(expr.getEnd(), ")"), ] : []), // " {" - deleteFromTo(arrow.end, openBrace.end), + this.deleteFromTo(arrow.end, openBrace.end), // "return " - deleteFromTo(statement.getStart(), expr.getStart()), + this.deleteFromTo(statement.getStart(), expr.getStart()), // " }" (may include semicolon) - deleteFromTo(expr.end, closeBrace.end), + this.deleteFromTo(expr.end, closeBrace.end), ); function hasComments(node: ts.Node): boolean { @@ -104,7 +104,3 @@ function getSimpleReturnExpression(block: ts.Block): ts.Expression | undefined { ? (block.statements[0] as ts.ReturnStatement).expression : undefined; } - -function deleteFromTo(start: number, end: number): Lint.Replacement { - return new Lint.Replacement(start, end - start, ""); -} diff --git a/src/rules/noUnnecessaryQualifierRule.ts b/src/rules/noUnnecessaryQualifierRule.ts new file mode 100644 index 00000000000..d2c1e5c7bcb --- /dev/null +++ b/src/rules/noUnnecessaryQualifierRule.ts @@ -0,0 +1,142 @@ +/** + * @license + * Copyright 2017 Palantir Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as ts from "typescript"; + +import * as Lint from "../index"; +import { arraysAreEqual } from "../utils"; + +export class Rule extends Lint.Rules.TypedRule { + /* tslint:disable:object-literal-sort-keys */ + public static metadata: Lint.IRuleMetadata = { + ruleName: "no-unnecessary-qualifier", + description: "Warns when a namespace qualifier (`A.x`) is unnecessary.", + hasFix: true, + optionsDescription: "Not configurable.", + options: null, + optionExamples: ["true"], + type: "style", + typescriptOnly: true, + requiresTypeInfo: true, + }; + /* tslint:enable:object-literal-sort-keys */ + + public static FAILURE_STRING(name: string) { + return `Qualifier is unnecessary since '${name}' is in scope.`; + } + + public applyWithProgram(sourceFile: ts.SourceFile, langSvc: ts.LanguageService): Lint.RuleFailure[] { + return this.applyWithWalker(new Walker(sourceFile, this.getOptions(), langSvc.getProgram())); + } +} + +class Walker extends Lint.ProgramAwareRuleWalker { + private namespacesInScope: Array = []; + + public visitModuleDeclaration(node: ts.ModuleDeclaration) { + this.namespacesInScope.push(node); + super.visitModuleDeclaration(node); + this.namespacesInScope.pop(); + } + + public visitEnumDeclaration(node: ts.EnumDeclaration) { + this.namespacesInScope.push(node); + super.visitEnumDeclaration(node); + this.namespacesInScope.pop(); + } + + public visitNode(node: ts.Node) { + switch (node.kind) { + case ts.SyntaxKind.QualifiedName: + const { left, right } = node as ts.QualifiedName; + this.visitNamespaceAccess(node, left, right); + break; + case ts.SyntaxKind.PropertyAccessExpression: + const { expression, name } = node as ts.PropertyAccessExpression; + if (isEntityNameExpression(expression)) { + this.visitNamespaceAccess(node, expression, name); + break; + } + // fall through + default: + super.visitNode(node); + } + } + + private visitNamespaceAccess(node: ts.Node, qualifier: ts.EntityNameOrEntityNameExpression, name: ts.Identifier) { + if (this.qualifierIsUnnecessary(qualifier, name)) { + const fix = this.createFix(this.deleteFromTo(qualifier.getStart(), name.getStart())); + this.addFailureAtNode(qualifier, Rule.FAILURE_STRING(qualifier.getText()), fix); + } else { + // Only look for nested qualifier errors if we didn't already fail on the outer qualifier. + super.visitNode(node); + } + } + + private qualifierIsUnnecessary(qualifier: ts.EntityNameOrEntityNameExpression, name: ts.Identifier): boolean { + const namespaceSymbol = this.symbolAtLocation(qualifier); + if (namespaceSymbol === undefined || !this.symbolIsNamespaceInScope(namespaceSymbol)) { + return false; + } + + const accessedSymbol = this.symbolAtLocation(name); + if (accessedSymbol === undefined) { + return false; + } + + // If the symbol in scope is different, the qualifier is necessary. + const fromScope = this.getSymbolInScope(qualifier, accessedSymbol.flags, name.text); + return fromScope === undefined || symbolsAreEqual(fromScope, accessedSymbol); + } + + private getSymbolInScope(node: ts.Node, flags: ts.SymbolFlags, name: string): ts.Symbol | undefined { + // TODO:PERF `getSymbolsInScope` gets a long list. Is there a better way? + const scope = this.getTypeChecker().getSymbolsInScope(node, flags); + return scope.find((scopeSymbol) => scopeSymbol.name === name); + } + + private symbolAtLocation(node: ts.Node): ts.Symbol | undefined { + return this.getTypeChecker().getSymbolAtLocation(node); + } + + private symbolIsNamespaceInScope(symbol: ts.Symbol): boolean { + if (symbol.getDeclarations().some((decl) => this.namespacesInScope.some((ns) => nodesAreEqual(ns, decl)))) { + return true; + } + const alias = this.tryGetAliasedSymbol(symbol); + return alias !== undefined && this.symbolIsNamespaceInScope(alias); + } + + private tryGetAliasedSymbol(symbol: ts.Symbol): ts.Symbol | undefined { + return Lint.isSymbolFlagSet(symbol, ts.SymbolFlags.Alias) ? this.getTypeChecker().getAliasedSymbol(symbol) : undefined; + } +} + +function isEntityNameExpression(expr: ts.Expression): expr is ts.EntityNameExpression { + return expr.kind === ts.SyntaxKind.Identifier || + expr.kind === ts.SyntaxKind.PropertyAccessExpression && isEntityNameExpression((expr as ts.PropertyAccessExpression).expression); +} + +// TODO: Should just be `===`. See https://github.com/palantir/tslint/issues/1969 +function nodesAreEqual(a: ts.Node, b: ts.Node) { + return a.pos === b.pos; +} + +// Only needed in global files. Likely due to https://github.com/palantir/tslint/issues/1969. See `test.global.ts.lint`. +function symbolsAreEqual(a: ts.Symbol, b: ts.Symbol): boolean { + return arraysAreEqual(a.declarations, b.declarations, nodesAreEqual); +} diff --git a/src/rules/unifiedSignaturesRule.ts b/src/rules/unifiedSignaturesRule.ts index 5d8c8a36ede..8d844604ddf 100644 --- a/src/rules/unifiedSignaturesRule.ts +++ b/src/rules/unifiedSignaturesRule.ts @@ -18,6 +18,7 @@ import * as ts from "typescript"; import * as Lint from "../index"; +import { arraysAreEqual, Equal } from "../utils"; import { getOverloadKey, isSignatureDeclaration } from "./adjacentOverloadSignaturesRule"; @@ -202,9 +203,6 @@ type GetOverload = (node: T) => { signature: ts.SignatureDeclaration, key: st */ type IsTypeParameter = (typeName: string) => boolean; -/** Return true if both parameters are equal. */ -type Equal = (a: T, b: T) => boolean; - /** Given type parameters, returns a function to test whether a type is one of those parameters. */ function getIsTypeParameter(typeParameters?: ts.TypeParameterDeclaration[]): IsTypeParameter { if (!typeParameters) { @@ -297,7 +295,3 @@ function forEachPair(values: T[], action: (a: T, b: T) => void): void { } } } - -function arraysAreEqual(a: T[] | undefined, b: T[] | undefined, eq: Equal): boolean { - return a === b || !!a && !!b && a.length === b.length && a.every((x, idx) => eq(x, b[idx])); -} diff --git a/src/utils.ts b/src/utils.ts index d0643016314..82479f6366b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -107,3 +107,10 @@ export function stripComments(content: string): string { export function escapeRegExp(re: string): string { return re.replace(/[.+*?|^$[\]{}()\\]/g, "\\$&"); } + +/** Return true if both parameters are equal. */ +export type Equal = (a: T, b: T) => boolean; + +export function arraysAreEqual(a: T[] | undefined, b: T[] | undefined, eq: Equal): boolean { + return a === b || !!a && !!b && a.length === b.length && a.every((x, idx) => eq(x, b[idx])); +} diff --git a/test/rules/no-unnecessary-qualifier/b.ts b/test/rules/no-unnecessary-qualifier/b.ts new file mode 100644 index 00000000000..a87fbc00e59 --- /dev/null +++ b/test/rules/no-unnecessary-qualifier/b.ts @@ -0,0 +1 @@ +export type T = number; \ No newline at end of file diff --git a/test/rules/no-unnecessary-qualifier/test-global.ts.fix b/test/rules/no-unnecessary-qualifier/test-global.ts.fix new file mode 100644 index 00000000000..6db27bd8f6a --- /dev/null +++ b/test/rules/no-unnecessary-qualifier/test-global.ts.fix @@ -0,0 +1,5 @@ +// We need `symbolsAreEqual` in global files. +namespace N { + export type T = number; + export const x: T = 0; +} diff --git a/test/rules/no-unnecessary-qualifier/test-global.ts.lint b/test/rules/no-unnecessary-qualifier/test-global.ts.lint new file mode 100644 index 00000000000..2901f62a880 --- /dev/null +++ b/test/rules/no-unnecessary-qualifier/test-global.ts.lint @@ -0,0 +1,6 @@ +// We need `symbolsAreEqual` in global files. +namespace N { + export type T = number; + export const x: N.T = 0; + ~ [Qualifier is unnecessary since 'N' is in scope.] +} diff --git a/test/rules/no-unnecessary-qualifier/test.ts.fix b/test/rules/no-unnecessary-qualifier/test.ts.fix new file mode 100644 index 00000000000..cd80a83a502 --- /dev/null +++ b/test/rules/no-unnecessary-qualifier/test.ts.fix @@ -0,0 +1,41 @@ +namespace N { + export type T = number; + export const x: T = 0; + + export namespace M { + export type U = T; + export const y: U = 0; + export const z: U = 0; + export const a = x; + export const b = y; + export const c = z; + } +} + +namespace A.B.C1 { + export const D = 7; +} + +namespace A.B.C2 { + const copy = C1.D; +} + +import * as B from "./b"; +declare module "./b" { + const x: T; +} + +namespace X { + export type T = number; + namespace Y { + type T = string; + // This qualifier *is* necessary since 'X.T' is shadowed. + const x: X.T = 0; + } +} + +enum E { + A, + B = A +} + diff --git a/test/rules/no-unnecessary-qualifier/test.ts.lint b/test/rules/no-unnecessary-qualifier/test.ts.lint new file mode 100644 index 00000000000..6e7b2b4cca0 --- /dev/null +++ b/test/rules/no-unnecessary-qualifier/test.ts.lint @@ -0,0 +1,54 @@ +namespace N { + export type T = number; + export const x: N.T = 0; + ~ [N] + + export namespace M { + export type U = N.T; + ~ [N] + export const y: M.U = 0; + ~ [M] + export const z: N.M.U = 0; + ~~~ [NM] + export const a = N.x; + ~ [N] + export const b = M.y; + ~ [M] + export const c = N.M.z; + ~~~ [NM] + } +} + +namespace A.B.C1 { + export const D = 7; +} + +namespace A.B.C2 { + const copy = A.B.C1.D; + ~~~ [Qualifier is unnecessary since 'A.B' is in scope.] +} + +import * as B from "./b"; +declare module "./b" { + const x: B.T; + ~ [Qualifier is unnecessary since 'B' is in scope.] +} + +namespace X { + export type T = number; + namespace Y { + type T = string; + // This qualifier *is* necessary since 'X.T' is shadowed. + const x: X.T = 0; + } +} + +enum E { + A, + B = E.A + ~ [Qualifier is unnecessary since 'E' is in scope.] +} + +[N]: Qualifier is unnecessary since 'N' is in scope. +[M]: Qualifier is unnecessary since 'M' is in scope. +[NM]: Qualifier is unnecessary since 'N.M' is in scope. diff --git a/test/rules/no-unnecessary-qualifier/tsconfig.json b/test/rules/no-unnecessary-qualifier/tsconfig.json new file mode 100644 index 00000000000..db953a729b9 --- /dev/null +++ b/test/rules/no-unnecessary-qualifier/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "declaration": true + } +} \ No newline at end of file diff --git a/test/rules/no-unnecessary-qualifier/tslint.json b/test/rules/no-unnecessary-qualifier/tslint.json new file mode 100644 index 00000000000..2945858d5e7 --- /dev/null +++ b/test/rules/no-unnecessary-qualifier/tslint.json @@ -0,0 +1,8 @@ +{ + "linterOptions": { + "typeCheck": true + }, + "rules": { + "no-unnecessary-qualifier": true + } +} \ No newline at end of file From 15ed0fe89ff29a8396ab32d5ff9d654515ef1453 Mon Sep 17 00:00:00 2001 From: Romke van der Meulen Date: Tue, 17 Jan 2017 04:14:02 +0100 Subject: [PATCH 030/131] README: fix code example so that it type checks (#2050) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ca0c5aa6ded..70b6bbd648d 100644 --- a/README.md +++ b/README.md @@ -247,7 +247,7 @@ const files = Linter.getFileNames(program); const results = files.map(file => { const fileContents = program.getSourceFile(file).getFullText(); const linter = new Linter(file, fileContents, options, program); - return result.lint(); + return linter.lint(); }); ``` From d0322f4663180402d86422c634987b2e1e2890cb Mon Sep 17 00:00:00 2001 From: Klaus Meinhardt Date: Wed, 18 Jan 2017 17:02:37 +0100 Subject: [PATCH 031/131] Ensure backwards compatibility of #2036 (#2055) This commit can be reverted once tslint drops support for typescript 2.0.10 Fixes #2054 --- docs/_data/rules.json | 2 +- docs/rules/whitespace/index.html | 2 +- src/language/utils.ts | 28 ++++++++++++++++++---------- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/docs/_data/rules.json b/docs/_data/rules.json index c8e76a99a14..44c43320380 100644 --- a/docs/_data/rules.json +++ b/docs/_data/rules.json @@ -1744,7 +1744,7 @@ "ruleName": "whitespace", "description": "Enforces whitespace style conventions.", "rationale": "Helps maintain a readable, consistent style in your codebase.", - "optionsDescription": "\nSeven arguments may be optionally provided:\n\n* `\"check-branch\"` checks branching statements (`if`/`else`/`for`/`while`) are followed by whitespace.\n* `\"check-decl\"`checks that variable declarations have whitespace around the equals token.\n* `\"check-operator\"` checks for whitespace around operator tokens.\n* `\"check-module\"` checks for whitespace in import & export statements.\n* `\"check-separator\"` checks for whitespace after separator tokens (`,`/`;`).\n* `\"check-type\"` checks for whitespace before a variable type specification.\n* `\"check-typecast\"` checks for whitespace between a typecast and its target.\n* `\"check-preblock\"` checks for whitespace before the opening brace of a block", + "optionsDescription": "\nEight arguments may be optionally provided:\n\n* `\"check-branch\"` checks branching statements (`if`/`else`/`for`/`while`) are followed by whitespace.\n* `\"check-decl\"`checks that variable declarations have whitespace around the equals token.\n* `\"check-operator\"` checks for whitespace around operator tokens.\n* `\"check-module\"` checks for whitespace in import & export statements.\n* `\"check-separator\"` checks for whitespace after separator tokens (`,`/`;`).\n* `\"check-type\"` checks for whitespace before a variable type specification.\n* `\"check-typecast\"` checks for whitespace between a typecast and its target.\n* `\"check-preblock\"` checks for whitespace before the opening brace of a block", "options": { "type": "array", "items": { diff --git a/docs/rules/whitespace/index.html b/docs/rules/whitespace/index.html index 97d6f7bfb29..c3f685c206a 100644 --- a/docs/rules/whitespace/index.html +++ b/docs/rules/whitespace/index.html @@ -4,7 +4,7 @@ rationale: 'Helps maintain a readable, consistent style in your codebase.' optionsDescription: |- - Seven arguments may be optionally provided: + Eight arguments may be optionally provided: * `"check-branch"` checks branching statements (`if`/`else`/`for`/`while`) are followed by whitespace. * `"check-decl"`checks that variable declarations have whitespace around the equals token. diff --git a/src/language/utils.ts b/src/language/utils.ts index 151a35db694..f9d31bec4c0 100644 --- a/src/language/utils.ts +++ b/src/language/utils.ts @@ -281,7 +281,10 @@ export function forEachToken(node: ts.Node, skipTrivia: boolean, cb: ForEachToke } function iterateChildren(child: ts.Node): void { - if (child.kind < ts.SyntaxKind.FirstNode) { + if (child.kind < ts.SyntaxKind.FirstNode || + // for backwards compatibility to typescript 2.0.10 + // JsxText was no Token, but a Node in that version + child.kind === ts.SyntaxKind.JsxText) { // we found a token, tokens have no children, stop recursing here return callback(child); } @@ -341,13 +344,20 @@ export function forEachComment(node: ts.Node, cb: ForEachCommentCallback) { // don't search for comments inside JsxText if (canHaveLeadingTrivia(tokenKind, parent)) { // Comments before the first token (pos.fullStart === 0) are all considered leading comments, so no need for special treatment - ts.forEachLeadingCommentRange(fullText, pos.fullStart, commentCallback); + const comments = ts.getLeadingCommentRanges(fullText, pos.fullStart); + if (comments !== undefined) { + for (const comment of comments) { + cb(fullText, comment.kind, {fullStart: pos.fullStart, tokenStart: comment.pos, end: comment.end}); + } + } } if (canHaveTrailingTrivia(tokenKind, parent)) { - ts.forEachTrailingCommentRange(fullText, pos.end, commentCallback); - } - function commentCallback(tokenStart: number, end: number, kind: ts.SyntaxKind) { - cb(fullText, kind, {tokenStart, end, fullStart: pos.fullStart}); + const comments = ts.getTrailingCommentRanges(fullText, pos.end); + if (comments !== undefined) { + for (const comment of comments) { + cb(fullText, comment.kind, {fullStart: pos.fullStart, tokenStart: comment.pos, end: comment.end}); + } + } } }); } @@ -402,8 +412,6 @@ function canHaveTrailingTrivia(tokenKind: ts.SyntaxKind, parent: ts.Node): boole * This value is typically obtained from `node.getFullStart()` or `node.getEnd()` */ export function hasCommentAfterPosition(text: string, position: number): boolean { - const cb = () => true; - return ts.forEachTrailingCommentRange(text, position, cb) || - ts.forEachLeadingCommentRange(text, position, cb) || - false; // return boolean instead of undefined if no comment is found + return ts.getTrailingCommentRanges(text, position) !== undefined || + ts.getTrailingCommentRanges(text, position) !== undefined; } From c795a3d2de2a5ef0c1f4787f770480c38437747c Mon Sep 17 00:00:00 2001 From: Noah Chen Date: Wed, 18 Jan 2017 11:08:17 -0500 Subject: [PATCH 032/131] Add test to circleci for environments with typescript@2.0.10 (#2034) --- circle.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/circle.yml b/circle.yml index 1387283aaff..4b2d8cf7528 100644 --- a/circle.yml +++ b/circle.yml @@ -4,10 +4,11 @@ general: - gh-pages dependencies: pre: - - case $CIRCLE_NODE_INDEX in 0) nvm use 4.2 ;; 1) nvm use 5.7 ;; 2) nvm use 6.1 ;; esac + - case $CIRCLE_NODE_INDEX in 0) nvm use 4.2 ;; 1) nvm use 5.7 ;; [2-3]) nvm use 6.1 ;; esac test: override: - - npm run verify + - case $CIRCLE_NODE_INDEX in [0-2]) npm run verify ;; 3) npm run clean && npm run compile && npm i typescript@2.0.10 && npm run test ;; esac: + parallel: true deployment: npm-latest: # match semver tag (e.g. 3.12.7) From 3a6d35b533652daf41cb4217c72c3601e52b5d15 Mon Sep 17 00:00:00 2001 From: Noah Chen Date: Wed, 18 Jan 2017 13:47:34 -0500 Subject: [PATCH 033/131] gitignore rules.json (#2069) --- .gitignore | 2 + docs/_data/rules.json | 1772 ----------------------------------------- 2 files changed, 2 insertions(+), 1772 deletions(-) delete mode 100644 docs/_data/rules.json diff --git a/.gitignore b/.gitignore index 7b4feb5fd1c..66385e0bbbb 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,5 @@ typings/.basedir.ts # vim swap files *.swo *.swp + +docs/_data/rules.json diff --git a/docs/_data/rules.json b/docs/_data/rules.json deleted file mode 100644 index 44c43320380..00000000000 --- a/docs/_data/rules.json +++ /dev/null @@ -1,1772 +0,0 @@ -[ - { - "ruleName": "adjacent-overload-signatures", - "description": "Enforces function overloads to be consecutive.", - "optionsDescription": "Not configurable.", - "options": null, - "optionExamples": [ - "true" - ], - "rationale": "Improves readability and organization by grouping naturally related items together.", - "type": "typescript", - "typescriptOnly": true - }, - { - "ruleName": "align", - "description": "Enforces vertical alignment.", - "rationale": "Helps maintain a readable, consistent style in your codebase.", - "optionsDescription": "\nThree arguments may be optionally provided:\n\n* `\"parameters\"` checks alignment of function parameters.\n* `\"arguments\"` checks alignment of function call arguments.\n* `\"statements\"` checks alignment of statements.", - "options": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "arguments", - "parameters", - "statements" - ] - }, - "minLength": 1, - "maxLength": 3 - }, - "optionExamples": [ - "[true, \"parameters\", \"statements\"]" - ], - "type": "style", - "typescriptOnly": false - }, - { - "ruleName": "array-type", - "description": "Requires using either 'T[]' or 'Array' for arrays.", - "hasFix": true, - "optionsDescription": "\nOne of the following arguments must be provided:\n\n* `\"array\"` enforces use of `T[]` for all types T.\n* `\"generic\"` enforces use of `Array` for all types T.\n* `\"array-simple\"` enforces use of `T[]` if `T` is a simple type (primitive or type reference).", - "options": { - "type": "string", - "enum": [ - "array", - "generic", - "array-simple" - ] - }, - "optionExamples": [ - "[true, \"array\"]", - "[true, \"generic\"]", - "[true, \"array-simple\"]" - ], - "type": "style", - "typescriptOnly": true - }, - { - "ruleName": "arrow-parens", - "description": "Requires parentheses around the parameters of arrow function definitions.", - "hasFix": true, - "rationale": "Maintains stylistic consistency with other arrow function definitions.", - "optionsDescription": "\nIf `ban-single-arg-parens` is specified, then arrow functions with one parameter\nmust not have parentheses if removing them is allowed by TypeScript.", - "options": { - "type": "string", - "enum": [ - "ban-single-arg-parens" - ] - }, - "optionExamples": [ - "true", - "[true, \"ban-single-arg-parens\"]" - ], - "type": "style", - "typescriptOnly": false - }, - { - "ruleName": "arrow-return-shorthand", - "description": "Suggests to convert `() => { return x; }` to `() => x`.", - "hasFix": true, - "optionsDescription": "\nIf `multiline` is specified, then this will warn even if the function spans multiple lines.", - "options": { - "type": "string", - "enum": [ - "multiline" - ] - }, - "optionExamples": [ - "[true]", - "[true, \"multiline\"]" - ], - "type": "style", - "typescriptOnly": false - }, - { - "ruleName": "ban", - "description": "Bans the use of specific functions or global methods.", - "optionsDescription": "\nA list of `['object', 'method', 'optional explanation here']` or `['globalMethod']` which ban `object.method()`\nor respectively `globalMethod()`.", - "options": { - "type": "list", - "listType": { - "type": "array", - "items": { - "type": "string" - }, - "minLength": 1, - "maxLength": 3 - } - }, - "optionExamples": [ - "[true, [\"someGlobalMethod\"], [\"someObject\", \"someFunction\"],\n [\"someObject\", \"otherFunction\", \"Optional explanation\"]]" - ], - "type": "functionality", - "typescriptOnly": false - }, - { - "ruleName": "callable-types", - "description": "An interface or literal type with just a call signature can be written as a function type.", - "rationale": "style", - "optionsDescription": "Not configurable.", - "options": null, - "type": "style", - "typescriptOnly": true - }, - { - "ruleName": "class-name", - "description": "Enforces PascalCased class and interface names.", - "rationale": "Makes it easy to differentitate classes from regular variables at a glance.", - "optionsDescription": "Not configurable.", - "options": null, - "optionExamples": [ - "true" - ], - "type": "style", - "typescriptOnly": false - }, - { - "ruleName": "comment-format", - "description": "Enforces formatting rules for single-line comments.", - "rationale": "Helps maintain a consistent, readable style in your codebase.", - "optionsDescription": "\nThree arguments may be optionally provided:\n\n* `\"check-space\"` requires that all single-line comments must begin with a space, as in `// comment`\n * note that comments starting with `///` are also allowed, for things such as `///`\n* `\"check-lowercase\"` requires that the first non-whitespace character of a comment must be lowercase, if applicable.\n* `\"check-uppercase\"` requires that the first non-whitespace character of a comment must be uppercase, if applicable.\n\nExceptions to `\"check-lowercase\"` or `\"check-uppercase\"` can be managed with object that may be passed as last argument.\n\nOne of two options can be provided in this object:\n \n * `\"ignoreWords\"` - array of strings - words that will be ignored at the beginning of the comment.\n * `\"ignorePattern\"` - string - RegExp pattern that will be ignored at the beginning of the comment.\n", - "options": { - "type": "array", - "items": { - "anyOf": [ - { - "type": "string", - "enum": [ - "check-space", - "check-lowercase", - "check-uppercase" - ] - }, - { - "type": "object", - "properties": { - "ignoreWords": { - "type": "array", - "items": { - "type": "string" - } - }, - "ignorePattern": { - "type": "string" - } - }, - "minProperties": 1, - "maxProperties": 1 - } - ] - }, - "minLength": 1, - "maxLength": 4 - }, - "optionExamples": [ - "[true, \"check-space\", \"check-uppercase\"]", - "[true, \"check-lowercase\", {\"ignoreWords\": [\"TODO\", \"HACK\"]}]", - "[true, \"check-lowercase\", {\"ignorePattern\": \"STD\\w{2,3}\\b\"}]" - ], - "type": "style", - "typescriptOnly": false - }, - { - "ruleName": "completed-docs", - "description": "Enforces documentation for important items be filled out.", - "optionsDescription": "\nEither `true` to enable for all, or any of\n`[\"classes\", \"functions\", \"methods\", \"properties\"]\nto choose individual ones.`", - "options": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "classes", - "functions", - "methods", - "properties" - ] - } - }, - "optionExamples": [ - "true", - "[true, \"classes\", \"functions\"]" - ], - "type": "style", - "typescriptOnly": false - }, - { - "ruleName": "curly", - "description": "Enforces braces for `if`/`for`/`do`/`while` statements.", - "rationale": "\n```ts\nif (foo === bar)\n foo++;\n bar++;\n```\n\nIn the code above, the author almost certainly meant for both `foo++` and `bar++`\nto be executed only if `foo === bar`. However, he forgot braces and `bar++` will be executed\nno matter what. This rule could prevent such a mistake.", - "optionsDescription": "Not configurable.", - "options": null, - "optionExamples": [ - "true" - ], - "type": "functionality", - "typescriptOnly": false - }, - { - "ruleName": "cyclomatic-complexity", - "description": "Enforces a threshold of cyclomatic complexity.", - "descriptionDetails": "\nCyclomatic complexity is assessed for each function of any type. A starting value of 20\nis assigned and this value is then incremented for every statement which can branch the\ncontrol flow within the function. The following statements and expressions contribute\nto cyclomatic complexity:\n* `catch`\n* `if` and `? :`\n* `||` and `&&` due to short-circuit evaluation\n* `for`, `for in` and `for of` loops\n* `while` and `do while` loops", - "rationale": "\nCyclomatic complexity is a code metric which indicates the level of complexity in a\nfunction. High cyclomatic complexity indicates confusing code which may be prone to\nerrors or difficult to modify.", - "optionsDescription": "\nAn optional upper limit for cyclomatic complexity can be specified. If no limit option\nis provided a default value of $(Rule.DEFAULT_THRESHOLD) will be used.", - "options": { - "type": "number", - "minimum": "$(Rule.MINIMUM_THRESHOLD)" - }, - "optionExamples": [ - "true", - "[true, 20]" - ], - "type": "maintainability", - "typescriptOnly": false - }, - { - "ruleName": "eofline", - "description": "Ensures the file ends with a newline.", - "rationale": "It is a [standard convention](http://stackoverflow.com/q/729692/3124288) to end files with a newline.", - "optionsDescription": "Not configurable.", - "options": null, - "optionExamples": [ - "true" - ], - "type": "maintainability", - "typescriptOnly": false - }, - { - "ruleName": "file-header", - "description": "Enforces a certain header comment for all files, matched by a regular expression.", - "optionsDescription": "Regular expression to match the header.", - "options": { - "type": "string" - }, - "optionExamples": [ - "[true, \"Copyright \\\\d{4}\"]" - ], - "type": "style", - "typescriptOnly": false - }, - { - "ruleName": "forin", - "description": "Requires a `for ... in` statement to be filtered with an `if` statement.", - "rationale": "\n```ts\nfor (let key in someObject) {\n if (someObject.hasOwnProperty(key)) {\n // code here\n }\n}\n```\nPrevents accidental interation over properties inherited from an object's prototype.\nSee [MDN's `for...in`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...in)\ndocumentation for more information about `for...in` loops.", - "optionsDescription": "Not configurable.", - "options": null, - "optionExamples": [ - "true" - ], - "type": "functionality", - "typescriptOnly": false - }, - { - "ruleName": "import-blacklist", - "description": "\nDisallows importing the specified modules directly via `import` and `require`.\nInstead only sub modules may be imported from that module.", - "rationale": "\nSome libraries allow importing their submodules instead of the entire module.\nThis is good practise as it avoids loading unused modules.", - "optionsDescription": "A list of blacklisted modules.", - "options": { - "type": "array", - "items": { - "type": "string" - }, - "minLength": 1 - }, - "optionExamples": [ - "true", - "[true, \"rxjs\", \"lodash\"]" - ], - "type": "functionality", - "typescriptOnly": false - }, - { - "ruleName": "import-spacing", - "description": "Ensures proper spacing between import statement keywords", - "optionsDescription": "Not configurable.", - "options": null, - "optionExamples": [ - "true" - ], - "type": "style", - "typescriptOnly": false - }, - { - "ruleName": "indent", - "description": "Enforces indentation with tabs or spaces.", - "rationale": "\nUsing only one of tabs or spaces for indentation leads to more consistent editor behavior,\ncleaner diffs in version control, and easier programatic manipulation.", - "optionsDescription": "\nOne of the following arguments must be provided:\n\n* `\"spaces\"` enforces consistent spaces.\n* `\"tabs\"` enforces consistent tabs.", - "options": { - "type": "string", - "enum": [ - "tabs", - "spaces" - ] - }, - "optionExamples": [ - "[true, \"spaces\"]" - ], - "type": "maintainability", - "typescriptOnly": false - }, - { - "ruleName": "interface-name", - "description": "Requires interface names to begin with a capital 'I'", - "rationale": "Makes it easy to differentitate interfaces from regular classes at a glance.", - "optionsDescription": "\nOne of the following two options must be provided:\n\n* `\"always-prefix\"` requires interface names to start with an \"I\"\n* `\"never-prefix\"` requires interface names to not have an \"I\" prefix", - "options": { - "type": "string", - "enum": [ - "always-prefix", - "never-prefix" - ] - }, - "optionExamples": [ - "[true, \"always-prefix\"]", - "[true, \"never-prefix\"]" - ], - "type": "style", - "typescriptOnly": true - }, - { - "ruleName": "interface-over-type-literal", - "description": "Prefer an interface declaration over a type literal (`type T = { ... }`)", - "rationale": "style", - "optionsDescription": "Not configurable.", - "options": null, - "type": "style", - "typescriptOnly": true - }, - { - "ruleName": "jsdoc-format", - "description": "Enforces basic format rules for JSDoc comments.", - "descriptionDetails": "\nThe following rules are enforced for JSDoc comments (comments starting with `/**`):\n\n* each line contains an asterisk and asterisks must be aligned\n* each asterisk must be followed by either a space or a newline (except for the first and the last)\n* the only characters before the asterisk on each line must be whitespace characters\n* one line comments must start with `/** ` and end with `*/`", - "rationale": "Helps maintain a consistent, readable style for JSDoc comments.", - "optionsDescription": "Not configurable.", - "options": null, - "optionExamples": [ - "true" - ], - "type": "style", - "typescriptOnly": false - }, - { - "ruleName": "label-position", - "description": "Only allows labels in sensible locations.", - "descriptionDetails": "This rule only allows labels to be on `do/for/while/switch` statements.", - "rationale": "\nLabels in JavaScript only can be used in conjunction with `break` or `continue`,\nconstructs meant to be used for loop flow control. While you can theoretically use\nlabels on any block statement in JS, it is considered poor code structure to do so.", - "optionsDescription": "Not configurable.", - "options": null, - "optionExamples": [ - "true" - ], - "type": "functionality", - "typescriptOnly": false - }, - { - "ruleName": "linebreak-style", - "description": "Enforces a consistent linebreak style.", - "optionsDescription": "\nOne of the following options must be provided:\n\n* `\"LF\"` requires LF (`\\n`) linebreaks\n* `\"CRLF\"` requires CRLF (`\\r\\n`) linebreaks", - "options": { - "type": "string", - "enum": [ - "LF", - "CRLF" - ] - }, - "optionExamples": [ - "[true, \"LF\"]", - "[true, \"CRLF\"]" - ], - "type": "maintainability", - "typescriptOnly": false - }, - { - "ruleName": "max-classes-per-file", - "description": "\nA file may not contain more than the specified number of classes", - "rationale": "\nEnsures that files have a single responsibility so that that classes each exist in their own files", - "optionsDescription": "\nThe one required argument is an integer indicating the maximum number of classes that can appear in a file.", - "options": { - "type": "array", - "items": [ - { - "type": "number", - "minimum": 1 - } - ], - "additionalItems": false, - "minLength": 1, - "maxLength": 2 - }, - "optionExamples": [ - "[true, 1]", - "[true, 5]" - ], - "type": "maintainability", - "typescriptOnly": false - }, - { - "ruleName": "max-file-line-count", - "description": "Requires files to remain under a certain number of lines", - "rationale": "\nLimiting the number of lines allowed in a file allows files to remain small, \nsingle purpose, and maintainable.", - "optionsDescription": "An integer indicating the maximum number of lines.", - "options": { - "type": "number", - "minimum": "1" - }, - "optionExamples": [ - "[true, 300]" - ], - "type": "maintainability", - "typescriptOnly": false - }, - { - "ruleName": "max-line-length", - "description": "Requires lines to be under a certain max length.", - "rationale": "\nLimiting the length of a line of code improves code readability.\nIt also makes comparing code side-by-side easier and improves compatibility with\nvarious editors, IDEs, and diff viewers.", - "optionsDescription": "An integer indicating the max length of lines.", - "options": { - "type": "number", - "minimum": "1" - }, - "optionExamples": [ - "[true, 120]" - ], - "type": "maintainability", - "typescriptOnly": false - }, - { - "ruleName": "member-access", - "description": "Requires explicit visibility declarations for class members.", - "rationale": "Explicit visibility declarations can make code more readable and accessible for those new to TS.", - "optionsDescription": "\nTwo arguments may be optionally provided:\n\n* `\"check-accessor\"` enforces explicit visibility on get/set accessors (can only be public)\n* `\"check-constructor\"` enforces explicit visibility on constructors (can only be public)", - "options": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "check-accessor", - "check-constructor" - ] - }, - "minLength": 0, - "maxLength": 2 - }, - "optionExamples": [ - "true", - "[true, \"check-accessor\"]" - ], - "type": "typescript", - "typescriptOnly": true - }, - { - "ruleName": "member-ordering", - "description": "Enforces member ordering.", - "rationale": "A consistent ordering for class members can make classes easier to read, navigate, and edit.", - "optionsDescription": "\nOne argument, which is an object, must be provided. It should contain an `order` property.\nThe `order` property should have a value of one of the following strings:\n\n* `fields-first`\n* `statics-first`\n* `instance-sandwich`\n\nAlternatively, the value for `order` maybe be an array consisting of the following strings:\n\n* `public-static-field`\n* `protected-static-field`\n* `private-static-field`\n* `public-instance-field`\n* `protected-instance-field`\n* `private-instance-field`\n* `constructor`\n* `public-static-method`\n* `protected-static-method`\n* `private-static-method`\n* `public-instance-method`\n* `protected-instance-method`\n* `private-instance-method`\n\nThis is useful if one of the preset orders does not meet your needs.", - "options": { - "type": "object", - "properties": { - "order": { - "oneOf": [ - { - "type": "string", - "enum": [ - "fields-first", - "statics-first", - "instance-sandwich" - ] - }, - { - "type": "array", - "items": { - "type": "string", - "enum": [ - "public-static-field", - "public-static-method", - "protected-static-field", - "protected-static-method", - "private-static-field", - "private-static-method", - "public-instance-field", - "protected-instance-field", - "private-instance-field", - "constructor", - "public-instance-method", - "protected-instance-method", - "private-instance-method" - ] - }, - "maxLength": 13 - } - ] - } - }, - "additionalProperties": false - }, - "optionExamples": [ - "[true, { \"order\": \"fields-first\" }]" - ], - "type": "typescript", - "typescriptOnly": true - }, - { - "ruleName": "new-parens", - "description": "Requires parentheses when invoking a constructor via the `new` keyword.", - "rationale": "Maintains stylistic consistency with other function calls.", - "optionsDescription": "Not configurable.", - "options": null, - "optionExamples": [ - "true" - ], - "type": "style", - "typescriptOnly": false - }, - { - "ruleName": "no-angle-bracket-type-assertion", - "description": "Requires the use of `as Type` for type assertions instead of ``.", - "hasFix": true, - "rationale": "\nBoth formats of type assertions have the same effect, but only `as` type assertions\nwork in `.tsx` files. This rule ensures that you have a consistent type assertion style\nacross your codebase.", - "optionsDescription": "Not configurable.", - "options": null, - "optionExamples": [ - "true" - ], - "type": "style", - "typescriptOnly": true - }, - { - "ruleName": "no-any", - "description": "Diallows usages of `any` as a type declaration.", - "rationale": "Using `any` as a type declaration nullifies the compile-time benefits of the type system.", - "optionsDescription": "Not configurable.", - "options": null, - "optionExamples": [ - "true" - ], - "type": "typescript", - "typescriptOnly": true - }, - { - "ruleName": "no-arg", - "description": "Disallows use of `arguments.callee`.", - "rationale": "\nUsing `arguments.callee` makes various performance optimizations impossible.\nSee [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/arguments/callee)\nfor more details on why to avoid `arguments.callee`.", - "optionsDescription": "Not configurable.", - "options": null, - "optionExamples": [ - "true" - ], - "type": "functionality", - "typescriptOnly": false - }, - { - "ruleName": "no-bitwise", - "description": "Disallows bitwise operators.", - "descriptionDetails": "\nSpecifically, the following bitwise operators are banned:\n`&`, `&=`, `|`, `|=`,\n`^`, `^=`, `<<`, `<<=`,\n`>>`, `>>=`, `>>>`, `>>>=`, and `~`.\nThis rule does not ban the use of `&` and `|` for intersection and union types.", - "rationale": "\nBitwise operators are often typos - for example `bool1 & bool2` instead of `bool1 && bool2`.\nThey also can be an indicator of overly clever code which decreases maintainability.", - "optionsDescription": "Not configurable.", - "options": null, - "optionExamples": [ - "true" - ], - "type": "functionality", - "typescriptOnly": false - }, - { - "ruleName": "no-boolean-literal-compare", - "description": "Warns on comparison to a boolean literal, as in `x === true`.", - "hasFix": true, - "optionsDescription": "Not configurable.", - "options": null, - "optionExamples": [ - "true" - ], - "type": "style", - "typescriptOnly": true, - "requiresTypeInfo": true - }, - { - "ruleName": "no-conditional-assignment", - "description": "Disallows any type of assignment in conditionals.", - "descriptionDetails": "This applies to `do-while`, `for`, `if`, and `while` statements.", - "rationale": "\nAssignments in conditionals are often typos:\nfor example `if (var1 = var2)` instead of `if (var1 == var2)`.\nThey also can be an indicator of overly clever code which decreases maintainability.", - "optionsDescription": "Not configurable.", - "options": null, - "optionExamples": [ - "true" - ], - "type": "functionality", - "typescriptOnly": false - }, - { - "ruleName": "no-consecutive-blank-lines", - "description": "Disallows one or more blank lines in a row.", - "rationale": "Helps maintain a readable style in your codebase.", - "optionsDescription": "\nAn optional number of maximum allowed sequential blanks can be specified. If no value\nis provided, a default of $(Rule.DEFAULT_ALLOWED_BLANKS) will be used.", - "options": { - "type": "number", - "minimum": "$(Rule.MINIMUM_ALLOWED_BLANKS)" - }, - "optionExamples": [ - "true", - "[true, 2]" - ], - "type": "style", - "typescriptOnly": false - }, - { - "ruleName": "no-console", - "description": "Bans the use of specified `console` methods.", - "rationale": "In general, `console` methods aren't appropriate for production code.", - "optionsDescription": "A list of method names to ban.", - "options": { - "type": "array", - "items": { - "type": "string" - } - }, - "optionExamples": [ - "[true, \"log\", \"error\"]" - ], - "type": "functionality", - "typescriptOnly": false - }, - { - "ruleName": "no-construct", - "description": "Disallows access to the constructors of `String`, `Number`, and `Boolean`.", - "descriptionDetails": "Disallows constructor use such as `new Number(foo)` but does not disallow `Number(foo)`.", - "rationale": "\nThere is little reason to use `String`, `Number`, or `Boolean` as constructors.\nIn almost all cases, the regular function-call version is more appropriate.\n[More details](http://stackoverflow.com/q/4719320/3124288) are available on StackOverflow.", - "optionsDescription": "Not configurable.", - "options": null, - "optionExamples": [ - "true" - ], - "type": "functionality", - "typescriptOnly": false - }, - { - "ruleName": "no-debugger", - "description": "Disallows `debugger` statements.", - "rationale": "In general, `debugger` statements aren't appropriate for production code.", - "optionsDescription": "Not configurable.", - "options": null, - "optionExamples": [ - "true" - ], - "type": "functionality", - "typescriptOnly": false - }, - { - "ruleName": "no-default-export", - "description": "Disallows default exports in ES6-style modules.", - "descriptionDetails": "Use named exports instead.", - "rationale": "\nNamed imports/exports [promote clarity](https://github.com/palantir/tslint/issues/1182#issue-151780453).\nIn addition, current tooling differs on the correct way to handle default imports/exports.\nAvoiding them all together can help avoid tooling bugs and conflicts.", - "optionsDescription": "Not configurable.", - "options": null, - "optionExamples": [ - "true" - ], - "type": "maintainability", - "typescriptOnly": false - }, - { - "ruleName": "no-duplicate-variable", - "description": "Disallows duplicate variable declarations in the same block scope.", - "descriptionDetails": "\nThis rule is only useful when using the `var` keyword -\nthe compiler will detect redeclarations of `let` and `const` variables.", - "rationale": "\nA variable can be reassigned if necessary -\nthere's no good reason to have a duplicate variable declaration.", - "optionsDescription": "Not configurable.", - "options": null, - "optionExamples": [ - "true" - ], - "type": "functionality", - "typescriptOnly": false - }, - { - "ruleName": "no-empty-interface", - "description": "Forbids empty interfaces.", - "rationale": "An empty interface is equivalent to its supertype (or `{}`).", - "optionsDescription": "Not configurable.", - "options": null, - "type": "typescript", - "typescriptOnly": true - }, - { - "ruleName": "no-empty", - "description": "Disallows empty blocks.", - "descriptionDetails": "Blocks with a comment inside are not considered empty.", - "rationale": "Empty blocks are often indicators of missing code.", - "optionsDescription": "Not configurable.", - "options": null, - "optionExamples": [ - "true" - ], - "type": "functionality", - "typescriptOnly": false - }, - { - "ruleName": "no-eval", - "description": "Disallows `eval` function invocations.", - "rationale": "\n`eval()` is dangerous as it allows arbitrary code execution with full privileges. There are\n[alternatives](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval)\nfor most of the use cases for `eval()`.", - "optionsDescription": "Not configurable.", - "options": null, - "optionExamples": [ - "true" - ], - "type": "functionality", - "typescriptOnly": false - }, - { - "ruleName": "no-for-in-array", - "description": "Disallows iterating over an array with a for-in loop.", - "descriptionDetails": "\nA for-in loop (`for (var k in o)`) iterates over the properties of an Object.\n\nWhile it is legal to use for-in loops with array types, it is not common.\nfor-in will iterate over the indices of the array as strings, omitting any \"holes\" in\nthe array.\n\nMore common is to use for-of, which iterates over the values of an array.\nIf you want to iterate over the indices, alternatives include:\n\narray.forEach((value, index) => { ... });\nfor (const [index, value] of array.entries()) { ... }\nfor (let i = 0; i < array.length; i++) { ... }\n", - "optionsDescription": "Not configurable.", - "options": null, - "optionExamples": [ - "true" - ], - "requiresTypeInfo": true, - "type": "functionality", - "typescriptOnly": false - }, - { - "ruleName": "no-inferrable-types", - "description": "Disallows explicit type declarations for variables or parameters initialized to a number, string, or boolean.", - "rationale": "Explicit types where they can be easily infered by the compiler make code more verbose.", - "optionsDescription": "\nOne argument may be optionally provided:\n\n* `ignore-params` allows specifying an inferrable type annotation for function params.\nThis can be useful when combining with the `typedef` rule.", - "options": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "ignore-params" - ] - }, - "minLength": 0, - "maxLength": 1 - }, - "optionExamples": [ - "true", - "[true, \"ignore-params\"]" - ], - "type": "typescript", - "typescriptOnly": true - }, - { - "ruleName": "no-inferred-empty-object-type", - "description": "Disallow type inference of {} (empty object type) at function and constructor call sites", - "optionsDescription": "Not configurable.", - "options": null, - "optionExamples": [ - "true" - ], - "type": "functionality", - "typescriptOnly": true, - "requiresTypeInfo": true - }, - { - "ruleName": "no-internal-module", - "description": "Disallows internal `module`", - "rationale": "Using `module` leads to a confusion of concepts with external modules. Use the newer `namespace` keyword instead.", - "optionsDescription": "Not configurable.", - "options": null, - "optionExamples": [ - "true" - ], - "type": "typescript", - "typescriptOnly": true - }, - { - "ruleName": "no-invalid-this", - "description": "Disallows using the `this` keyword outside of classes.", - "rationale": "See [the rule's author's rationale here.](https://github.com/palantir/tslint/pull/1105#issue-147549402)", - "optionsDescription": "\nOne argument may be optionally provided:\n\n* `check-function-in-method` disallows using the `this` keyword in functions within class methods.", - "options": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "check-function-in-method" - ] - }, - "minLength": 0, - "maxLength": 1 - }, - "optionExamples": [ - "true", - "[true, \"check-function-in-method\"]" - ], - "type": "functionality", - "typescriptOnly": false - }, - { - "ruleName": "no-magic-numbers", - "description": "\nDisallows the use constant number values outside of variable assignments.\nWhen no list of allowed values is specified, -1, 0 and 1 are allowed by default.", - "rationale": "\nMagic numbers should be avoided as they often lack documentation, forcing\nthem to be stored in variables gives them implicit documentation.", - "optionsDescription": "A list of allowed numbers.", - "options": { - "type": "array", - "items": { - "type": "number" - }, - "minLength": 1 - }, - "optionExamples": [ - "true", - "[true, 1, 2, 3]" - ], - "type": "typescript", - "typescriptOnly": false - }, - { - "ruleName": "no-mergeable-namespace", - "description": "Disallows mergeable namespaces in the same file.", - "optionsDescription": "Not configurable.", - "options": null, - "optionExamples": [ - "true" - ], - "type": "maintainability", - "typescriptOnly": true - }, - { - "ruleName": "no-misused-new", - "description": "Warns on apparent attempts to define constructors for interfaces or `new` for classes.", - "optionsDescription": "Not configurable.", - "options": null, - "optionExamples": [ - "true" - ], - "type": "functionality", - "typescriptOnly": true - }, - { - "ruleName": "no-namespace", - "description": "Disallows use of internal `module`s and `namespace`s.", - "descriptionDetails": "This rule still allows the use of `declare module ... {}`", - "rationale": "\nES6-style external modules are the standard way to modularize code.\nUsing `module {}` and `namespace {}` are outdated ways to organize TypeScript code.", - "optionsDescription": "\nOne argument may be optionally provided:\n\n* `allow-declarations` allows `declare namespace ... {}` to describe external APIs.", - "options": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "allow-declarations" - ] - }, - "minLength": 0, - "maxLength": 1 - }, - "optionExamples": [ - "true", - "[true, \"allow-declarations\"]" - ], - "type": "typescript", - "typescriptOnly": true - }, - { - "ruleName": "no-null-keyword", - "description": "Disallows use of the `null` keyword literal.", - "rationale": "\nInstead of having the dual concepts of `null` and`undefined` in a codebase,\nthis rule ensures that only `undefined` is used.", - "optionsDescription": "Not configurable.", - "options": null, - "optionExamples": [ - "true" - ], - "type": "functionality", - "typescriptOnly": false - }, - { - "ruleName": "no-parameter-properties", - "description": "Disallows parameter properties in class constructors.", - "rationale": "\nParameter properties can be confusing to those new to TS as they are less explicit\nthan other ways of declaring and initializing class members.", - "optionsDescription": "Not configurable.", - "options": null, - "optionExamples": [ - "true" - ], - "type": "style", - "typescriptOnly": true - }, - { - "ruleName": "no-reference", - "description": "Disallows `/// ` imports (use ES6-style imports instead).", - "rationale": "\nUsing `/// ` comments to load other files is outdated.\nUse ES6-style imports to reference other files.", - "optionsDescription": "Not configurable.", - "options": null, - "optionExamples": [ - "true" - ], - "type": "typescript", - "typescriptOnly": false - }, - { - "ruleName": "no-require-imports", - "description": "Disallows invocation of `require()`.", - "rationale": "Prefer the newer ES6-style imports over `require()`.", - "optionsDescription": "Not configurable.", - "options": null, - "optionExamples": [ - "true" - ], - "type": "maintainability", - "typescriptOnly": false - }, - { - "ruleName": "no-shadowed-variable", - "description": "Disallows shadowing variable declarations.", - "rationale": "Shadowing a variable masks access to it and obscures to what value an identifier actually refers.", - "optionsDescription": "Not configurable.", - "options": null, - "optionExamples": [ - "true" - ], - "type": "functionality", - "typescriptOnly": false - }, - { - "ruleName": "no-string-literal", - "description": "Disallows object access via string literals.", - "rationale": "Encourages using strongly-typed property access.", - "optionsDescription": "Not configurable.", - "options": null, - "optionExamples": [ - "true" - ], - "type": "functionality", - "typescriptOnly": false - }, - { - "ruleName": "no-string-throw", - "description": "Flags throwing plain strings or concatenations of strings because only Errors produce proper stack traces.", - "hasFix": true, - "options": null, - "optionsDescription": "Not configurable.", - "type": "functionality", - "typescriptOnly": false - }, - { - "ruleName": "no-switch-case-fall-through", - "description": "Disallows falling through case statements.", - "descriptionDetails": "\nFor example, the following is not allowed:\n\n```ts\nswitch(foo) {\n case 1:\n someFunc(foo);\n case 2:\n someOtherFunc(foo);\n}\n```\n\nHowever, fall through is allowed when case statements are consecutive or\na magic `/* falls through */` comment is present. The following is valid:\n\n```ts\nswitch(foo) {\n case 1:\n someFunc(foo);\n /* falls through */\n case 2:\n case 3:\n someOtherFunc(foo);\n}\n```", - "rationale": "Fall though in switch statements is often unintentional and a bug.", - "optionsDescription": "Not configurable.", - "options": null, - "optionExamples": [ - "true" - ], - "type": "functionality", - "typescriptOnly": false - }, - { - "ruleName": "no-trailing-whitespace", - "description": "Disallows trailing whitespace at the end of a line.", - "rationale": "Keeps version control diffs clean as it prevents accidental whitespace from being committed.", - "optionsDescription": "Not configurable.", - "options": null, - "optionExamples": [ - "true" - ], - "type": "maintainability", - "typescriptOnly": false - }, - { - "ruleName": "no-unnecessary-qualifier", - "description": "Warns when a namespace qualifier (`A.x`) is unnecessary.", - "hasFix": true, - "optionsDescription": "Not configurable.", - "options": null, - "optionExamples": [ - "true" - ], - "type": "style", - "typescriptOnly": true, - "requiresTypeInfo": true - }, - { - "ruleName": "no-unsafe-finally", - "description": "\nDisallows control flow statements, such as `return`, `continue`,\n`break` and `throws` in finally blocks.", - "descriptionDetails": "", - "rationale": "\nWhen used inside `finally` blocks, control flow statements,\nsuch as `return`, `continue`, `break` and `throws`\noverride any other control flow statements in the same try/catch scope.\nThis is confusing and unexpected behavior.", - "optionsDescription": "Not configurable.", - "options": null, - "optionExamples": [ - "true" - ], - "type": "functionality", - "typescriptOnly": false - }, - { - "ruleName": "no-unused-expression", - "description": "Disallows unused expression statements.", - "descriptionDetails": "\nUnused expressions are expression statements which are not assignments or function calls\n(and thus usually no-ops).", - "rationale": "\nDetects potential errors where an assignment or function call was intended.", - "optionsDescription": "\nOne argument may be optionally provided:\n\n* `allow-fast-null-checks` allows to use logical operators to perform fast null checks and perform\nmethod or function calls for side effects (e.g. `e && e.preventDefault()`).", - "options": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "allow-fast-null-checks" - ] - }, - "minLength": 0, - "maxLength": 1 - }, - "optionExamples": [ - "true", - "[true, \"allow-fast-null-checks\"]" - ], - "type": "functionality", - "typescriptOnly": false - }, - { - "ruleName": "no-unused-new", - "description": "Disallows unused 'new' expression statements.", - "descriptionDetails": "\nUnused 'new' expressions indicate that a constructor is being invoked solely for its side effects.", - "rationale": "\nDetects constructs such as `new SomeClass()`, where a constructor is used solely for its side effects, which is considered\npoor style.", - "optionsDescription": "Not configurable.", - "options": null, - "optionExamples": [ - "true" - ], - "type": "functionality", - "typescriptOnly": false - }, - { - "ruleName": "no-unused-variable", - "deprecationMessage": "Use the tsc compiler options --noUnusedParameters and --noUnusedLocals instead.", - "description": "Disallows unused imports, variables, functions and private class members.", - "hasFix": true, - "optionsDescription": "\nThree optional arguments may be optionally provided:\n\n* `\"check-parameters\"` disallows unused function and constructor parameters.\n * NOTE: this option is experimental and does not work with classes\n that use abstract method declarations, among other things.\n* `\"react\"` relaxes the rule for a namespace import named `React`\n(from either the module `\"react\"` or `\"react/addons\"`).\nAny JSX expression in the file will be treated as a usage of `React`\n(because it expands to `React.createElement `).\n* `{\"ignore-pattern\": \"pattern\"}` where pattern is a case-sensitive regexp.\nVariable names that match the pattern will be ignored.", - "options": { - "type": "array", - "items": { - "oneOf": [ - { - "type": "string", - "enum": [ - "check-parameters", - "react" - ] - }, - { - "type": "object", - "properties": { - "ignore-pattern": { - "type": "string" - } - }, - "additionalProperties": false - } - ] - }, - "minLength": 0, - "maxLength": 3 - }, - "optionExamples": [ - "[true, \"react\"]", - "[true, {\"ignore-pattern\": \"^_\"}]" - ], - "type": "functionality", - "typescriptOnly": true - }, - { - "ruleName": "no-use-before-declare", - "description": "Disallows usage of variables before their declaration.", - "descriptionDetails": "\nThis rule is primarily useful when using the `var` keyword -\nthe compiler will detect if a `let` and `const` variable is used before it is declared.", - "optionsDescription": "Not configurable.", - "options": null, - "optionExamples": [ - "true" - ], - "type": "functionality", - "typescriptOnly": false - }, - { - "ruleName": "no-var-keyword", - "description": "Disallows usage of the `var` keyword.", - "descriptionDetails": "Use `let` or `const` instead.", - "hasFix": true, - "optionsDescription": "Not configurable.", - "options": null, - "optionExamples": [ - "true" - ], - "type": "functionality", - "typescriptOnly": false - }, - { - "ruleName": "no-var-requires", - "description": "Disallows the use of require statements except in import statements.", - "descriptionDetails": "\nIn other words, the use of forms such as `var module = require(\"module\")` are banned.\nInstead use ES6 style imports or `import foo = require('foo')` imports.", - "optionsDescription": "Not configurable.", - "options": null, - "optionExamples": [ - "true" - ], - "type": "typescript", - "typescriptOnly": true - }, - { - "ruleName": "no-void-expression", - "description": "Requires expressions of type `void` to appear in statement position.", - "optionsDescription": "Not configurable.", - "options": null, - "requiresTypeInfo": true, - "type": "functionality", - "typescriptOnly": false - }, - { - "ruleName": "object-literal-key-quotes", - "description": "Enforces consistent object literal property quote style.", - "descriptionDetails": "\nObject literal property names can be defined in two ways: using literals or using strings.\nFor example, these two objects are equivalent:\n\nvar object1 = {\n property: true\n};\n\nvar object2 = {\n \"property\": true\n};\n\nIn many cases, it doesn’t matter if you choose to use an identifier instead of a string\nor vice-versa. Even so, you might decide to enforce a consistent style in your code.\n\nThis rules lets you enforce consistent quoting of property names. Either they should always\nbe quoted (default behavior) or quoted only as needed (\"as-needed\").", - "hasFix": true, - "optionsDescription": "\nPossible settings are:\n\n* `\"always\"`: Property names should always be quoted. (This is the default.)\n* `\"as-needed\"`: Only property names which require quotes may be quoted (e.g. those with spaces in them).\n* `\"consistent\"`: Property names should either all be quoted or unquoted.\n* `\"consistent-as-needed\"`: If any property name requires quotes, then all properties must be quoted. Otherwise, no\nproperty names may be quoted.\n\nFor ES6, computed property names (`{[name]: value}`) and methods (`{foo() {}}`) never need\nto be quoted.", - "options": { - "type": "string", - "enum": [ - "always", - "as-needed", - "consistent", - "consistent-as-needed" - ] - }, - "optionExamples": [ - "[true, \"as-needed\"]", - "[true, \"always\"]" - ], - "type": "style", - "typescriptOnly": false - }, - { - "ruleName": "object-literal-shorthand", - "description": "Enforces use of ES6 object literal shorthand when possible.", - "optionsDescription": "Not configurable.", - "options": null, - "optionExamples": [ - "true" - ], - "type": "style", - "typescriptOnly": false - }, - { - "ruleName": "object-literal-sort-keys", - "description": "Requires keys in object literals to be sorted alphabetically", - "rationale": "Useful in preventing merge conflicts", - "optionsDescription": "Not configurable.", - "options": null, - "optionExamples": [ - "true" - ], - "type": "maintainability", - "typescriptOnly": false - }, - { - "ruleName": "one-line", - "description": "Requires the specified tokens to be on the same line as the expression preceding them.", - "optionsDescription": "\nFive arguments may be optionally provided:\n\n* `\"check-catch\"` checks that `catch` is on the same line as the closing brace for `try`.\n* `\"check-finally\"` checks that `finally` is on the same line as the closing brace for `catch`.\n* `\"check-else\"` checks that `else` is on the same line as the closing brace for `if`.\n* `\"check-open-brace\"` checks that an open brace falls on the same line as its preceding expression.\n* `\"check-whitespace\"` checks preceding whitespace for the specified tokens.", - "options": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "check-catch", - "check-finally", - "check-else", - "check-open-brace", - "check-whitespace" - ] - }, - "minLength": 0, - "maxLength": 5 - }, - "optionExamples": [ - "[true, \"check-catch\", \"check-finally\", \"check-else\"]" - ], - "type": "style", - "typescriptOnly": false - }, - { - "ruleName": "one-variable-per-declaration", - "description": "Disallows multiple variable definitions in the same declaration statement.", - "optionsDescription": "\nOne argument may be optionally provided:\n\n* `ignore-for-loop` allows multiple variable definitions in a for loop declaration.", - "options": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "ignore-for-loop" - ] - }, - "minLength": 0, - "maxLength": 1 - }, - "optionExamples": [ - "true", - "[true, \"ignore-for-loop\"]" - ], - "type": "style", - "typescriptOnly": false - }, - { - "ruleName": "only-arrow-functions", - "description": "Disallows traditional (non-arrow) function expressions.", - "rationale": "Traditional functions don't bind lexical scope, which can lead to unexpected behavior when accessing 'this'.", - "optionsDescription": "\nTwo arguments may be optionally provided:\n\n* `\"allow-declarations\"` allows standalone function declarations.\n* `\"allow-named-functions\"` allows the expression `function foo() {}` but not `function() {}`.\n ", - "options": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "allow-declarations", - "allow-named-functions" - ] - }, - "minLength": 0, - "maxLength": 1 - }, - "optionExamples": [ - "true", - "[true, \"allow-declarations\", \"allow-named-functions\"]" - ], - "type": "typescript", - "typescriptOnly": false - }, - { - "ruleName": "ordered-imports", - "description": "Requires that import statements be alphabetized.", - "descriptionDetails": "\nEnforce a consistent ordering for ES6 imports:\n- Named imports must be alphabetized (i.e. \"import {A, B, C} from \"foo\";\")\n - The exact ordering can be controlled by the named-imports-order option.\n - \"longName as name\" imports are ordered by \"longName\".\n- Import sources must be alphabetized within groups, i.e.:\n import * as foo from \"a\";\n import * as bar from \"b\";\n- Groups of imports are delineated by blank lines. You can use these to group imports\n however you like, e.g. by first- vs. third-party or thematically.", - "hasFix": true, - "optionsDescription": "\nYou may set the `\"import-sources-order\"` option to control the ordering of source\nimports (the `\"foo\"` in `import {A, B, C} from \"foo\"`).\n\nPossible values for `\"import-sources-order\"` are:\n\n* `\"case-insensitive'`: Correct order is `\"Bar\"`, `\"baz\"`, `\"Foo\"`. (This is the default.)\n* `\"lowercase-first\"`: Correct order is `\"baz\"`, `\"Bar\"`, `\"Foo\"`.\n* `\"lowercase-last\"`: Correct order is `\"Bar\"`, `\"Foo\"`, `\"baz\"`.\n* `\"any\"`: Allow any order.\n\nYou may set the `\"named-imports-order\"` option to control the ordering of named\nimports (the `{A, B, C}` in `import {A, B, C} from \"foo\"`).\n\nPossible values for `\"named-imports-order\"` are:\n\n* `\"case-insensitive'`: Correct order is `{A, b, C}`. (This is the default.)\n* `\"lowercase-first\"`: Correct order is `{b, A, C}`.\n* `\"lowercase-last\"`: Correct order is `{A, C, b}`.\n* `\"any\"`: Allow any order.\n\n ", - "options": { - "type": "object", - "properties": { - "import-sources-order": { - "type": "string", - "enum": [ - "case-insensitive", - "lowercase-first", - "lowercase-last", - "any" - ] - }, - "named-imports-order": { - "type": "string", - "enum": [ - "case-insensitive", - "lowercase-first", - "lowercase-last", - "any" - ] - } - }, - "additionalProperties": false - }, - "optionExamples": [ - "true", - "[true, {\"import-sources-order\": \"lowercase-last\", \"named-imports-order\": \"lowercase-first\"}]" - ], - "type": "style", - "typescriptOnly": false - }, - { - "ruleName": "prefer-const", - "description": "Requires that variable declarations use `const` instead of `let` if possible.", - "descriptionDetails": "\nIf a variable is only assigned to once when it is declared, it should be declared using 'const'", - "hasFix": true, - "optionsDescription": "Not configurable.", - "options": null, - "optionExamples": [ - "true" - ], - "type": "maintainability", - "typescriptOnly": false - }, - { - "ruleName": "prefer-for-of", - "description": "Recommends a 'for-of' loop over a standard 'for' loop if the index is only used to access the array being iterated.", - "rationale": "A for(... of ...) loop is easier to implement and read when the index is not needed.", - "optionsDescription": "Not configurable.", - "options": null, - "optionExamples": [ - "true" - ], - "type": "typescript", - "typescriptOnly": false - }, - { - "ruleName": "prefer-method-signature", - "description": "Prefer `foo(): void` over `foo: () => void` in interfaces and types.", - "hasFix": true, - "optionsDescription": "Not configurable.", - "options": null, - "optionExamples": [ - "true" - ], - "type": "style", - "typescriptOnly": false - }, - { - "ruleName": "promise-function-async", - "description": "Requires any function or method that returns a promise to be marked async.", - "rationale": "\nEnsures that each function is only capable of 1) returning a rejected promise, or 2)\nthrowing an Error object. In contrast, non-`async` `Promise`-returning functions\nare technically capable of either. This practice removes a requirement for consuming\ncode to handle both cases.\n ", - "optionsDescription": "Not configurable.", - "options": null, - "optionExamples": [ - "true" - ], - "type": "typescript", - "typescriptOnly": false, - "requiresTypeInfo": true - }, - { - "ruleName": "quotemark", - "description": "Requires single or double quotes for string literals.", - "hasFix": true, - "optionsDescription": "\nFive arguments may be optionally provided:\n\n* `\"single\"` enforces single quotes.\n* `\"double\"` enforces double quotes.\n* `\"jsx-single\"` enforces single quotes for JSX attributes.\n* `\"jsx-double\"` enforces double quotes for JSX attributes.\n* `\"avoid-escape\"` allows you to use the \"other\" quotemark in cases where escaping would normally be required.\nFor example, `[true, \"double\", \"avoid-escape\"]` would not report a failure on the string literal `'Hello \"World\"'`.", - "options": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "single", - "double", - "jsx-single", - "jsx-double", - "avoid-escape" - ] - }, - "minLength": 0, - "maxLength": 5 - }, - "optionExamples": [ - "[true, \"single\", \"avoid-escape\"]", - "[true, \"single\", \"jsx-double\"]" - ], - "type": "style", - "typescriptOnly": false - }, - { - "ruleName": "radix", - "description": "Requires the radix parameter to be specified when calling `parseInt`.", - "rationale": "\nFrom [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseInt):\n> Always specify this parameter to eliminate reader confusion and to guarantee predictable behavior.\n> Different implementations produce different results when a radix is not specified, usually defaulting the value to 10.", - "optionsDescription": "Not configurable.", - "options": null, - "optionExamples": [ - "true" - ], - "type": "functionality", - "typescriptOnly": false - }, - { - "ruleName": "restrict-plus-operands", - "description": "When adding two variables, operands must both be of type number or of type string.", - "optionsDescription": "Not configurable.", - "options": null, - "optionExamples": [ - "true" - ], - "type": "functionality", - "typescriptOnly": false, - "requiresTypeInfo": true - }, - { - "ruleName": "semicolon", - "description": "Enforces consistent semicolon usage at the end of every statement.", - "hasFix": true, - "optionsDescription": "\nOne of the following arguments must be provided:\n\n* `\"always\"` enforces semicolons at the end of every statement.\n* `\"never\"` disallows semicolons at the end of every statement except for when they are necessary.\n\nThe following arguments may be optionaly provided:\n\n* `\"ignore-interfaces\"` skips checking semicolons at the end of interface members.\n* `\"ignore-bound-class-methods\"` skips checking semicolons at the end of bound class methods.", - "options": { - "type": "array", - "items": [ - { - "type": "string", - "enum": [ - "always", - "never" - ] - }, - { - "type": "string", - "enum": [ - "ignore-interfaces" - ] - } - ], - "additionalItems": false - }, - "optionExamples": [ - "[true, \"always\"]", - "[true, \"never\"]", - "[true, \"always\", \"ignore-interfaces\"]", - "[true, \"always\", \"ignore-bound-class-methods\"]" - ], - "type": "style", - "typescriptOnly": false - }, - { - "description": "Require or disallow a space before function parenthesis", - "hasFix": true, - "optionExamples": [ - "true", - "[true, \"always\"]", - "[true, \"never\"]", - "[true, {\"anonymous\": \"always\", \"named\": \"never\", \"asyncArrow\": \"always\"}]" - ], - "options": { - "properties": { - "anonymous": { - "enum": [ - "always", - "never" - ], - "type": "string" - }, - "asyncArrow": { - "enum": [ - "always", - "never" - ], - "type": "string" - }, - "constructor": { - "enum": [ - "always", - "never" - ], - "type": "string" - }, - "method": { - "enum": [ - "always", - "never" - ], - "type": "string" - }, - "named": { - "enum": [ - "always", - "never" - ], - "type": "string" - } - }, - "type": "object" - }, - "optionsDescription": "\nOne argument which is an object which may contain the keys `anonymous`, `named`, and `asyncArrow`\nThese should be set to either `\"always\"` or `\"never\"`.\n\n* `\"anonymous\"` checks before the opening paren in anonymous functions\n* `\"named\"` checks before the opening paren in named functions\n* `\"asyncArrow\"` checks before the opening paren in async arrow functions\n* `\"method\"` checks before the opening paren in class methods\n* `\"constructor\"` checks before the opening paren in class constructors\n ", - "ruleName": "space-before-function-paren", - "type": "style", - "typescriptOnly": false - }, - { - "ruleName": "strict-boolean-expressions", - "description": "Usage of && or || operators should be with boolean operands and\nexpressions in If, Do, While and For statements should be of type boolean", - "optionsDescription": "Not configurable.", - "options": null, - "optionExamples": [ - "true" - ], - "type": "functionality", - "typescriptOnly": true, - "requiresTypeInfo": true - }, - { - "ruleName": "switch-default", - "description": "Require a `default` case in all `switch` statements.", - "optionsDescription": "Not configurable.", - "options": null, - "optionExamples": [ - "true" - ], - "type": "functionality", - "typescriptOnly": false - }, - { - "ruleName": "trailing-comma", - "description": "\nRequires or disallows trailing commas in array and object literals, destructuring assignments, function and tuple typings,\nnamed imports and function parameters.", - "hasFix": true, - "optionsDescription": "\nOne argument which is an object with the keys `multiline` and `singleline`.\nBoth should be set to either `\"always\"` or `\"never\"`.\n\n* `\"multiline\"` checks multi-line object literals.\n* `\"singleline\"` checks single-line object literals.\n\nA array is considered \"multiline\" if its closing bracket is on a line\nafter the last array element. The same general logic is followed for\nobject literals, function and tuple typings, named import statements\nand function parameters.", - "options": { - "type": "object", - "properties": { - "multiline": { - "type": "string", - "enum": [ - "always", - "never" - ] - }, - "singleline": { - "type": "string", - "enum": [ - "always", - "never" - ] - } - }, - "additionalProperties": false - }, - "optionExamples": [ - "[true, {\"multiline\": \"always\", \"singleline\": \"never\"}]" - ], - "type": "maintainability", - "typescriptOnly": false - }, - { - "ruleName": "triple-equals", - "description": "Requires `===` and `!==` in place of `==` and `!=`.", - "optionsDescription": "\nTwo arguments may be optionally provided:\n\n* `\"allow-null-check\"` allows `==` and `!=` when comparing to `null`.\n* `\"allow-undefined-check\"` allows `==` and `!=` when comparing to `undefined`.", - "options": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "allow-null-check", - "allow-undefined-check" - ] - }, - "minLength": 0, - "maxLength": 2 - }, - "optionExamples": [ - "true", - "[true, \"allow-null-check\"]", - "[true, \"allow-undefined-check\"]" - ], - "type": "functionality", - "typescriptOnly": false - }, - { - "ruleName": "typedef", - "description": "Requires type definitions to exist.", - "optionsDescription": "\nSeven arguments may be optionally provided:\n\n* `\"call-signature\"` checks return type of functions.\n* `\"arrow-call-signature\"` checks return type of arrow functions.\n* `\"parameter\"` checks type specifier of function parameters for non-arrow functions.\n* `\"arrow-parameter\"` checks type specifier of function parameters for arrow functions.\n* `\"property-declaration\"` checks return types of interface properties.\n* `\"variable-declaration\"` checks variable declarations.\n* `\"member-variable-declaration\"` checks member variable declarations.", - "options": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "call-signature", - "arrow-call-signature", - "parameter", - "arrow-parameter", - "property-declaration", - "variable-declaration", - "member-variable-declaration" - ] - }, - "minLength": 0, - "maxLength": 7 - }, - "optionExamples": [ - "[true, \"call-signature\", \"parameter\", \"member-variable-declaration\"]" - ], - "type": "typescript", - "typescriptOnly": true - }, - { - "ruleName": "typedef-whitespace", - "description": "Requires or disallows whitespace for type definitions.", - "descriptionDetails": "Determines if a space is required or not before the colon in a type specifier.", - "optionsDescription": "\nTwo arguments which are both objects.\nThe first argument specifies how much space should be to the _left_ of a typedef colon.\nThe second argument specifies how much space should be to the _right_ of a typedef colon.\nEach key should have a value of `\"space\"` or `\"nospace\"`.\nPossible keys are:\n\n* `\"call-signature\"` checks return type of functions.\n* `\"index-signature\"` checks index type specifier of indexers.\n* `\"parameter\"` checks function parameters.\n* `\"property-declaration\"` checks object property declarations.\n* `\"variable-declaration\"` checks variable declaration.", - "options": { - "type": "array", - "items": [ - { - "type": "object", - "properties": { - "call-signature": { - "type": "string", - "enum": [ - "nospace", - "onespace", - "space" - ] - }, - "index-signature": { - "type": "string", - "enum": [ - "nospace", - "onespace", - "space" - ] - }, - "parameter": { - "type": "string", - "enum": [ - "nospace", - "onespace", - "space" - ] - }, - "property-declaration": { - "type": "string", - "enum": [ - "nospace", - "onespace", - "space" - ] - }, - "variable-declaration": { - "type": "string", - "enum": [ - "nospace", - "onespace", - "space" - ] - } - }, - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "call-signature": { - "type": "string", - "enum": [ - "nospace", - "onespace", - "space" - ] - }, - "index-signature": { - "type": "string", - "enum": [ - "nospace", - "onespace", - "space" - ] - }, - "parameter": { - "type": "string", - "enum": [ - "nospace", - "onespace", - "space" - ] - }, - "property-declaration": { - "type": "string", - "enum": [ - "nospace", - "onespace", - "space" - ] - }, - "variable-declaration": { - "type": "string", - "enum": [ - "nospace", - "onespace", - "space" - ] - } - }, - "additionalProperties": false - } - ], - "additionalItems": false - }, - "optionExamples": [ - "\n[\n true,\n {\n \"call-signature\": \"nospace\",\n \"index-signature\": \"nospace\",\n \"parameter\": \"nospace\",\n \"property-declaration\": \"nospace\",\n \"variable-declaration\": \"nospace\"\n },\n {\n \"call-signature\": \"onespace\",\n \"index-signature\": \"onespace\",\n \"parameter\": \"onespace\",\n \"property-declaration\": \"onespace\",\n \"variable-declaration\": \"onespace\"\n }\n]" - ], - "type": "typescript", - "typescriptOnly": true - }, - { - "ruleName": "typeof-compare", - "description": "Makes sure result of `typeof` is compared to correct string values", - "optionsDescription": "Not configurable.", - "options": null, - "optionExamples": [ - "true" - ], - "type": "functionality", - "typescriptOnly": false - }, - { - "ruleName": "unified-signatures", - "description": "Warns for any two overloads that could be unified into one by using a union or an optional/rest parameter.", - "optionsDescription": "Not configurable.", - "options": null, - "optionExamples": [ - "true" - ], - "type": "typescript", - "typescriptOnly": true - }, - { - "ruleName": "use-isnan", - "description": "Enforces use of the `isNaN()` function to check for NaN references instead of a comparison to the `NaN` constant.", - "rationale": "\nSince `NaN !== NaN`, comparisons with regular operators will produce unexpected results.\nSo, instead of `if (myVar === NaN)`, do `if (isNaN(myVar))`.", - "optionsDescription": "Not configurable.", - "options": null, - "optionExamples": [ - "true" - ], - "type": "functionality", - "typescriptOnly": false - }, - { - "ruleName": "variable-name", - "description": "Checks variable names for various errors.", - "optionsDescription": "\nFive arguments may be optionally provided:\n\n* `\"check-format\"`: allows only camelCased or UPPER_CASED variable names\n * `\"allow-leading-underscore\"` allows underscores at the beginning (only has an effect if \"check-format\" specified)\n * `\"allow-trailing-underscore\"` allows underscores at the end. (only has an effect if \"check-format\" specified)\n * `\"allow-pascal-case\"` allows PascalCase in addtion to camelCase.\n* `\"ban-keywords\"`: disallows the use of certain TypeScript keywords (`any`, `Number`, `number`, `String`,\n`string`, `Boolean`, `boolean`, `undefined`) as variable or parameter names.", - "options": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "check-format", - "allow-leading-underscore", - "allow-trailing-underscore", - "allow-pascal-case", - "ban-keywords" - ] - }, - "minLength": 0, - "maxLength": 5 - }, - "optionExamples": [ - "[true, \"ban-keywords\", \"check-format\", \"allow-leading-underscore\"]" - ], - "type": "style", - "typescriptOnly": false - }, - { - "ruleName": "whitespace", - "description": "Enforces whitespace style conventions.", - "rationale": "Helps maintain a readable, consistent style in your codebase.", - "optionsDescription": "\nEight arguments may be optionally provided:\n\n* `\"check-branch\"` checks branching statements (`if`/`else`/`for`/`while`) are followed by whitespace.\n* `\"check-decl\"`checks that variable declarations have whitespace around the equals token.\n* `\"check-operator\"` checks for whitespace around operator tokens.\n* `\"check-module\"` checks for whitespace in import & export statements.\n* `\"check-separator\"` checks for whitespace after separator tokens (`,`/`;`).\n* `\"check-type\"` checks for whitespace before a variable type specification.\n* `\"check-typecast\"` checks for whitespace between a typecast and its target.\n* `\"check-preblock\"` checks for whitespace before the opening brace of a block", - "options": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "check-branch", - "check-decl", - "check-operator", - "check-module", - "check-separator", - "check-type", - "check-typecast", - "check-preblock" - ] - }, - "minLength": 0, - "maxLength": 7 - }, - "optionExamples": [ - "[true, \"check-branch\", \"check-operator\", \"check-typecast\"]" - ], - "type": "style", - "typescriptOnly": false - } -] \ No newline at end of file From 3664812e38f542e9cae5a2d6698b3d09dc9b8da3 Mon Sep 17 00:00:00 2001 From: Noah Chen Date: Wed, 18 Jan 2017 15:28:08 -0500 Subject: [PATCH 034/131] Ignore .html files in docs/rules (#2071) --- .gitignore | 1 + .../adjacent-overload-signatures/index.html | 14 -- docs/rules/align/index.html | 42 ----- docs/rules/array-type/index.html | 35 ---- docs/rules/arrow-parens/index.html | 28 --- docs/rules/arrow-return-shorthand/index.html | 26 --- docs/rules/ban/index.html | 36 ---- docs/rules/callable-types/index.html | 12 -- docs/rules/class-name/index.html | 14 -- docs/rules/comment-format/index.html | 83 --------- docs/rules/completed-docs/index.html | 38 ----- docs/rules/curly/index.html | 24 --- docs/rules/cyclomatic-complexity/index.html | 39 ----- docs/rules/eofline/index.html | 14 -- docs/rules/file-header/index.html | 17 -- docs/rules/forin/index.html | 25 --- docs/rules/import-blacklist/index.html | 32 ---- docs/rules/import-spacing/index.html | 13 -- docs/rules/indent/index.html | 33 ---- docs/rules/index.html | 22 --- docs/rules/interface-name/index.html | 31 ---- .../interface-over-type-literal/index.html | 12 -- docs/rules/jsdoc-format/index.html | 22 --- docs/rules/label-position/index.html | 19 --- docs/rules/linebreak-style/index.html | 30 ---- docs/rules/max-classes-per-file/index.html | 40 ----- docs/rules/max-file-line-count/index.html | 23 --- docs/rules/max-line-length/index.html | 24 --- docs/rules/member-access/index.html | 40 ----- docs/rules/member-ordering/index.html | 107 ------------ docs/rules/new-parens/index.html | 14 -- .../index.html | 19 --- docs/rules/no-any/index.html | 14 -- docs/rules/no-arg/index.html | 18 -- docs/rules/no-bitwise/index.html | 24 --- docs/rules/no-boolean-compare/index.html | 14 -- .../no-boolean-literal-compare/index.html | 15 -- .../no-conditional-assignment/index.html | 19 --- .../no-consecutive-blank-lines/index.html | 24 --- docs/rules/no-console/index.html | 23 --- docs/rules/no-construct/index.html | 19 --- docs/rules/no-debugger/index.html | 14 -- docs/rules/no-default-export/index.html | 19 --- docs/rules/no-duplicate-variable/index.html | 21 --- docs/rules/no-empty-interface/index.html | 12 -- docs/rules/no-empty/index.html | 15 -- docs/rules/no-eval/index.html | 18 -- docs/rules/no-for-in-array/index.html | 28 --- docs/rules/no-inferrable-types/index.html | 38 ----- .../no-inferred-empty-object-type/index.html | 14 -- .../rules/no-interface-constructor/index.html | 14 -- docs/rules/no-internal-module/index.html | 14 -- docs/rules/no-invalid-this/index.html | 37 ---- docs/rules/no-magic-numbers/index.html | 32 ---- docs/rules/no-mergeable-namespace/index.html | 13 -- docs/rules/no-misused-new/index.html | 13 -- docs/rules/no-namespace/index.html | 41 ----- docs/rules/no-null-keyword/index.html | 17 -- docs/rules/no-parameter-properties/index.html | 17 -- docs/rules/no-reference/index.html | 17 -- docs/rules/no-require-imports/index.html | 14 -- docs/rules/no-shadowed-variable/index.html | 14 -- docs/rules/no-string-literal/index.html | 14 -- docs/rules/no-string-throw/index.html | 12 -- .../no-switch-case-fall-through/index.html | 40 ----- docs/rules/no-trailing-whitespace/index.html | 14 -- .../rules/no-unnecessary-qualifier/index.html | 15 -- docs/rules/no-unreachable/index.html | 13 -- docs/rules/no-unsafe-finally/index.html | 23 --- docs/rules/no-unused-expression/index.html | 44 ----- docs/rules/no-unused-new/index.html | 20 --- docs/rules/no-unused-variable/index.html | 67 -------- docs/rules/no-use-before-declare/index.html | 17 -- docs/rules/no-var-keyword/index.html | 15 -- docs/rules/no-var-requires/index.html | 17 -- docs/rules/no-void-expression/index.html | 12 -- .../object-literal-key-quotes/index.html | 59 ------- .../rules/object-literal-shorthand/index.html | 13 -- .../rules/object-literal-sort-keys/index.html | 14 -- docs/rules/one-line/index.html | 47 ----- .../one-variable-per-declaration/index.html | 36 ---- docs/rules/only-arrow-functions/index.html | 41 ----- docs/rules/ordered-imports/index.html | 89 ---------- .../prefer-arrow-shorthand-return/index.html | 14 -- docs/rules/prefer-const/index.html | 17 -- docs/rules/prefer-for-of/index.html | 14 -- docs/rules/prefer-method-signature/index.html | 14 -- docs/rules/promise-function-async/index.html | 21 --- docs/rules/quotemark/index.html | 50 ------ docs/rules/radix/index.html | 18 -- docs/rules/restrict-plus-operands/index.html | 14 -- docs/rules/semicolon/index.html | 56 ------ .../space-before-function-paren/index.html | 78 --------- .../strict-boolean-expressions/index.html | 16 -- docs/rules/switch-default/index.html | 13 -- docs/rules/trailing-comma/index.html | 61 ------- docs/rules/triple-equals/index.html | 40 ----- docs/rules/typedef-whitespace/index.html | 160 ------------------ docs/rules/typedef/index.html | 53 ------ docs/rules/typeof-compare/index.html | 13 -- docs/rules/unified-signatures/index.html | 13 -- docs/rules/use-isnan/index.html | 17 -- docs/rules/variable-name/index.html | 48 ------ docs/rules/whitespace/index.html | 57 ------- 104 files changed, 1 insertion(+), 2929 deletions(-) delete mode 100644 docs/rules/adjacent-overload-signatures/index.html delete mode 100644 docs/rules/align/index.html delete mode 100644 docs/rules/array-type/index.html delete mode 100644 docs/rules/arrow-parens/index.html delete mode 100644 docs/rules/arrow-return-shorthand/index.html delete mode 100644 docs/rules/ban/index.html delete mode 100644 docs/rules/callable-types/index.html delete mode 100644 docs/rules/class-name/index.html delete mode 100644 docs/rules/comment-format/index.html delete mode 100644 docs/rules/completed-docs/index.html delete mode 100644 docs/rules/curly/index.html delete mode 100644 docs/rules/cyclomatic-complexity/index.html delete mode 100644 docs/rules/eofline/index.html delete mode 100644 docs/rules/file-header/index.html delete mode 100644 docs/rules/forin/index.html delete mode 100644 docs/rules/import-blacklist/index.html delete mode 100644 docs/rules/import-spacing/index.html delete mode 100644 docs/rules/indent/index.html delete mode 100644 docs/rules/index.html delete mode 100644 docs/rules/interface-name/index.html delete mode 100644 docs/rules/interface-over-type-literal/index.html delete mode 100644 docs/rules/jsdoc-format/index.html delete mode 100644 docs/rules/label-position/index.html delete mode 100644 docs/rules/linebreak-style/index.html delete mode 100644 docs/rules/max-classes-per-file/index.html delete mode 100644 docs/rules/max-file-line-count/index.html delete mode 100644 docs/rules/max-line-length/index.html delete mode 100644 docs/rules/member-access/index.html delete mode 100644 docs/rules/member-ordering/index.html delete mode 100644 docs/rules/new-parens/index.html delete mode 100644 docs/rules/no-angle-bracket-type-assertion/index.html delete mode 100644 docs/rules/no-any/index.html delete mode 100644 docs/rules/no-arg/index.html delete mode 100644 docs/rules/no-bitwise/index.html delete mode 100644 docs/rules/no-boolean-compare/index.html delete mode 100644 docs/rules/no-boolean-literal-compare/index.html delete mode 100644 docs/rules/no-conditional-assignment/index.html delete mode 100644 docs/rules/no-consecutive-blank-lines/index.html delete mode 100644 docs/rules/no-console/index.html delete mode 100644 docs/rules/no-construct/index.html delete mode 100644 docs/rules/no-debugger/index.html delete mode 100644 docs/rules/no-default-export/index.html delete mode 100644 docs/rules/no-duplicate-variable/index.html delete mode 100644 docs/rules/no-empty-interface/index.html delete mode 100644 docs/rules/no-empty/index.html delete mode 100644 docs/rules/no-eval/index.html delete mode 100644 docs/rules/no-for-in-array/index.html delete mode 100644 docs/rules/no-inferrable-types/index.html delete mode 100644 docs/rules/no-inferred-empty-object-type/index.html delete mode 100644 docs/rules/no-interface-constructor/index.html delete mode 100644 docs/rules/no-internal-module/index.html delete mode 100644 docs/rules/no-invalid-this/index.html delete mode 100644 docs/rules/no-magic-numbers/index.html delete mode 100644 docs/rules/no-mergeable-namespace/index.html delete mode 100644 docs/rules/no-misused-new/index.html delete mode 100644 docs/rules/no-namespace/index.html delete mode 100644 docs/rules/no-null-keyword/index.html delete mode 100644 docs/rules/no-parameter-properties/index.html delete mode 100644 docs/rules/no-reference/index.html delete mode 100644 docs/rules/no-require-imports/index.html delete mode 100644 docs/rules/no-shadowed-variable/index.html delete mode 100644 docs/rules/no-string-literal/index.html delete mode 100644 docs/rules/no-string-throw/index.html delete mode 100644 docs/rules/no-switch-case-fall-through/index.html delete mode 100644 docs/rules/no-trailing-whitespace/index.html delete mode 100644 docs/rules/no-unnecessary-qualifier/index.html delete mode 100644 docs/rules/no-unreachable/index.html delete mode 100644 docs/rules/no-unsafe-finally/index.html delete mode 100644 docs/rules/no-unused-expression/index.html delete mode 100644 docs/rules/no-unused-new/index.html delete mode 100644 docs/rules/no-unused-variable/index.html delete mode 100644 docs/rules/no-use-before-declare/index.html delete mode 100644 docs/rules/no-var-keyword/index.html delete mode 100644 docs/rules/no-var-requires/index.html delete mode 100644 docs/rules/no-void-expression/index.html delete mode 100644 docs/rules/object-literal-key-quotes/index.html delete mode 100644 docs/rules/object-literal-shorthand/index.html delete mode 100644 docs/rules/object-literal-sort-keys/index.html delete mode 100644 docs/rules/one-line/index.html delete mode 100644 docs/rules/one-variable-per-declaration/index.html delete mode 100644 docs/rules/only-arrow-functions/index.html delete mode 100644 docs/rules/ordered-imports/index.html delete mode 100644 docs/rules/prefer-arrow-shorthand-return/index.html delete mode 100644 docs/rules/prefer-const/index.html delete mode 100644 docs/rules/prefer-for-of/index.html delete mode 100644 docs/rules/prefer-method-signature/index.html delete mode 100644 docs/rules/promise-function-async/index.html delete mode 100644 docs/rules/quotemark/index.html delete mode 100644 docs/rules/radix/index.html delete mode 100644 docs/rules/restrict-plus-operands/index.html delete mode 100644 docs/rules/semicolon/index.html delete mode 100644 docs/rules/space-before-function-paren/index.html delete mode 100644 docs/rules/strict-boolean-expressions/index.html delete mode 100644 docs/rules/switch-default/index.html delete mode 100644 docs/rules/trailing-comma/index.html delete mode 100644 docs/rules/triple-equals/index.html delete mode 100644 docs/rules/typedef-whitespace/index.html delete mode 100644 docs/rules/typedef/index.html delete mode 100644 docs/rules/typeof-compare/index.html delete mode 100644 docs/rules/unified-signatures/index.html delete mode 100644 docs/rules/use-isnan/index.html delete mode 100644 docs/rules/variable-name/index.html delete mode 100644 docs/rules/whitespace/index.html diff --git a/.gitignore b/.gitignore index 66385e0bbbb..ec83b7c5002 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ typings/.basedir.ts *.swp docs/_data/rules.json +docs/rules/ diff --git a/docs/rules/adjacent-overload-signatures/index.html b/docs/rules/adjacent-overload-signatures/index.html deleted file mode 100644 index 3506700140c..00000000000 --- a/docs/rules/adjacent-overload-signatures/index.html +++ /dev/null @@ -1,14 +0,0 @@ ---- -ruleName: adjacent-overload-signatures -description: Enforces function overloads to be consecutive. -optionsDescription: Not configurable. -options: null -optionExamples: - - 'true' -rationale: Improves readability and organization by grouping naturally related items together. -type: typescript -typescriptOnly: true -layout: rule -title: 'Rule: adjacent-overload-signatures' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/align/index.html b/docs/rules/align/index.html deleted file mode 100644 index f42a1421e39..00000000000 --- a/docs/rules/align/index.html +++ /dev/null @@ -1,42 +0,0 @@ ---- -ruleName: align -description: Enforces vertical alignment. -rationale: 'Helps maintain a readable, consistent style in your codebase.' -optionsDescription: |- - - Three arguments may be optionally provided: - - * `"parameters"` checks alignment of function parameters. - * `"arguments"` checks alignment of function call arguments. - * `"statements"` checks alignment of statements. -options: - type: array - items: - type: string - enum: - - arguments - - parameters - - statements - minLength: 1 - maxLength: 3 -optionExamples: - - '[true, "parameters", "statements"]' -type: style -typescriptOnly: false -layout: rule -title: 'Rule: align' -optionsJSON: |- - { - "type": "array", - "items": { - "type": "string", - "enum": [ - "arguments", - "parameters", - "statements" - ] - }, - "minLength": 1, - "maxLength": 3 - } ---- \ No newline at end of file diff --git a/docs/rules/array-type/index.html b/docs/rules/array-type/index.html deleted file mode 100644 index 029dfaa19a4..00000000000 --- a/docs/rules/array-type/index.html +++ /dev/null @@ -1,35 +0,0 @@ ---- -ruleName: array-type -description: 'Requires using either ''T[]'' or ''Array'' for arrays.' -hasFix: true -optionsDescription: |- - - One of the following arguments must be provided: - - * `"array"` enforces use of `T[]` for all types T. - * `"generic"` enforces use of `Array` for all types T. - * `"array-simple"` enforces use of `T[]` if `T` is a simple type (primitive or type reference). -options: - type: string - enum: - - array - - generic - - array-simple -optionExamples: - - '[true, "array"]' - - '[true, "generic"]' - - '[true, "array-simple"]' -type: style -typescriptOnly: true -layout: rule -title: 'Rule: array-type' -optionsJSON: |- - { - "type": "string", - "enum": [ - "array", - "generic", - "array-simple" - ] - } ---- \ No newline at end of file diff --git a/docs/rules/arrow-parens/index.html b/docs/rules/arrow-parens/index.html deleted file mode 100644 index d6bec9d8a20..00000000000 --- a/docs/rules/arrow-parens/index.html +++ /dev/null @@ -1,28 +0,0 @@ ---- -ruleName: arrow-parens -description: Requires parentheses around the parameters of arrow function definitions. -hasFix: true -rationale: Maintains stylistic consistency with other arrow function definitions. -optionsDescription: |- - - If `ban-single-arg-parens` is specified, then arrow functions with one parameter - must not have parentheses if removing them is allowed by TypeScript. -options: - type: string - enum: - - ban-single-arg-parens -optionExamples: - - 'true' - - '[true, "ban-single-arg-parens"]' -type: style -typescriptOnly: false -layout: rule -title: 'Rule: arrow-parens' -optionsJSON: |- - { - "type": "string", - "enum": [ - "ban-single-arg-parens" - ] - } ---- \ No newline at end of file diff --git a/docs/rules/arrow-return-shorthand/index.html b/docs/rules/arrow-return-shorthand/index.html deleted file mode 100644 index 69ca43199ca..00000000000 --- a/docs/rules/arrow-return-shorthand/index.html +++ /dev/null @@ -1,26 +0,0 @@ ---- -ruleName: arrow-return-shorthand -description: 'Suggests to convert `() => { return x; }` to `() => x`.' -hasFix: true -optionsDescription: |- - - If `multiline` is specified, then this will warn even if the function spans multiple lines. -options: - type: string - enum: - - multiline -optionExamples: - - '[true]' - - '[true, "multiline"]' -type: style -typescriptOnly: false -layout: rule -title: 'Rule: arrow-return-shorthand' -optionsJSON: |- - { - "type": "string", - "enum": [ - "multiline" - ] - } ---- \ No newline at end of file diff --git a/docs/rules/ban/index.html b/docs/rules/ban/index.html deleted file mode 100644 index 8d301eea4dc..00000000000 --- a/docs/rules/ban/index.html +++ /dev/null @@ -1,36 +0,0 @@ ---- -ruleName: ban -description: Bans the use of specific functions or global methods. -optionsDescription: |- - - A list of `['object', 'method', 'optional explanation here']` or `['globalMethod']` which ban `object.method()` - or respectively `globalMethod()`. -options: - type: list - listType: - type: array - items: - type: string - minLength: 1 - maxLength: 3 -optionExamples: - - |- - [true, ["someGlobalMethod"], ["someObject", "someFunction"], - ["someObject", "otherFunction", "Optional explanation"]] -type: functionality -typescriptOnly: false -layout: rule -title: 'Rule: ban' -optionsJSON: |- - { - "type": "list", - "listType": { - "type": "array", - "items": { - "type": "string" - }, - "minLength": 1, - "maxLength": 3 - } - } ---- \ No newline at end of file diff --git a/docs/rules/callable-types/index.html b/docs/rules/callable-types/index.html deleted file mode 100644 index 4c65f626683..00000000000 --- a/docs/rules/callable-types/index.html +++ /dev/null @@ -1,12 +0,0 @@ ---- -ruleName: callable-types -description: An interface or literal type with just a call signature can be written as a function type. -rationale: style -optionsDescription: Not configurable. -options: null -type: style -typescriptOnly: true -layout: rule -title: 'Rule: callable-types' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/class-name/index.html b/docs/rules/class-name/index.html deleted file mode 100644 index 0ae19d6e8be..00000000000 --- a/docs/rules/class-name/index.html +++ /dev/null @@ -1,14 +0,0 @@ ---- -ruleName: class-name -description: Enforces PascalCased class and interface names. -rationale: Makes it easy to differentitate classes from regular variables at a glance. -optionsDescription: Not configurable. -options: null -optionExamples: - - 'true' -type: style -typescriptOnly: false -layout: rule -title: 'Rule: class-name' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/comment-format/index.html b/docs/rules/comment-format/index.html deleted file mode 100644 index 65450916bb9..00000000000 --- a/docs/rules/comment-format/index.html +++ /dev/null @@ -1,83 +0,0 @@ ---- -ruleName: comment-format -description: Enforces formatting rules for single-line comments. -rationale: 'Helps maintain a consistent, readable style in your codebase.' -optionsDescription: | - - Three arguments may be optionally provided: - - * `"check-space"` requires that all single-line comments must begin with a space, as in `// comment` - * note that comments starting with `///` are also allowed, for things such as `///` - * `"check-lowercase"` requires that the first non-whitespace character of a comment must be lowercase, if applicable. - * `"check-uppercase"` requires that the first non-whitespace character of a comment must be uppercase, if applicable. - - Exceptions to `"check-lowercase"` or `"check-uppercase"` can be managed with object that may be passed as last argument. - - One of two options can be provided in this object: - - * `"ignoreWords"` - array of strings - words that will be ignored at the beginning of the comment. - * `"ignorePattern"` - string - RegExp pattern that will be ignored at the beginning of the comment. -options: - type: array - items: - anyOf: - - type: string - enum: - - check-space - - check-lowercase - - check-uppercase - - type: object - properties: - ignoreWords: - type: array - items: - type: string - ignorePattern: - type: string - minProperties: 1 - maxProperties: 1 - minLength: 1 - maxLength: 4 -optionExamples: - - '[true, "check-space", "check-uppercase"]' - - '[true, "check-lowercase", {"ignoreWords": ["TODO", "HACK"]}]' - - '[true, "check-lowercase", {"ignorePattern": "STD\w{2,3}\b"}]' -type: style -typescriptOnly: false -layout: rule -title: 'Rule: comment-format' -optionsJSON: |- - { - "type": "array", - "items": { - "anyOf": [ - { - "type": "string", - "enum": [ - "check-space", - "check-lowercase", - "check-uppercase" - ] - }, - { - "type": "object", - "properties": { - "ignoreWords": { - "type": "array", - "items": { - "type": "string" - } - }, - "ignorePattern": { - "type": "string" - } - }, - "minProperties": 1, - "maxProperties": 1 - } - ] - }, - "minLength": 1, - "maxLength": 4 - } ---- \ No newline at end of file diff --git a/docs/rules/completed-docs/index.html b/docs/rules/completed-docs/index.html deleted file mode 100644 index 0ea545d71e5..00000000000 --- a/docs/rules/completed-docs/index.html +++ /dev/null @@ -1,38 +0,0 @@ ---- -ruleName: completed-docs -description: Enforces documentation for important items be filled out. -optionsDescription: |- - - Either `true` to enable for all, or any of - `["classes", "functions", "methods", "properties"] - to choose individual ones.` -options: - type: array - items: - type: string - enum: - - classes - - functions - - methods - - properties -optionExamples: - - 'true' - - '[true, "classes", "functions"]' -type: style -typescriptOnly: false -layout: rule -title: 'Rule: completed-docs' -optionsJSON: |- - { - "type": "array", - "items": { - "type": "string", - "enum": [ - "classes", - "functions", - "methods", - "properties" - ] - } - } ---- \ No newline at end of file diff --git a/docs/rules/curly/index.html b/docs/rules/curly/index.html deleted file mode 100644 index 5e9075da7fe..00000000000 --- a/docs/rules/curly/index.html +++ /dev/null @@ -1,24 +0,0 @@ ---- -ruleName: curly -description: Enforces braces for `if`/`for`/`do`/`while` statements. -rationale: |- - - ```ts - if (foo === bar) - foo++; - bar++; - ``` - - In the code above, the author almost certainly meant for both `foo++` and `bar++` - to be executed only if `foo === bar`. However, he forgot braces and `bar++` will be executed - no matter what. This rule could prevent such a mistake. -optionsDescription: Not configurable. -options: null -optionExamples: - - 'true' -type: functionality -typescriptOnly: false -layout: rule -title: 'Rule: curly' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/cyclomatic-complexity/index.html b/docs/rules/cyclomatic-complexity/index.html deleted file mode 100644 index 12ba0162133..00000000000 --- a/docs/rules/cyclomatic-complexity/index.html +++ /dev/null @@ -1,39 +0,0 @@ ---- -ruleName: cyclomatic-complexity -description: Enforces a threshold of cyclomatic complexity. -descriptionDetails: |- - - Cyclomatic complexity is assessed for each function of any type. A starting value of 20 - is assigned and this value is then incremented for every statement which can branch the - control flow within the function. The following statements and expressions contribute - to cyclomatic complexity: - * `catch` - * `if` and `? :` - * `||` and `&&` due to short-circuit evaluation - * `for`, `for in` and `for of` loops - * `while` and `do while` loops -rationale: |- - - Cyclomatic complexity is a code metric which indicates the level of complexity in a - function. High cyclomatic complexity indicates confusing code which may be prone to - errors or difficult to modify. -optionsDescription: |- - - An optional upper limit for cyclomatic complexity can be specified. If no limit option - is provided a default value of $(Rule.DEFAULT_THRESHOLD) will be used. -options: - type: number - minimum: $(Rule.MINIMUM_THRESHOLD) -optionExamples: - - 'true' - - '[true, 20]' -type: maintainability -typescriptOnly: false -layout: rule -title: 'Rule: cyclomatic-complexity' -optionsJSON: |- - { - "type": "number", - "minimum": "$(Rule.MINIMUM_THRESHOLD)" - } ---- \ No newline at end of file diff --git a/docs/rules/eofline/index.html b/docs/rules/eofline/index.html deleted file mode 100644 index 985298840d8..00000000000 --- a/docs/rules/eofline/index.html +++ /dev/null @@ -1,14 +0,0 @@ ---- -ruleName: eofline -description: Ensures the file ends with a newline. -rationale: 'It is a [standard convention](http://stackoverflow.com/q/729692/3124288) to end files with a newline.' -optionsDescription: Not configurable. -options: null -optionExamples: - - 'true' -type: maintainability -typescriptOnly: false -layout: rule -title: 'Rule: eofline' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/file-header/index.html b/docs/rules/file-header/index.html deleted file mode 100644 index 7cc981b6bb8..00000000000 --- a/docs/rules/file-header/index.html +++ /dev/null @@ -1,17 +0,0 @@ ---- -ruleName: file-header -description: 'Enforces a certain header comment for all files, matched by a regular expression.' -optionsDescription: Regular expression to match the header. -options: - type: string -optionExamples: - - '[true, "Copyright \\d{4}"]' -type: style -typescriptOnly: false -layout: rule -title: 'Rule: file-header' -optionsJSON: |- - { - "type": "string" - } ---- \ No newline at end of file diff --git a/docs/rules/forin/index.html b/docs/rules/forin/index.html deleted file mode 100644 index e41a69c1068..00000000000 --- a/docs/rules/forin/index.html +++ /dev/null @@ -1,25 +0,0 @@ ---- -ruleName: forin -description: Requires a `for ... in` statement to be filtered with an `if` statement. -rationale: |- - - ```ts - for (let key in someObject) { - if (someObject.hasOwnProperty(key)) { - // code here - } - } - ``` - Prevents accidental interation over properties inherited from an object's prototype. - See [MDN's `for...in`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...in) - documentation for more information about `for...in` loops. -optionsDescription: Not configurable. -options: null -optionExamples: - - 'true' -type: functionality -typescriptOnly: false -layout: rule -title: 'Rule: forin' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/import-blacklist/index.html b/docs/rules/import-blacklist/index.html deleted file mode 100644 index 41ec9639819..00000000000 --- a/docs/rules/import-blacklist/index.html +++ /dev/null @@ -1,32 +0,0 @@ ---- -ruleName: import-blacklist -description: |- - - Disallows importing the specified modules directly via `import` and `require`. - Instead only sub modules may be imported from that module. -rationale: |- - - Some libraries allow importing their submodules instead of the entire module. - This is good practise as it avoids loading unused modules. -optionsDescription: A list of blacklisted modules. -options: - type: array - items: - type: string - minLength: 1 -optionExamples: - - 'true' - - '[true, "rxjs", "lodash"]' -type: functionality -typescriptOnly: false -layout: rule -title: 'Rule: import-blacklist' -optionsJSON: |- - { - "type": "array", - "items": { - "type": "string" - }, - "minLength": 1 - } ---- \ No newline at end of file diff --git a/docs/rules/import-spacing/index.html b/docs/rules/import-spacing/index.html deleted file mode 100644 index a95c212b9c2..00000000000 --- a/docs/rules/import-spacing/index.html +++ /dev/null @@ -1,13 +0,0 @@ ---- -ruleName: import-spacing -description: Ensures proper spacing between import statement keywords -optionsDescription: Not configurable. -options: null -optionExamples: - - 'true' -type: style -typescriptOnly: false -layout: rule -title: 'Rule: import-spacing' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/indent/index.html b/docs/rules/indent/index.html deleted file mode 100644 index 28d55c314b5..00000000000 --- a/docs/rules/indent/index.html +++ /dev/null @@ -1,33 +0,0 @@ ---- -ruleName: indent -description: Enforces indentation with tabs or spaces. -rationale: |- - - Using only one of tabs or spaces for indentation leads to more consistent editor behavior, - cleaner diffs in version control, and easier programatic manipulation. -optionsDescription: |- - - One of the following arguments must be provided: - - * `"spaces"` enforces consistent spaces. - * `"tabs"` enforces consistent tabs. -options: - type: string - enum: - - tabs - - spaces -optionExamples: - - '[true, "spaces"]' -type: maintainability -typescriptOnly: false -layout: rule -title: 'Rule: indent' -optionsJSON: |- - { - "type": "string", - "enum": [ - "tabs", - "spaces" - ] - } ---- \ No newline at end of file diff --git a/docs/rules/index.html b/docs/rules/index.html deleted file mode 100644 index 537271b7ba2..00000000000 --- a/docs/rules/index.html +++ /dev/null @@ -1,22 +0,0 @@ ---- -layout: page -title: Rules -permalink: /rules/ -menu: main -order: 2 ---- -

TypeScript Specific

-

These rules find errors related to TypeScript features:

-{% include rule_list.html ruleType="typescript" %} - -

Functionality

-

These rules catch common errors in JS programming or otherwise confusing constructs that are prone to producing bugs:

-{% include rule_list.html ruleType="functionality" %} - -

Maintainability

-

These rules make code maintenance easier:

-{% include rule_list.html ruleType="maintainability" %} - -

Style

-

These rules enforce consistent style across your codebase:

-{% include rule_list.html ruleType="style" %} diff --git a/docs/rules/interface-name/index.html b/docs/rules/interface-name/index.html deleted file mode 100644 index 91375f2a0ec..00000000000 --- a/docs/rules/interface-name/index.html +++ /dev/null @@ -1,31 +0,0 @@ ---- -ruleName: interface-name -description: Requires interface names to begin with a capital 'I' -rationale: Makes it easy to differentitate interfaces from regular classes at a glance. -optionsDescription: |- - - One of the following two options must be provided: - - * `"always-prefix"` requires interface names to start with an "I" - * `"never-prefix"` requires interface names to not have an "I" prefix -options: - type: string - enum: - - always-prefix - - never-prefix -optionExamples: - - '[true, "always-prefix"]' - - '[true, "never-prefix"]' -type: style -typescriptOnly: true -layout: rule -title: 'Rule: interface-name' -optionsJSON: |- - { - "type": "string", - "enum": [ - "always-prefix", - "never-prefix" - ] - } ---- \ No newline at end of file diff --git a/docs/rules/interface-over-type-literal/index.html b/docs/rules/interface-over-type-literal/index.html deleted file mode 100644 index 3f89916e141..00000000000 --- a/docs/rules/interface-over-type-literal/index.html +++ /dev/null @@ -1,12 +0,0 @@ ---- -ruleName: interface-over-type-literal -description: 'Prefer an interface declaration over a type literal (`type T = { ... }`)' -rationale: style -optionsDescription: Not configurable. -options: null -type: style -typescriptOnly: true -layout: rule -title: 'Rule: interface-over-type-literal' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/jsdoc-format/index.html b/docs/rules/jsdoc-format/index.html deleted file mode 100644 index 269ceab1b0a..00000000000 --- a/docs/rules/jsdoc-format/index.html +++ /dev/null @@ -1,22 +0,0 @@ ---- -ruleName: jsdoc-format -description: Enforces basic format rules for JSDoc comments. -descriptionDetails: |- - - The following rules are enforced for JSDoc comments (comments starting with `/**`): - - * each line contains an asterisk and asterisks must be aligned - * each asterisk must be followed by either a space or a newline (except for the first and the last) - * the only characters before the asterisk on each line must be whitespace characters - * one line comments must start with `/** ` and end with `*/` -rationale: 'Helps maintain a consistent, readable style for JSDoc comments.' -optionsDescription: Not configurable. -options: null -optionExamples: - - 'true' -type: style -typescriptOnly: false -layout: rule -title: 'Rule: jsdoc-format' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/label-position/index.html b/docs/rules/label-position/index.html deleted file mode 100644 index d82ae64eab8..00000000000 --- a/docs/rules/label-position/index.html +++ /dev/null @@ -1,19 +0,0 @@ ---- -ruleName: label-position -description: Only allows labels in sensible locations. -descriptionDetails: This rule only allows labels to be on `do/for/while/switch` statements. -rationale: |- - - Labels in JavaScript only can be used in conjunction with `break` or `continue`, - constructs meant to be used for loop flow control. While you can theoretically use - labels on any block statement in JS, it is considered poor code structure to do so. -optionsDescription: Not configurable. -options: null -optionExamples: - - 'true' -type: functionality -typescriptOnly: false -layout: rule -title: 'Rule: label-position' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/linebreak-style/index.html b/docs/rules/linebreak-style/index.html deleted file mode 100644 index 363c990858e..00000000000 --- a/docs/rules/linebreak-style/index.html +++ /dev/null @@ -1,30 +0,0 @@ ---- -ruleName: linebreak-style -description: Enforces a consistent linebreak style. -optionsDescription: |- - - One of the following options must be provided: - - * `"LF"` requires LF (`\n`) linebreaks - * `"CRLF"` requires CRLF (`\r\n`) linebreaks -options: - type: string - enum: - - LF - - CRLF -optionExamples: - - '[true, "LF"]' - - '[true, "CRLF"]' -type: maintainability -typescriptOnly: false -layout: rule -title: 'Rule: linebreak-style' -optionsJSON: |- - { - "type": "string", - "enum": [ - "LF", - "CRLF" - ] - } ---- \ No newline at end of file diff --git a/docs/rules/max-classes-per-file/index.html b/docs/rules/max-classes-per-file/index.html deleted file mode 100644 index 985d4d6824a..00000000000 --- a/docs/rules/max-classes-per-file/index.html +++ /dev/null @@ -1,40 +0,0 @@ ---- -ruleName: max-classes-per-file -description: |- - - A file may not contain more than the specified number of classes -rationale: |- - - Ensures that files have a single responsibility so that that classes each exist in their own files -optionsDescription: |- - - The one required argument is an integer indicating the maximum number of classes that can appear in a file. -options: - type: array - items: - - type: number - minimum: 1 - additionalItems: false - minLength: 1 - maxLength: 2 -optionExamples: - - '[true, 1]' - - '[true, 5]' -type: maintainability -typescriptOnly: false -layout: rule -title: 'Rule: max-classes-per-file' -optionsJSON: |- - { - "type": "array", - "items": [ - { - "type": "number", - "minimum": 1 - } - ], - "additionalItems": false, - "minLength": 1, - "maxLength": 2 - } ---- \ No newline at end of file diff --git a/docs/rules/max-file-line-count/index.html b/docs/rules/max-file-line-count/index.html deleted file mode 100644 index 88556799827..00000000000 --- a/docs/rules/max-file-line-count/index.html +++ /dev/null @@ -1,23 +0,0 @@ ---- -ruleName: max-file-line-count -description: Requires files to remain under a certain number of lines -rationale: |- - - Limiting the number of lines allowed in a file allows files to remain small, - single purpose, and maintainable. -optionsDescription: An integer indicating the maximum number of lines. -options: - type: number - minimum: '1' -optionExamples: - - '[true, 300]' -type: maintainability -typescriptOnly: false -layout: rule -title: 'Rule: max-file-line-count' -optionsJSON: |- - { - "type": "number", - "minimum": "1" - } ---- \ No newline at end of file diff --git a/docs/rules/max-line-length/index.html b/docs/rules/max-line-length/index.html deleted file mode 100644 index fdd451fce6a..00000000000 --- a/docs/rules/max-line-length/index.html +++ /dev/null @@ -1,24 +0,0 @@ ---- -ruleName: max-line-length -description: Requires lines to be under a certain max length. -rationale: |- - - Limiting the length of a line of code improves code readability. - It also makes comparing code side-by-side easier and improves compatibility with - various editors, IDEs, and diff viewers. -optionsDescription: An integer indicating the max length of lines. -options: - type: number - minimum: '1' -optionExamples: - - '[true, 120]' -type: maintainability -typescriptOnly: false -layout: rule -title: 'Rule: max-line-length' -optionsJSON: |- - { - "type": "number", - "minimum": "1" - } ---- \ No newline at end of file diff --git a/docs/rules/member-access/index.html b/docs/rules/member-access/index.html deleted file mode 100644 index ae5c4991013..00000000000 --- a/docs/rules/member-access/index.html +++ /dev/null @@ -1,40 +0,0 @@ ---- -ruleName: member-access -description: Requires explicit visibility declarations for class members. -rationale: Explicit visibility declarations can make code more readable and accessible for those new to TS. -optionsDescription: |- - - Two arguments may be optionally provided: - - * `"check-accessor"` enforces explicit visibility on get/set accessors (can only be public) - * `"check-constructor"` enforces explicit visibility on constructors (can only be public) -options: - type: array - items: - type: string - enum: - - check-accessor - - check-constructor - minLength: 0 - maxLength: 2 -optionExamples: - - 'true' - - '[true, "check-accessor"]' -type: typescript -typescriptOnly: true -layout: rule -title: 'Rule: member-access' -optionsJSON: |- - { - "type": "array", - "items": { - "type": "string", - "enum": [ - "check-accessor", - "check-constructor" - ] - }, - "minLength": 0, - "maxLength": 2 - } ---- \ No newline at end of file diff --git a/docs/rules/member-ordering/index.html b/docs/rules/member-ordering/index.html deleted file mode 100644 index 295ced8cb52..00000000000 --- a/docs/rules/member-ordering/index.html +++ /dev/null @@ -1,107 +0,0 @@ ---- -ruleName: member-ordering -description: Enforces member ordering. -rationale: 'A consistent ordering for class members can make classes easier to read, navigate, and edit.' -optionsDescription: |- - - One argument, which is an object, must be provided. It should contain an `order` property. - The `order` property should have a value of one of the following strings: - - * `fields-first` - * `statics-first` - * `instance-sandwich` - - Alternatively, the value for `order` maybe be an array consisting of the following strings: - - * `public-static-field` - * `protected-static-field` - * `private-static-field` - * `public-instance-field` - * `protected-instance-field` - * `private-instance-field` - * `constructor` - * `public-static-method` - * `protected-static-method` - * `private-static-method` - * `public-instance-method` - * `protected-instance-method` - * `private-instance-method` - - This is useful if one of the preset orders does not meet your needs. -options: - type: object - properties: - order: - oneOf: - - type: string - enum: - - fields-first - - statics-first - - instance-sandwich - - type: array - items: - type: string - enum: - - public-static-field - - public-static-method - - protected-static-field - - protected-static-method - - private-static-field - - private-static-method - - public-instance-field - - protected-instance-field - - private-instance-field - - constructor - - public-instance-method - - protected-instance-method - - private-instance-method - maxLength: 13 - additionalProperties: false -optionExamples: - - '[true, { "order": "fields-first" }]' -type: typescript -typescriptOnly: true -layout: rule -title: 'Rule: member-ordering' -optionsJSON: |- - { - "type": "object", - "properties": { - "order": { - "oneOf": [ - { - "type": "string", - "enum": [ - "fields-first", - "statics-first", - "instance-sandwich" - ] - }, - { - "type": "array", - "items": { - "type": "string", - "enum": [ - "public-static-field", - "public-static-method", - "protected-static-field", - "protected-static-method", - "private-static-field", - "private-static-method", - "public-instance-field", - "protected-instance-field", - "private-instance-field", - "constructor", - "public-instance-method", - "protected-instance-method", - "private-instance-method" - ] - }, - "maxLength": 13 - } - ] - } - }, - "additionalProperties": false - } ---- \ No newline at end of file diff --git a/docs/rules/new-parens/index.html b/docs/rules/new-parens/index.html deleted file mode 100644 index 58aa3916f69..00000000000 --- a/docs/rules/new-parens/index.html +++ /dev/null @@ -1,14 +0,0 @@ ---- -ruleName: new-parens -description: Requires parentheses when invoking a constructor via the `new` keyword. -rationale: Maintains stylistic consistency with other function calls. -optionsDescription: Not configurable. -options: null -optionExamples: - - 'true' -type: style -typescriptOnly: false -layout: rule -title: 'Rule: new-parens' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/no-angle-bracket-type-assertion/index.html b/docs/rules/no-angle-bracket-type-assertion/index.html deleted file mode 100644 index bb441ed52bd..00000000000 --- a/docs/rules/no-angle-bracket-type-assertion/index.html +++ /dev/null @@ -1,19 +0,0 @@ ---- -ruleName: no-angle-bracket-type-assertion -description: Requires the use of `as Type` for type assertions instead of ``. -hasFix: true -rationale: |- - - Both formats of type assertions have the same effect, but only `as` type assertions - work in `.tsx` files. This rule ensures that you have a consistent type assertion style - across your codebase. -optionsDescription: Not configurable. -options: null -optionExamples: - - 'true' -type: style -typescriptOnly: true -layout: rule -title: 'Rule: no-angle-bracket-type-assertion' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/no-any/index.html b/docs/rules/no-any/index.html deleted file mode 100644 index 9e96a7e560a..00000000000 --- a/docs/rules/no-any/index.html +++ /dev/null @@ -1,14 +0,0 @@ ---- -ruleName: no-any -description: Diallows usages of `any` as a type declaration. -rationale: Using `any` as a type declaration nullifies the compile-time benefits of the type system. -optionsDescription: Not configurable. -options: null -optionExamples: - - 'true' -type: typescript -typescriptOnly: true -layout: rule -title: 'Rule: no-any' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/no-arg/index.html b/docs/rules/no-arg/index.html deleted file mode 100644 index a49b3401ff5..00000000000 --- a/docs/rules/no-arg/index.html +++ /dev/null @@ -1,18 +0,0 @@ ---- -ruleName: no-arg -description: Disallows use of `arguments.callee`. -rationale: |- - - Using `arguments.callee` makes various performance optimizations impossible. - See [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/arguments/callee) - for more details on why to avoid `arguments.callee`. -optionsDescription: Not configurable. -options: null -optionExamples: - - 'true' -type: functionality -typescriptOnly: false -layout: rule -title: 'Rule: no-arg' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/no-bitwise/index.html b/docs/rules/no-bitwise/index.html deleted file mode 100644 index 9c83ca93d89..00000000000 --- a/docs/rules/no-bitwise/index.html +++ /dev/null @@ -1,24 +0,0 @@ ---- -ruleName: no-bitwise -description: Disallows bitwise operators. -descriptionDetails: |- - - Specifically, the following bitwise operators are banned: - `&`, `&=`, `|`, `|=`, - `^`, `^=`, `<<`, `<<=`, - `>>`, `>>=`, `>>>`, `>>>=`, and `~`. - This rule does not ban the use of `&` and `|` for intersection and union types. -rationale: |- - - Bitwise operators are often typos - for example `bool1 & bool2` instead of `bool1 && bool2`. - They also can be an indicator of overly clever code which decreases maintainability. -optionsDescription: Not configurable. -options: null -optionExamples: - - 'true' -type: functionality -typescriptOnly: false -layout: rule -title: 'Rule: no-bitwise' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/no-boolean-compare/index.html b/docs/rules/no-boolean-compare/index.html deleted file mode 100644 index 1c1a89423b2..00000000000 --- a/docs/rules/no-boolean-compare/index.html +++ /dev/null @@ -1,14 +0,0 @@ ---- -ruleName: no-boolean-compare -description: 'Warns on comparison to a boolean literal, as in `x === true`.' -hasFix: true -optionsDescription: Not configurable. -options: null -optionExamples: - - 'true' -type: style -typescriptOnly: true -layout: rule -title: 'Rule: no-boolean-compare' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/no-boolean-literal-compare/index.html b/docs/rules/no-boolean-literal-compare/index.html deleted file mode 100644 index 74860ef768f..00000000000 --- a/docs/rules/no-boolean-literal-compare/index.html +++ /dev/null @@ -1,15 +0,0 @@ ---- -ruleName: no-boolean-literal-compare -description: 'Warns on comparison to a boolean literal, as in `x === true`.' -hasFix: true -optionsDescription: Not configurable. -options: null -optionExamples: - - 'true' -type: style -typescriptOnly: true -requiresTypeInfo: true -layout: rule -title: 'Rule: no-boolean-literal-compare' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/no-conditional-assignment/index.html b/docs/rules/no-conditional-assignment/index.html deleted file mode 100644 index b7bdc4041ef..00000000000 --- a/docs/rules/no-conditional-assignment/index.html +++ /dev/null @@ -1,19 +0,0 @@ ---- -ruleName: no-conditional-assignment -description: Disallows any type of assignment in conditionals. -descriptionDetails: 'This applies to `do-while`, `for`, `if`, and `while` statements.' -rationale: |- - - Assignments in conditionals are often typos: - for example `if (var1 = var2)` instead of `if (var1 == var2)`. - They also can be an indicator of overly clever code which decreases maintainability. -optionsDescription: Not configurable. -options: null -optionExamples: - - 'true' -type: functionality -typescriptOnly: false -layout: rule -title: 'Rule: no-conditional-assignment' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/no-consecutive-blank-lines/index.html b/docs/rules/no-consecutive-blank-lines/index.html deleted file mode 100644 index 180294b281f..00000000000 --- a/docs/rules/no-consecutive-blank-lines/index.html +++ /dev/null @@ -1,24 +0,0 @@ ---- -ruleName: no-consecutive-blank-lines -description: Disallows one or more blank lines in a row. -rationale: Helps maintain a readable style in your codebase. -optionsDescription: |- - - An optional number of maximum allowed sequential blanks can be specified. If no value - is provided, a default of $(Rule.DEFAULT_ALLOWED_BLANKS) will be used. -options: - type: number - minimum: $(Rule.MINIMUM_ALLOWED_BLANKS) -optionExamples: - - 'true' - - '[true, 2]' -type: style -typescriptOnly: false -layout: rule -title: 'Rule: no-consecutive-blank-lines' -optionsJSON: |- - { - "type": "number", - "minimum": "$(Rule.MINIMUM_ALLOWED_BLANKS)" - } ---- \ No newline at end of file diff --git a/docs/rules/no-console/index.html b/docs/rules/no-console/index.html deleted file mode 100644 index 3a04d5c29f2..00000000000 --- a/docs/rules/no-console/index.html +++ /dev/null @@ -1,23 +0,0 @@ ---- -ruleName: no-console -description: Bans the use of specified `console` methods. -rationale: 'In general, `console` methods aren''t appropriate for production code.' -optionsDescription: A list of method names to ban. -options: - type: array - items: - type: string -optionExamples: - - '[true, "log", "error"]' -type: functionality -typescriptOnly: false -layout: rule -title: 'Rule: no-console' -optionsJSON: |- - { - "type": "array", - "items": { - "type": "string" - } - } ---- \ No newline at end of file diff --git a/docs/rules/no-construct/index.html b/docs/rules/no-construct/index.html deleted file mode 100644 index d7f937db798..00000000000 --- a/docs/rules/no-construct/index.html +++ /dev/null @@ -1,19 +0,0 @@ ---- -ruleName: no-construct -description: 'Disallows access to the constructors of `String`, `Number`, and `Boolean`.' -descriptionDetails: Disallows constructor use such as `new Number(foo)` but does not disallow `Number(foo)`. -rationale: |- - - There is little reason to use `String`, `Number`, or `Boolean` as constructors. - In almost all cases, the regular function-call version is more appropriate. - [More details](http://stackoverflow.com/q/4719320/3124288) are available on StackOverflow. -optionsDescription: Not configurable. -options: null -optionExamples: - - 'true' -type: functionality -typescriptOnly: false -layout: rule -title: 'Rule: no-construct' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/no-debugger/index.html b/docs/rules/no-debugger/index.html deleted file mode 100644 index a3133012e5f..00000000000 --- a/docs/rules/no-debugger/index.html +++ /dev/null @@ -1,14 +0,0 @@ ---- -ruleName: no-debugger -description: Disallows `debugger` statements. -rationale: 'In general, `debugger` statements aren''t appropriate for production code.' -optionsDescription: Not configurable. -options: null -optionExamples: - - 'true' -type: functionality -typescriptOnly: false -layout: rule -title: 'Rule: no-debugger' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/no-default-export/index.html b/docs/rules/no-default-export/index.html deleted file mode 100644 index 8e7a279a2a7..00000000000 --- a/docs/rules/no-default-export/index.html +++ /dev/null @@ -1,19 +0,0 @@ ---- -ruleName: no-default-export -description: Disallows default exports in ES6-style modules. -descriptionDetails: Use named exports instead. -rationale: |- - - Named imports/exports [promote clarity](https://github.com/palantir/tslint/issues/1182#issue-151780453). - In addition, current tooling differs on the correct way to handle default imports/exports. - Avoiding them all together can help avoid tooling bugs and conflicts. -optionsDescription: Not configurable. -options: null -optionExamples: - - 'true' -type: maintainability -typescriptOnly: false -layout: rule -title: 'Rule: no-default-export' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/no-duplicate-variable/index.html b/docs/rules/no-duplicate-variable/index.html deleted file mode 100644 index 3dee824d2ce..00000000000 --- a/docs/rules/no-duplicate-variable/index.html +++ /dev/null @@ -1,21 +0,0 @@ ---- -ruleName: no-duplicate-variable -description: Disallows duplicate variable declarations in the same block scope. -descriptionDetails: |- - - This rule is only useful when using the `var` keyword - - the compiler will detect redeclarations of `let` and `const` variables. -rationale: |- - - A variable can be reassigned if necessary - - there's no good reason to have a duplicate variable declaration. -optionsDescription: Not configurable. -options: null -optionExamples: - - 'true' -type: functionality -typescriptOnly: false -layout: rule -title: 'Rule: no-duplicate-variable' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/no-empty-interface/index.html b/docs/rules/no-empty-interface/index.html deleted file mode 100644 index cd2fcfec4f6..00000000000 --- a/docs/rules/no-empty-interface/index.html +++ /dev/null @@ -1,12 +0,0 @@ ---- -ruleName: no-empty-interface -description: Forbids empty interfaces. -rationale: 'An empty interface is equivalent to its supertype (or `{}`).' -optionsDescription: Not configurable. -options: null -type: typescript -typescriptOnly: true -layout: rule -title: 'Rule: no-empty-interface' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/no-empty/index.html b/docs/rules/no-empty/index.html deleted file mode 100644 index 5fe4cc64ff0..00000000000 --- a/docs/rules/no-empty/index.html +++ /dev/null @@ -1,15 +0,0 @@ ---- -ruleName: no-empty -description: Disallows empty blocks. -descriptionDetails: Blocks with a comment inside are not considered empty. -rationale: Empty blocks are often indicators of missing code. -optionsDescription: Not configurable. -options: null -optionExamples: - - 'true' -type: functionality -typescriptOnly: false -layout: rule -title: 'Rule: no-empty' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/no-eval/index.html b/docs/rules/no-eval/index.html deleted file mode 100644 index 63f08fba017..00000000000 --- a/docs/rules/no-eval/index.html +++ /dev/null @@ -1,18 +0,0 @@ ---- -ruleName: no-eval -description: Disallows `eval` function invocations. -rationale: |- - - `eval()` is dangerous as it allows arbitrary code execution with full privileges. There are - [alternatives](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval) - for most of the use cases for `eval()`. -optionsDescription: Not configurable. -options: null -optionExamples: - - 'true' -type: functionality -typescriptOnly: false -layout: rule -title: 'Rule: no-eval' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/no-for-in-array/index.html b/docs/rules/no-for-in-array/index.html deleted file mode 100644 index 781e895234f..00000000000 --- a/docs/rules/no-for-in-array/index.html +++ /dev/null @@ -1,28 +0,0 @@ ---- -ruleName: no-for-in-array -description: Disallows iterating over an array with a for-in loop. -descriptionDetails: | - - A for-in loop (`for (var k in o)`) iterates over the properties of an Object. - - While it is legal to use for-in loops with array types, it is not common. - for-in will iterate over the indices of the array as strings, omitting any "holes" in - the array. - - More common is to use for-of, which iterates over the values of an array. - If you want to iterate over the indices, alternatives include: - - array.forEach((value, index) => { ... }); - for (const [index, value] of array.entries()) { ... } - for (let i = 0; i < array.length; i++) { ... } -optionsDescription: Not configurable. -options: null -optionExamples: - - 'true' -requiresTypeInfo: true -type: functionality -typescriptOnly: false -layout: rule -title: 'Rule: no-for-in-array' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/no-inferrable-types/index.html b/docs/rules/no-inferrable-types/index.html deleted file mode 100644 index 4518dc603e5..00000000000 --- a/docs/rules/no-inferrable-types/index.html +++ /dev/null @@ -1,38 +0,0 @@ ---- -ruleName: no-inferrable-types -description: 'Disallows explicit type declarations for variables or parameters initialized to a number, string, or boolean.' -rationale: Explicit types where they can be easily infered by the compiler make code more verbose. -optionsDescription: |- - - One argument may be optionally provided: - - * `ignore-params` allows specifying an inferrable type annotation for function params. - This can be useful when combining with the `typedef` rule. -options: - type: array - items: - type: string - enum: - - ignore-params - minLength: 0 - maxLength: 1 -optionExamples: - - 'true' - - '[true, "ignore-params"]' -type: typescript -typescriptOnly: true -layout: rule -title: 'Rule: no-inferrable-types' -optionsJSON: |- - { - "type": "array", - "items": { - "type": "string", - "enum": [ - "ignore-params" - ] - }, - "minLength": 0, - "maxLength": 1 - } ---- \ No newline at end of file diff --git a/docs/rules/no-inferred-empty-object-type/index.html b/docs/rules/no-inferred-empty-object-type/index.html deleted file mode 100644 index aa2a9c3f558..00000000000 --- a/docs/rules/no-inferred-empty-object-type/index.html +++ /dev/null @@ -1,14 +0,0 @@ ---- -ruleName: no-inferred-empty-object-type -description: 'Disallow type inference of {} (empty object type) at function and constructor call sites' -optionsDescription: Not configurable. -options: null -optionExamples: - - 'true' -type: functionality -typescriptOnly: true -requiresTypeInfo: true -layout: rule -title: 'Rule: no-inferred-empty-object-type' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/no-interface-constructor/index.html b/docs/rules/no-interface-constructor/index.html deleted file mode 100644 index b2f5778f784..00000000000 --- a/docs/rules/no-interface-constructor/index.html +++ /dev/null @@ -1,14 +0,0 @@ ---- -ruleName: no-interface-constructor -description: Warns on apparent attempts to define constructors for interfaces. -rationale: '`interface I { new(): I }` declares a type where for `x: I`, `new x()` is also of type `I`.' -optionsDescription: Not configurable. -options: null -optionExamples: - - 'true' -type: functionality -typescriptOnly: true -layout: rule -title: 'Rule: no-interface-constructor' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/no-internal-module/index.html b/docs/rules/no-internal-module/index.html deleted file mode 100644 index 5092d8ef529..00000000000 --- a/docs/rules/no-internal-module/index.html +++ /dev/null @@ -1,14 +0,0 @@ ---- -ruleName: no-internal-module -description: Disallows internal `module` -rationale: Using `module` leads to a confusion of concepts with external modules. Use the newer `namespace` keyword instead. -optionsDescription: Not configurable. -options: null -optionExamples: - - 'true' -type: typescript -typescriptOnly: true -layout: rule -title: 'Rule: no-internal-module' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/no-invalid-this/index.html b/docs/rules/no-invalid-this/index.html deleted file mode 100644 index fb2d3d34ed2..00000000000 --- a/docs/rules/no-invalid-this/index.html +++ /dev/null @@ -1,37 +0,0 @@ ---- -ruleName: no-invalid-this -description: Disallows using the `this` keyword outside of classes. -rationale: 'See [the rule''s author''s rationale here.](https://github.com/palantir/tslint/pull/1105#issue-147549402)' -optionsDescription: |- - - One argument may be optionally provided: - - * `check-function-in-method` disallows using the `this` keyword in functions within class methods. -options: - type: array - items: - type: string - enum: - - check-function-in-method - minLength: 0 - maxLength: 1 -optionExamples: - - 'true' - - '[true, "check-function-in-method"]' -type: functionality -typescriptOnly: false -layout: rule -title: 'Rule: no-invalid-this' -optionsJSON: |- - { - "type": "array", - "items": { - "type": "string", - "enum": [ - "check-function-in-method" - ] - }, - "minLength": 0, - "maxLength": 1 - } ---- \ No newline at end of file diff --git a/docs/rules/no-magic-numbers/index.html b/docs/rules/no-magic-numbers/index.html deleted file mode 100644 index 3ebe8bc3ed0..00000000000 --- a/docs/rules/no-magic-numbers/index.html +++ /dev/null @@ -1,32 +0,0 @@ ---- -ruleName: no-magic-numbers -description: |- - - Disallows the use constant number values outside of variable assignments. - When no list of allowed values is specified, -1, 0 and 1 are allowed by default. -rationale: |- - - Magic numbers should be avoided as they often lack documentation, forcing - them to be stored in variables gives them implicit documentation. -optionsDescription: A list of allowed numbers. -options: - type: array - items: - type: number - minLength: 1 -optionExamples: - - 'true' - - '[true, 1, 2, 3]' -type: typescript -typescriptOnly: false -layout: rule -title: 'Rule: no-magic-numbers' -optionsJSON: |- - { - "type": "array", - "items": { - "type": "number" - }, - "minLength": 1 - } ---- \ No newline at end of file diff --git a/docs/rules/no-mergeable-namespace/index.html b/docs/rules/no-mergeable-namespace/index.html deleted file mode 100644 index ca24c662893..00000000000 --- a/docs/rules/no-mergeable-namespace/index.html +++ /dev/null @@ -1,13 +0,0 @@ ---- -ruleName: no-mergeable-namespace -description: Disallows mergeable namespaces in the same file. -optionsDescription: Not configurable. -options: null -optionExamples: - - 'true' -type: maintainability -typescriptOnly: true -layout: rule -title: 'Rule: no-mergeable-namespace' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/no-misused-new/index.html b/docs/rules/no-misused-new/index.html deleted file mode 100644 index 771e8ed5754..00000000000 --- a/docs/rules/no-misused-new/index.html +++ /dev/null @@ -1,13 +0,0 @@ ---- -ruleName: no-misused-new -description: Warns on apparent attempts to define constructors for interfaces or `new` for classes. -optionsDescription: Not configurable. -options: null -optionExamples: - - 'true' -type: functionality -typescriptOnly: true -layout: rule -title: 'Rule: no-misused-new' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/no-namespace/index.html b/docs/rules/no-namespace/index.html deleted file mode 100644 index d29ecbf5262..00000000000 --- a/docs/rules/no-namespace/index.html +++ /dev/null @@ -1,41 +0,0 @@ ---- -ruleName: no-namespace -description: Disallows use of internal `module`s and `namespace`s. -descriptionDetails: 'This rule still allows the use of `declare module ... {}`' -rationale: |- - - ES6-style external modules are the standard way to modularize code. - Using `module {}` and `namespace {}` are outdated ways to organize TypeScript code. -optionsDescription: |- - - One argument may be optionally provided: - - * `allow-declarations` allows `declare namespace ... {}` to describe external APIs. -options: - type: array - items: - type: string - enum: - - allow-declarations - minLength: 0 - maxLength: 1 -optionExamples: - - 'true' - - '[true, "allow-declarations"]' -type: typescript -typescriptOnly: true -layout: rule -title: 'Rule: no-namespace' -optionsJSON: |- - { - "type": "array", - "items": { - "type": "string", - "enum": [ - "allow-declarations" - ] - }, - "minLength": 0, - "maxLength": 1 - } ---- \ No newline at end of file diff --git a/docs/rules/no-null-keyword/index.html b/docs/rules/no-null-keyword/index.html deleted file mode 100644 index c4c46d69f8d..00000000000 --- a/docs/rules/no-null-keyword/index.html +++ /dev/null @@ -1,17 +0,0 @@ ---- -ruleName: no-null-keyword -description: Disallows use of the `null` keyword literal. -rationale: |- - - Instead of having the dual concepts of `null` and`undefined` in a codebase, - this rule ensures that only `undefined` is used. -optionsDescription: Not configurable. -options: null -optionExamples: - - 'true' -type: functionality -typescriptOnly: false -layout: rule -title: 'Rule: no-null-keyword' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/no-parameter-properties/index.html b/docs/rules/no-parameter-properties/index.html deleted file mode 100644 index bac7fbb8f6e..00000000000 --- a/docs/rules/no-parameter-properties/index.html +++ /dev/null @@ -1,17 +0,0 @@ ---- -ruleName: no-parameter-properties -description: Disallows parameter properties in class constructors. -rationale: |- - - Parameter properties can be confusing to those new to TS as they are less explicit - than other ways of declaring and initializing class members. -optionsDescription: Not configurable. -options: null -optionExamples: - - 'true' -type: style -typescriptOnly: true -layout: rule -title: 'Rule: no-parameter-properties' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/no-reference/index.html b/docs/rules/no-reference/index.html deleted file mode 100644 index 85ff055965e..00000000000 --- a/docs/rules/no-reference/index.html +++ /dev/null @@ -1,17 +0,0 @@ ---- -ruleName: no-reference -description: Disallows `/// ` imports (use ES6-style imports instead). -rationale: |- - - Using `/// ` comments to load other files is outdated. - Use ES6-style imports to reference other files. -optionsDescription: Not configurable. -options: null -optionExamples: - - 'true' -type: typescript -typescriptOnly: false -layout: rule -title: 'Rule: no-reference' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/no-require-imports/index.html b/docs/rules/no-require-imports/index.html deleted file mode 100644 index 0791ef11775..00000000000 --- a/docs/rules/no-require-imports/index.html +++ /dev/null @@ -1,14 +0,0 @@ ---- -ruleName: no-require-imports -description: Disallows invocation of `require()`. -rationale: Prefer the newer ES6-style imports over `require()`. -optionsDescription: Not configurable. -options: null -optionExamples: - - 'true' -type: maintainability -typescriptOnly: false -layout: rule -title: 'Rule: no-require-imports' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/no-shadowed-variable/index.html b/docs/rules/no-shadowed-variable/index.html deleted file mode 100644 index c9b41de6420..00000000000 --- a/docs/rules/no-shadowed-variable/index.html +++ /dev/null @@ -1,14 +0,0 @@ ---- -ruleName: no-shadowed-variable -description: Disallows shadowing variable declarations. -rationale: Shadowing a variable masks access to it and obscures to what value an identifier actually refers. -optionsDescription: Not configurable. -options: null -optionExamples: - - 'true' -type: functionality -typescriptOnly: false -layout: rule -title: 'Rule: no-shadowed-variable' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/no-string-literal/index.html b/docs/rules/no-string-literal/index.html deleted file mode 100644 index c4472e65b53..00000000000 --- a/docs/rules/no-string-literal/index.html +++ /dev/null @@ -1,14 +0,0 @@ ---- -ruleName: no-string-literal -description: Disallows object access via string literals. -rationale: Encourages using strongly-typed property access. -optionsDescription: Not configurable. -options: null -optionExamples: - - 'true' -type: functionality -typescriptOnly: false -layout: rule -title: 'Rule: no-string-literal' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/no-string-throw/index.html b/docs/rules/no-string-throw/index.html deleted file mode 100644 index de7259b8141..00000000000 --- a/docs/rules/no-string-throw/index.html +++ /dev/null @@ -1,12 +0,0 @@ ---- -ruleName: no-string-throw -description: Flags throwing plain strings or concatenations of strings because only Errors produce proper stack traces. -hasFix: true -options: null -optionsDescription: Not configurable. -type: functionality -typescriptOnly: false -layout: rule -title: 'Rule: no-string-throw' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/no-switch-case-fall-through/index.html b/docs/rules/no-switch-case-fall-through/index.html deleted file mode 100644 index e6ff8459f6d..00000000000 --- a/docs/rules/no-switch-case-fall-through/index.html +++ /dev/null @@ -1,40 +0,0 @@ ---- -ruleName: no-switch-case-fall-through -description: Disallows falling through case statements. -descriptionDetails: |- - - For example, the following is not allowed: - - ```ts - switch(foo) { - case 1: - someFunc(foo); - case 2: - someOtherFunc(foo); - } - ``` - - However, fall through is allowed when case statements are consecutive or - a magic `/* falls through */` comment is present. The following is valid: - - ```ts - switch(foo) { - case 1: - someFunc(foo); - /* falls through */ - case 2: - case 3: - someOtherFunc(foo); - } - ``` -rationale: Fall though in switch statements is often unintentional and a bug. -optionsDescription: Not configurable. -options: null -optionExamples: - - 'true' -type: functionality -typescriptOnly: false -layout: rule -title: 'Rule: no-switch-case-fall-through' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/no-trailing-whitespace/index.html b/docs/rules/no-trailing-whitespace/index.html deleted file mode 100644 index eef2dc69bf4..00000000000 --- a/docs/rules/no-trailing-whitespace/index.html +++ /dev/null @@ -1,14 +0,0 @@ ---- -ruleName: no-trailing-whitespace -description: Disallows trailing whitespace at the end of a line. -rationale: Keeps version control diffs clean as it prevents accidental whitespace from being committed. -optionsDescription: Not configurable. -options: null -optionExamples: - - 'true' -type: maintainability -typescriptOnly: false -layout: rule -title: 'Rule: no-trailing-whitespace' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/no-unnecessary-qualifier/index.html b/docs/rules/no-unnecessary-qualifier/index.html deleted file mode 100644 index 342ca422939..00000000000 --- a/docs/rules/no-unnecessary-qualifier/index.html +++ /dev/null @@ -1,15 +0,0 @@ ---- -ruleName: no-unnecessary-qualifier -description: Warns when a namespace qualifier (`A.x`) is unnecessary. -hasFix: true -optionsDescription: Not configurable. -options: null -optionExamples: - - 'true' -type: style -typescriptOnly: true -requiresTypeInfo: true -layout: rule -title: 'Rule: no-unnecessary-qualifier' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/no-unreachable/index.html b/docs/rules/no-unreachable/index.html deleted file mode 100644 index f660c7e3198..00000000000 --- a/docs/rules/no-unreachable/index.html +++ /dev/null @@ -1,13 +0,0 @@ ---- -ruleName: no-unreachable -description: 'Disallows unreachable code after `break`, `catch`, `throw`, and `return` statements.' -rationale: Unreachable code is often indication of a logic error. -optionsDescription: Not configurable. -options: null -optionExamples: - - 'true' -type: functionality -optionsJSON: 'null' -layout: rule -title: 'Rule: no-unreachable' ---- \ No newline at end of file diff --git a/docs/rules/no-unsafe-finally/index.html b/docs/rules/no-unsafe-finally/index.html deleted file mode 100644 index 1d6e46900c8..00000000000 --- a/docs/rules/no-unsafe-finally/index.html +++ /dev/null @@ -1,23 +0,0 @@ ---- -ruleName: no-unsafe-finally -description: |- - - Disallows control flow statements, such as `return`, `continue`, - `break` and `throws` in finally blocks. -descriptionDetails: '' -rationale: |- - - When used inside `finally` blocks, control flow statements, - such as `return`, `continue`, `break` and `throws` - override any other control flow statements in the same try/catch scope. - This is confusing and unexpected behavior. -optionsDescription: Not configurable. -options: null -optionExamples: - - 'true' -type: functionality -typescriptOnly: false -layout: rule -title: 'Rule: no-unsafe-finally' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/no-unused-expression/index.html b/docs/rules/no-unused-expression/index.html deleted file mode 100644 index 6856223df77..00000000000 --- a/docs/rules/no-unused-expression/index.html +++ /dev/null @@ -1,44 +0,0 @@ ---- -ruleName: no-unused-expression -description: Disallows unused expression statements. -descriptionDetails: |- - - Unused expressions are expression statements which are not assignments or function calls - (and thus usually no-ops). -rationale: |- - - Detects potential errors where an assignment or function call was intended. -optionsDescription: |- - - One argument may be optionally provided: - - * `allow-fast-null-checks` allows to use logical operators to perform fast null checks and perform - method or function calls for side effects (e.g. `e && e.preventDefault()`). -options: - type: array - items: - type: string - enum: - - allow-fast-null-checks - minLength: 0 - maxLength: 1 -optionExamples: - - 'true' - - '[true, "allow-fast-null-checks"]' -type: functionality -typescriptOnly: false -layout: rule -title: 'Rule: no-unused-expression' -optionsJSON: |- - { - "type": "array", - "items": { - "type": "string", - "enum": [ - "allow-fast-null-checks" - ] - }, - "minLength": 0, - "maxLength": 1 - } ---- \ No newline at end of file diff --git a/docs/rules/no-unused-new/index.html b/docs/rules/no-unused-new/index.html deleted file mode 100644 index 4508b8fb8c6..00000000000 --- a/docs/rules/no-unused-new/index.html +++ /dev/null @@ -1,20 +0,0 @@ ---- -ruleName: no-unused-new -description: Disallows unused 'new' expression statements. -descriptionDetails: |- - - Unused 'new' expressions indicate that a constructor is being invoked solely for its side effects. -rationale: |- - - Detects constructs such as `new SomeClass()`, where a constructor is used solely for its side effects, which is considered - poor style. -optionsDescription: Not configurable. -options: null -optionExamples: - - 'true' -type: functionality -typescriptOnly: false -layout: rule -title: 'Rule: no-unused-new' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/no-unused-variable/index.html b/docs/rules/no-unused-variable/index.html deleted file mode 100644 index 8d68ff2ebcd..00000000000 --- a/docs/rules/no-unused-variable/index.html +++ /dev/null @@ -1,67 +0,0 @@ ---- -ruleName: no-unused-variable -deprecationMessage: Use the tsc compiler options --noUnusedParameters and --noUnusedLocals instead. -description: 'Disallows unused imports, variables, functions and private class members.' -hasFix: true -optionsDescription: |- - - Three optional arguments may be optionally provided: - - * `"check-parameters"` disallows unused function and constructor parameters. - * NOTE: this option is experimental and does not work with classes - that use abstract method declarations, among other things. - * `"react"` relaxes the rule for a namespace import named `React` - (from either the module `"react"` or `"react/addons"`). - Any JSX expression in the file will be treated as a usage of `React` - (because it expands to `React.createElement `). - * `{"ignore-pattern": "pattern"}` where pattern is a case-sensitive regexp. - Variable names that match the pattern will be ignored. -options: - type: array - items: - oneOf: - - type: string - enum: - - check-parameters - - react - - type: object - properties: - ignore-pattern: - type: string - additionalProperties: false - minLength: 0 - maxLength: 3 -optionExamples: - - '[true, "react"]' - - '[true, {"ignore-pattern": "^_"}]' -type: functionality -typescriptOnly: true -layout: rule -title: 'Rule: no-unused-variable' -optionsJSON: |- - { - "type": "array", - "items": { - "oneOf": [ - { - "type": "string", - "enum": [ - "check-parameters", - "react" - ] - }, - { - "type": "object", - "properties": { - "ignore-pattern": { - "type": "string" - } - }, - "additionalProperties": false - } - ] - }, - "minLength": 0, - "maxLength": 3 - } ---- \ No newline at end of file diff --git a/docs/rules/no-use-before-declare/index.html b/docs/rules/no-use-before-declare/index.html deleted file mode 100644 index d2c24056758..00000000000 --- a/docs/rules/no-use-before-declare/index.html +++ /dev/null @@ -1,17 +0,0 @@ ---- -ruleName: no-use-before-declare -description: Disallows usage of variables before their declaration. -descriptionDetails: |- - - This rule is primarily useful when using the `var` keyword - - the compiler will detect if a `let` and `const` variable is used before it is declared. -optionsDescription: Not configurable. -options: null -optionExamples: - - 'true' -type: functionality -typescriptOnly: false -layout: rule -title: 'Rule: no-use-before-declare' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/no-var-keyword/index.html b/docs/rules/no-var-keyword/index.html deleted file mode 100644 index 88b9b544610..00000000000 --- a/docs/rules/no-var-keyword/index.html +++ /dev/null @@ -1,15 +0,0 @@ ---- -ruleName: no-var-keyword -description: Disallows usage of the `var` keyword. -descriptionDetails: Use `let` or `const` instead. -hasFix: true -optionsDescription: Not configurable. -options: null -optionExamples: - - 'true' -type: functionality -typescriptOnly: false -layout: rule -title: 'Rule: no-var-keyword' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/no-var-requires/index.html b/docs/rules/no-var-requires/index.html deleted file mode 100644 index ecbbca8fe5a..00000000000 --- a/docs/rules/no-var-requires/index.html +++ /dev/null @@ -1,17 +0,0 @@ ---- -ruleName: no-var-requires -description: Disallows the use of require statements except in import statements. -descriptionDetails: |- - - In other words, the use of forms such as `var module = require("module")` are banned. - Instead use ES6 style imports or `import foo = require('foo')` imports. -optionsDescription: Not configurable. -options: null -optionExamples: - - 'true' -type: typescript -typescriptOnly: true -layout: rule -title: 'Rule: no-var-requires' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/no-void-expression/index.html b/docs/rules/no-void-expression/index.html deleted file mode 100644 index 6722b9d77a0..00000000000 --- a/docs/rules/no-void-expression/index.html +++ /dev/null @@ -1,12 +0,0 @@ ---- -ruleName: no-void-expression -description: Requires expressions of type `void` to appear in statement position. -optionsDescription: Not configurable. -options: null -requiresTypeInfo: true -type: functionality -typescriptOnly: false -layout: rule -title: 'Rule: no-void-expression' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/object-literal-key-quotes/index.html b/docs/rules/object-literal-key-quotes/index.html deleted file mode 100644 index 7f39682b16f..00000000000 --- a/docs/rules/object-literal-key-quotes/index.html +++ /dev/null @@ -1,59 +0,0 @@ ---- -ruleName: object-literal-key-quotes -description: Enforces consistent object literal property quote style. -descriptionDetails: |- - - Object literal property names can be defined in two ways: using literals or using strings. - For example, these two objects are equivalent: - - var object1 = { - property: true - }; - - var object2 = { - "property": true - }; - - In many cases, it doesn’t matter if you choose to use an identifier instead of a string - or vice-versa. Even so, you might decide to enforce a consistent style in your code. - - This rules lets you enforce consistent quoting of property names. Either they should always - be quoted (default behavior) or quoted only as needed ("as-needed"). -hasFix: true -optionsDescription: |- - - Possible settings are: - - * `"always"`: Property names should always be quoted. (This is the default.) - * `"as-needed"`: Only property names which require quotes may be quoted (e.g. those with spaces in them). - * `"consistent"`: Property names should either all be quoted or unquoted. - * `"consistent-as-needed"`: If any property name requires quotes, then all properties must be quoted. Otherwise, no - property names may be quoted. - - For ES6, computed property names (`{[name]: value}`) and methods (`{foo() {}}`) never need - to be quoted. -options: - type: string - enum: - - always - - as-needed - - consistent - - consistent-as-needed -optionExamples: - - '[true, "as-needed"]' - - '[true, "always"]' -type: style -typescriptOnly: false -layout: rule -title: 'Rule: object-literal-key-quotes' -optionsJSON: |- - { - "type": "string", - "enum": [ - "always", - "as-needed", - "consistent", - "consistent-as-needed" - ] - } ---- \ No newline at end of file diff --git a/docs/rules/object-literal-shorthand/index.html b/docs/rules/object-literal-shorthand/index.html deleted file mode 100644 index 1a6ca8708e8..00000000000 --- a/docs/rules/object-literal-shorthand/index.html +++ /dev/null @@ -1,13 +0,0 @@ ---- -ruleName: object-literal-shorthand -description: Enforces use of ES6 object literal shorthand when possible. -optionsDescription: Not configurable. -options: null -optionExamples: - - 'true' -type: style -typescriptOnly: false -layout: rule -title: 'Rule: object-literal-shorthand' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/object-literal-sort-keys/index.html b/docs/rules/object-literal-sort-keys/index.html deleted file mode 100644 index 44dcc1850e8..00000000000 --- a/docs/rules/object-literal-sort-keys/index.html +++ /dev/null @@ -1,14 +0,0 @@ ---- -ruleName: object-literal-sort-keys -description: Requires keys in object literals to be sorted alphabetically -rationale: Useful in preventing merge conflicts -optionsDescription: Not configurable. -options: null -optionExamples: - - 'true' -type: maintainability -typescriptOnly: false -layout: rule -title: 'Rule: object-literal-sort-keys' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/one-line/index.html b/docs/rules/one-line/index.html deleted file mode 100644 index 646e89b8d56..00000000000 --- a/docs/rules/one-line/index.html +++ /dev/null @@ -1,47 +0,0 @@ ---- -ruleName: one-line -description: Requires the specified tokens to be on the same line as the expression preceding them. -optionsDescription: |- - - Five arguments may be optionally provided: - - * `"check-catch"` checks that `catch` is on the same line as the closing brace for `try`. - * `"check-finally"` checks that `finally` is on the same line as the closing brace for `catch`. - * `"check-else"` checks that `else` is on the same line as the closing brace for `if`. - * `"check-open-brace"` checks that an open brace falls on the same line as its preceding expression. - * `"check-whitespace"` checks preceding whitespace for the specified tokens. -options: - type: array - items: - type: string - enum: - - check-catch - - check-finally - - check-else - - check-open-brace - - check-whitespace - minLength: 0 - maxLength: 5 -optionExamples: - - '[true, "check-catch", "check-finally", "check-else"]' -type: style -typescriptOnly: false -layout: rule -title: 'Rule: one-line' -optionsJSON: |- - { - "type": "array", - "items": { - "type": "string", - "enum": [ - "check-catch", - "check-finally", - "check-else", - "check-open-brace", - "check-whitespace" - ] - }, - "minLength": 0, - "maxLength": 5 - } ---- \ No newline at end of file diff --git a/docs/rules/one-variable-per-declaration/index.html b/docs/rules/one-variable-per-declaration/index.html deleted file mode 100644 index 327b8b59f65..00000000000 --- a/docs/rules/one-variable-per-declaration/index.html +++ /dev/null @@ -1,36 +0,0 @@ ---- -ruleName: one-variable-per-declaration -description: Disallows multiple variable definitions in the same declaration statement. -optionsDescription: |- - - One argument may be optionally provided: - - * `ignore-for-loop` allows multiple variable definitions in a for loop declaration. -options: - type: array - items: - type: string - enum: - - ignore-for-loop - minLength: 0 - maxLength: 1 -optionExamples: - - 'true' - - '[true, "ignore-for-loop"]' -type: style -typescriptOnly: false -layout: rule -title: 'Rule: one-variable-per-declaration' -optionsJSON: |- - { - "type": "array", - "items": { - "type": "string", - "enum": [ - "ignore-for-loop" - ] - }, - "minLength": 0, - "maxLength": 1 - } ---- \ No newline at end of file diff --git a/docs/rules/only-arrow-functions/index.html b/docs/rules/only-arrow-functions/index.html deleted file mode 100644 index bff41cac593..00000000000 --- a/docs/rules/only-arrow-functions/index.html +++ /dev/null @@ -1,41 +0,0 @@ ---- -ruleName: only-arrow-functions -description: Disallows traditional (non-arrow) function expressions. -rationale: 'Traditional functions don''t bind lexical scope, which can lead to unexpected behavior when accessing ''this''.' -optionsDescription: |- - - Two arguments may be optionally provided: - - * `"allow-declarations"` allows standalone function declarations. - * `"allow-named-functions"` allows the expression `function foo() {}` but not `function() {}`. - -options: - type: array - items: - type: string - enum: - - allow-declarations - - allow-named-functions - minLength: 0 - maxLength: 1 -optionExamples: - - 'true' - - '[true, "allow-declarations", "allow-named-functions"]' -type: typescript -typescriptOnly: false -layout: rule -title: 'Rule: only-arrow-functions' -optionsJSON: |- - { - "type": "array", - "items": { - "type": "string", - "enum": [ - "allow-declarations", - "allow-named-functions" - ] - }, - "minLength": 0, - "maxLength": 1 - } ---- \ No newline at end of file diff --git a/docs/rules/ordered-imports/index.html b/docs/rules/ordered-imports/index.html deleted file mode 100644 index 607a5097967..00000000000 --- a/docs/rules/ordered-imports/index.html +++ /dev/null @@ -1,89 +0,0 @@ ---- -ruleName: ordered-imports -description: Requires that import statements be alphabetized. -descriptionDetails: |- - - Enforce a consistent ordering for ES6 imports: - - Named imports must be alphabetized (i.e. "import {A, B, C} from "foo";") - - The exact ordering can be controlled by the named-imports-order option. - - "longName as name" imports are ordered by "longName". - - Import sources must be alphabetized within groups, i.e.: - import * as foo from "a"; - import * as bar from "b"; - - Groups of imports are delineated by blank lines. You can use these to group imports - however you like, e.g. by first- vs. third-party or thematically. -hasFix: true -optionsDescription: |- - - You may set the `"import-sources-order"` option to control the ordering of source - imports (the `"foo"` in `import {A, B, C} from "foo"`). - - Possible values for `"import-sources-order"` are: - - * `"case-insensitive'`: Correct order is `"Bar"`, `"baz"`, `"Foo"`. (This is the default.) - * `"lowercase-first"`: Correct order is `"baz"`, `"Bar"`, `"Foo"`. - * `"lowercase-last"`: Correct order is `"Bar"`, `"Foo"`, `"baz"`. - * `"any"`: Allow any order. - - You may set the `"named-imports-order"` option to control the ordering of named - imports (the `{A, B, C}` in `import {A, B, C} from "foo"`). - - Possible values for `"named-imports-order"` are: - - * `"case-insensitive'`: Correct order is `{A, b, C}`. (This is the default.) - * `"lowercase-first"`: Correct order is `{b, A, C}`. - * `"lowercase-last"`: Correct order is `{A, C, b}`. - * `"any"`: Allow any order. - - -options: - type: object - properties: - import-sources-order: - type: string - enum: - - case-insensitive - - lowercase-first - - lowercase-last - - any - named-imports-order: - type: string - enum: - - case-insensitive - - lowercase-first - - lowercase-last - - any - additionalProperties: false -optionExamples: - - 'true' - - '[true, {"import-sources-order": "lowercase-last", "named-imports-order": "lowercase-first"}]' -type: style -typescriptOnly: false -layout: rule -title: 'Rule: ordered-imports' -optionsJSON: |- - { - "type": "object", - "properties": { - "import-sources-order": { - "type": "string", - "enum": [ - "case-insensitive", - "lowercase-first", - "lowercase-last", - "any" - ] - }, - "named-imports-order": { - "type": "string", - "enum": [ - "case-insensitive", - "lowercase-first", - "lowercase-last", - "any" - ] - } - }, - "additionalProperties": false - } ---- \ No newline at end of file diff --git a/docs/rules/prefer-arrow-shorthand-return/index.html b/docs/rules/prefer-arrow-shorthand-return/index.html deleted file mode 100644 index 615e5a844da..00000000000 --- a/docs/rules/prefer-arrow-shorthand-return/index.html +++ /dev/null @@ -1,14 +0,0 @@ ---- -ruleName: prefer-arrow-shorthand-return -description: 'Suggests to convert `() => { return x; }` to `() => x`.' -optionsDescription: Not configurable. -options: null -optionExamples: - - '[true]' - - '[true, "multiline"]' -type: functionality -typescriptOnly: false -layout: rule -title: 'Rule: prefer-arrow-shorthand-return' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/prefer-const/index.html b/docs/rules/prefer-const/index.html deleted file mode 100644 index 6ff68747269..00000000000 --- a/docs/rules/prefer-const/index.html +++ /dev/null @@ -1,17 +0,0 @@ ---- -ruleName: prefer-const -description: Requires that variable declarations use `const` instead of `let` if possible. -descriptionDetails: |- - - If a variable is only assigned to once when it is declared, it should be declared using 'const' -hasFix: true -optionsDescription: Not configurable. -options: null -optionExamples: - - 'true' -type: maintainability -typescriptOnly: false -layout: rule -title: 'Rule: prefer-const' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/prefer-for-of/index.html b/docs/rules/prefer-for-of/index.html deleted file mode 100644 index 182c695e16f..00000000000 --- a/docs/rules/prefer-for-of/index.html +++ /dev/null @@ -1,14 +0,0 @@ ---- -ruleName: prefer-for-of -description: Recommends a 'for-of' loop over a standard 'for' loop if the index is only used to access the array being iterated. -rationale: A for(... of ...) loop is easier to implement and read when the index is not needed. -optionsDescription: Not configurable. -options: null -optionExamples: - - 'true' -type: typescript -typescriptOnly: false -layout: rule -title: 'Rule: prefer-for-of' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/prefer-method-signature/index.html b/docs/rules/prefer-method-signature/index.html deleted file mode 100644 index c5b89e93be1..00000000000 --- a/docs/rules/prefer-method-signature/index.html +++ /dev/null @@ -1,14 +0,0 @@ ---- -ruleName: prefer-method-signature -description: 'Prefer `foo(): void` over `foo: () => void` in interfaces and types.' -hasFix: true -optionsDescription: Not configurable. -options: null -optionExamples: - - 'true' -type: style -typescriptOnly: false -layout: rule -title: 'Rule: prefer-method-signature' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/promise-function-async/index.html b/docs/rules/promise-function-async/index.html deleted file mode 100644 index 2da06dd6f4a..00000000000 --- a/docs/rules/promise-function-async/index.html +++ /dev/null @@ -1,21 +0,0 @@ ---- -ruleName: promise-function-async -description: Requires any function or method that returns a promise to be marked async. -rationale: |- - - Ensures that each function is only capable of 1) returning a rejected promise, or 2) - throwing an Error object. In contrast, non-`async` `Promise`-returning functions - are technically capable of either. This practice removes a requirement for consuming - code to handle both cases. - -optionsDescription: Not configurable. -options: null -optionExamples: - - 'true' -type: typescript -typescriptOnly: false -requiresTypeInfo: true -layout: rule -title: 'Rule: promise-function-async' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/quotemark/index.html b/docs/rules/quotemark/index.html deleted file mode 100644 index c3c295f2d8e..00000000000 --- a/docs/rules/quotemark/index.html +++ /dev/null @@ -1,50 +0,0 @@ ---- -ruleName: quotemark -description: Requires single or double quotes for string literals. -hasFix: true -optionsDescription: |- - - Five arguments may be optionally provided: - - * `"single"` enforces single quotes. - * `"double"` enforces double quotes. - * `"jsx-single"` enforces single quotes for JSX attributes. - * `"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"'`. -options: - type: array - items: - type: string - enum: - - single - - double - - jsx-single - - jsx-double - - avoid-escape - minLength: 0 - maxLength: 5 -optionExamples: - - '[true, "single", "avoid-escape"]' - - '[true, "single", "jsx-double"]' -type: style -typescriptOnly: false -layout: rule -title: 'Rule: quotemark' -optionsJSON: |- - { - "type": "array", - "items": { - "type": "string", - "enum": [ - "single", - "double", - "jsx-single", - "jsx-double", - "avoid-escape" - ] - }, - "minLength": 0, - "maxLength": 5 - } ---- \ No newline at end of file diff --git a/docs/rules/radix/index.html b/docs/rules/radix/index.html deleted file mode 100644 index b3af9ac6b9d..00000000000 --- a/docs/rules/radix/index.html +++ /dev/null @@ -1,18 +0,0 @@ ---- -ruleName: radix -description: Requires the radix parameter to be specified when calling `parseInt`. -rationale: |- - - From [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseInt): - > Always specify this parameter to eliminate reader confusion and to guarantee predictable behavior. - > Different implementations produce different results when a radix is not specified, usually defaulting the value to 10. -optionsDescription: Not configurable. -options: null -optionExamples: - - 'true' -type: functionality -typescriptOnly: false -layout: rule -title: 'Rule: radix' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/restrict-plus-operands/index.html b/docs/rules/restrict-plus-operands/index.html deleted file mode 100644 index 1a20bb14ee8..00000000000 --- a/docs/rules/restrict-plus-operands/index.html +++ /dev/null @@ -1,14 +0,0 @@ ---- -ruleName: restrict-plus-operands -description: 'When adding two variables, operands must both be of type number or of type string.' -optionsDescription: Not configurable. -options: null -optionExamples: - - 'true' -type: functionality -typescriptOnly: false -requiresTypeInfo: true -layout: rule -title: 'Rule: restrict-plus-operands' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/semicolon/index.html b/docs/rules/semicolon/index.html deleted file mode 100644 index e42fd34fcf6..00000000000 --- a/docs/rules/semicolon/index.html +++ /dev/null @@ -1,56 +0,0 @@ ---- -ruleName: semicolon -description: Enforces consistent semicolon usage at the end of every statement. -hasFix: true -optionsDescription: |- - - One of the following arguments must be provided: - - * `"always"` enforces semicolons at the end of every statement. - * `"never"` disallows semicolons at the end of every statement except for when they are necessary. - - The following arguments may be optionaly provided: - - * `"ignore-interfaces"` skips checking semicolons at the end of interface members. - * `"ignore-bound-class-methods"` skips checking semicolons at the end of bound class methods. -options: - type: array - items: - - type: string - enum: - - always - - never - - type: string - enum: - - ignore-interfaces - additionalItems: false -optionExamples: - - '[true, "always"]' - - '[true, "never"]' - - '[true, "always", "ignore-interfaces"]' - - '[true, "always", "ignore-bound-class-methods"]' -type: style -typescriptOnly: false -layout: rule -title: 'Rule: semicolon' -optionsJSON: |- - { - "type": "array", - "items": [ - { - "type": "string", - "enum": [ - "always", - "never" - ] - }, - { - "type": "string", - "enum": [ - "ignore-interfaces" - ] - } - ], - "additionalItems": false - } ---- \ No newline at end of file diff --git a/docs/rules/space-before-function-paren/index.html b/docs/rules/space-before-function-paren/index.html deleted file mode 100644 index e9a200d58ee..00000000000 --- a/docs/rules/space-before-function-paren/index.html +++ /dev/null @@ -1,78 +0,0 @@ ---- -description: Require or disallow a space before function parenthesis -hasFix: true -optionExamples: - - 'true' - - '[true, "always"]' - - '[true, "never"]' - - '[true, {"anonymous": "always", "named": "never", "asyncArrow": "always"}]' -options: - properties: - anonymous: &ref_0 - enum: - - always - - never - type: string - asyncArrow: *ref_0 - constructor: *ref_0 - method: *ref_0 - named: *ref_0 - type: object -optionsDescription: |- - - One argument which is an object which may contain the keys `anonymous`, `named`, and `asyncArrow` - These should be set to either `"always"` or `"never"`. - - * `"anonymous"` checks before the opening paren in anonymous functions - * `"named"` checks before the opening paren in named functions - * `"asyncArrow"` checks before the opening paren in async arrow functions - * `"method"` checks before the opening paren in class methods - * `"constructor"` checks before the opening paren in class constructors - -ruleName: space-before-function-paren -type: style -typescriptOnly: false -layout: rule -title: 'Rule: space-before-function-paren' -optionsJSON: |- - { - "properties": { - "anonymous": { - "enum": [ - "always", - "never" - ], - "type": "string" - }, - "asyncArrow": { - "enum": [ - "always", - "never" - ], - "type": "string" - }, - "constructor": { - "enum": [ - "always", - "never" - ], - "type": "string" - }, - "method": { - "enum": [ - "always", - "never" - ], - "type": "string" - }, - "named": { - "enum": [ - "always", - "never" - ], - "type": "string" - } - }, - "type": "object" - } ---- \ No newline at end of file diff --git a/docs/rules/strict-boolean-expressions/index.html b/docs/rules/strict-boolean-expressions/index.html deleted file mode 100644 index a3bf5cbd61c..00000000000 --- a/docs/rules/strict-boolean-expressions/index.html +++ /dev/null @@ -1,16 +0,0 @@ ---- -ruleName: strict-boolean-expressions -description: |- - Usage of && or || operators should be with boolean operands and - expressions in If, Do, While and For statements should be of type boolean -optionsDescription: Not configurable. -options: null -optionExamples: - - 'true' -type: functionality -typescriptOnly: true -requiresTypeInfo: true -layout: rule -title: 'Rule: strict-boolean-expressions' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/switch-default/index.html b/docs/rules/switch-default/index.html deleted file mode 100644 index 4bfa95371fa..00000000000 --- a/docs/rules/switch-default/index.html +++ /dev/null @@ -1,13 +0,0 @@ ---- -ruleName: switch-default -description: Require a `default` case in all `switch` statements. -optionsDescription: Not configurable. -options: null -optionExamples: - - 'true' -type: functionality -typescriptOnly: false -layout: rule -title: 'Rule: switch-default' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/trailing-comma/index.html b/docs/rules/trailing-comma/index.html deleted file mode 100644 index d41f07af51b..00000000000 --- a/docs/rules/trailing-comma/index.html +++ /dev/null @@ -1,61 +0,0 @@ ---- -ruleName: trailing-comma -description: |- - - Requires or disallows trailing commas in array and object literals, destructuring assignments, function and tuple typings, - named imports and function parameters. -hasFix: true -optionsDescription: |- - - One argument which is an object with the keys `multiline` and `singleline`. - Both should be set to either `"always"` or `"never"`. - - * `"multiline"` checks multi-line object literals. - * `"singleline"` checks single-line object literals. - - A array is considered "multiline" if its closing bracket is on a line - after the last array element. The same general logic is followed for - object literals, function and tuple typings, named import statements - and function parameters. -options: - type: object - properties: - multiline: - type: string - enum: - - always - - never - singleline: - type: string - enum: - - always - - never - additionalProperties: false -optionExamples: - - '[true, {"multiline": "always", "singleline": "never"}]' -type: maintainability -typescriptOnly: false -layout: rule -title: 'Rule: trailing-comma' -optionsJSON: |- - { - "type": "object", - "properties": { - "multiline": { - "type": "string", - "enum": [ - "always", - "never" - ] - }, - "singleline": { - "type": "string", - "enum": [ - "always", - "never" - ] - } - }, - "additionalProperties": false - } ---- \ No newline at end of file diff --git a/docs/rules/triple-equals/index.html b/docs/rules/triple-equals/index.html deleted file mode 100644 index 1fdfe98e5b8..00000000000 --- a/docs/rules/triple-equals/index.html +++ /dev/null @@ -1,40 +0,0 @@ ---- -ruleName: triple-equals -description: Requires `===` and `!==` in place of `==` and `!=`. -optionsDescription: |- - - Two arguments may be optionally provided: - - * `"allow-null-check"` allows `==` and `!=` when comparing to `null`. - * `"allow-undefined-check"` allows `==` and `!=` when comparing to `undefined`. -options: - type: array - items: - type: string - enum: - - allow-null-check - - allow-undefined-check - minLength: 0 - maxLength: 2 -optionExamples: - - 'true' - - '[true, "allow-null-check"]' - - '[true, "allow-undefined-check"]' -type: functionality -typescriptOnly: false -layout: rule -title: 'Rule: triple-equals' -optionsJSON: |- - { - "type": "array", - "items": { - "type": "string", - "enum": [ - "allow-null-check", - "allow-undefined-check" - ] - }, - "minLength": 0, - "maxLength": 2 - } ---- \ No newline at end of file diff --git a/docs/rules/typedef-whitespace/index.html b/docs/rules/typedef-whitespace/index.html deleted file mode 100644 index 8fcd3b63087..00000000000 --- a/docs/rules/typedef-whitespace/index.html +++ /dev/null @@ -1,160 +0,0 @@ ---- -ruleName: typedef-whitespace -description: Requires or disallows whitespace for type definitions. -descriptionDetails: Determines if a space is required or not before the colon in a type specifier. -optionsDescription: |- - - Two arguments which are both objects. - The first argument specifies how much space should be to the _left_ of a typedef colon. - The second argument specifies how much space should be to the _right_ of a typedef colon. - Each key should have a value of `"space"` or `"nospace"`. - Possible keys are: - - * `"call-signature"` checks return type of functions. - * `"index-signature"` checks index type specifier of indexers. - * `"parameter"` checks function parameters. - * `"property-declaration"` checks object property declarations. - * `"variable-declaration"` checks variable declaration. -options: - type: array - items: - - &ref_1 - type: object - properties: - call-signature: &ref_0 - type: string - enum: - - nospace - - onespace - - space - index-signature: *ref_0 - parameter: *ref_0 - property-declaration: *ref_0 - variable-declaration: *ref_0 - additionalProperties: false - - *ref_1 - additionalItems: false -optionExamples: - - |- - - [ - true, - { - "call-signature": "nospace", - "index-signature": "nospace", - "parameter": "nospace", - "property-declaration": "nospace", - "variable-declaration": "nospace" - }, - { - "call-signature": "onespace", - "index-signature": "onespace", - "parameter": "onespace", - "property-declaration": "onespace", - "variable-declaration": "onespace" - } - ] -type: typescript -typescriptOnly: true -layout: rule -title: 'Rule: typedef-whitespace' -optionsJSON: |- - { - "type": "array", - "items": [ - { - "type": "object", - "properties": { - "call-signature": { - "type": "string", - "enum": [ - "nospace", - "onespace", - "space" - ] - }, - "index-signature": { - "type": "string", - "enum": [ - "nospace", - "onespace", - "space" - ] - }, - "parameter": { - "type": "string", - "enum": [ - "nospace", - "onespace", - "space" - ] - }, - "property-declaration": { - "type": "string", - "enum": [ - "nospace", - "onespace", - "space" - ] - }, - "variable-declaration": { - "type": "string", - "enum": [ - "nospace", - "onespace", - "space" - ] - } - }, - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "call-signature": { - "type": "string", - "enum": [ - "nospace", - "onespace", - "space" - ] - }, - "index-signature": { - "type": "string", - "enum": [ - "nospace", - "onespace", - "space" - ] - }, - "parameter": { - "type": "string", - "enum": [ - "nospace", - "onespace", - "space" - ] - }, - "property-declaration": { - "type": "string", - "enum": [ - "nospace", - "onespace", - "space" - ] - }, - "variable-declaration": { - "type": "string", - "enum": [ - "nospace", - "onespace", - "space" - ] - } - }, - "additionalProperties": false - } - ], - "additionalItems": false - } ---- \ No newline at end of file diff --git a/docs/rules/typedef/index.html b/docs/rules/typedef/index.html deleted file mode 100644 index 67ca7f977f2..00000000000 --- a/docs/rules/typedef/index.html +++ /dev/null @@ -1,53 +0,0 @@ ---- -ruleName: typedef -description: Requires type definitions to exist. -optionsDescription: |- - - Seven arguments may be optionally provided: - - * `"call-signature"` checks return type of functions. - * `"arrow-call-signature"` checks return type of arrow functions. - * `"parameter"` checks type specifier of function parameters for non-arrow functions. - * `"arrow-parameter"` checks type specifier of function parameters for arrow functions. - * `"property-declaration"` checks return types of interface properties. - * `"variable-declaration"` checks variable declarations. - * `"member-variable-declaration"` checks member variable declarations. -options: - type: array - items: - type: string - enum: - - call-signature - - arrow-call-signature - - parameter - - arrow-parameter - - property-declaration - - variable-declaration - - member-variable-declaration - minLength: 0 - maxLength: 7 -optionExamples: - - '[true, "call-signature", "parameter", "member-variable-declaration"]' -type: typescript -typescriptOnly: true -layout: rule -title: 'Rule: typedef' -optionsJSON: |- - { - "type": "array", - "items": { - "type": "string", - "enum": [ - "call-signature", - "arrow-call-signature", - "parameter", - "arrow-parameter", - "property-declaration", - "variable-declaration", - "member-variable-declaration" - ] - }, - "minLength": 0, - "maxLength": 7 - } ---- \ No newline at end of file diff --git a/docs/rules/typeof-compare/index.html b/docs/rules/typeof-compare/index.html deleted file mode 100644 index a879dcc2679..00000000000 --- a/docs/rules/typeof-compare/index.html +++ /dev/null @@ -1,13 +0,0 @@ ---- -ruleName: typeof-compare -description: Makes sure result of `typeof` is compared to correct string values -optionsDescription: Not configurable. -options: null -optionExamples: - - 'true' -type: functionality -typescriptOnly: false -layout: rule -title: 'Rule: typeof-compare' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/unified-signatures/index.html b/docs/rules/unified-signatures/index.html deleted file mode 100644 index 39e0a1c7eea..00000000000 --- a/docs/rules/unified-signatures/index.html +++ /dev/null @@ -1,13 +0,0 @@ ---- -ruleName: unified-signatures -description: Warns for any two overloads that could be unified into one by using a union or an optional/rest parameter. -optionsDescription: Not configurable. -options: null -optionExamples: - - 'true' -type: typescript -typescriptOnly: true -layout: rule -title: 'Rule: unified-signatures' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/use-isnan/index.html b/docs/rules/use-isnan/index.html deleted file mode 100644 index 848ec439738..00000000000 --- a/docs/rules/use-isnan/index.html +++ /dev/null @@ -1,17 +0,0 @@ ---- -ruleName: use-isnan -description: Enforces use of the `isNaN()` function to check for NaN references instead of a comparison to the `NaN` constant. -rationale: |- - - Since `NaN !== NaN`, comparisons with regular operators will produce unexpected results. - So, instead of `if (myVar === NaN)`, do `if (isNaN(myVar))`. -optionsDescription: Not configurable. -options: null -optionExamples: - - 'true' -type: functionality -typescriptOnly: false -layout: rule -title: 'Rule: use-isnan' -optionsJSON: 'null' ---- \ No newline at end of file diff --git a/docs/rules/variable-name/index.html b/docs/rules/variable-name/index.html deleted file mode 100644 index 0808782623f..00000000000 --- a/docs/rules/variable-name/index.html +++ /dev/null @@ -1,48 +0,0 @@ ---- -ruleName: variable-name -description: Checks variable names for various errors. -optionsDescription: |- - - Five arguments may be optionally provided: - - * `"check-format"`: allows only camelCased or UPPER_CASED variable names - * `"allow-leading-underscore"` allows underscores at the beginning (only has an effect if "check-format" specified) - * `"allow-trailing-underscore"` allows underscores at the end. (only has an effect if "check-format" specified) - * `"allow-pascal-case"` allows PascalCase in addtion to camelCase. - * `"ban-keywords"`: disallows the use of certain TypeScript keywords (`any`, `Number`, `number`, `String`, - `string`, `Boolean`, `boolean`, `undefined`) as variable or parameter names. -options: - type: array - items: - type: string - enum: - - check-format - - allow-leading-underscore - - allow-trailing-underscore - - allow-pascal-case - - ban-keywords - minLength: 0 - maxLength: 5 -optionExamples: - - '[true, "ban-keywords", "check-format", "allow-leading-underscore"]' -type: style -typescriptOnly: false -layout: rule -title: 'Rule: variable-name' -optionsJSON: |- - { - "type": "array", - "items": { - "type": "string", - "enum": [ - "check-format", - "allow-leading-underscore", - "allow-trailing-underscore", - "allow-pascal-case", - "ban-keywords" - ] - }, - "minLength": 0, - "maxLength": 5 - } ---- \ No newline at end of file diff --git a/docs/rules/whitespace/index.html b/docs/rules/whitespace/index.html deleted file mode 100644 index c3f685c206a..00000000000 --- a/docs/rules/whitespace/index.html +++ /dev/null @@ -1,57 +0,0 @@ ---- -ruleName: whitespace -description: Enforces whitespace style conventions. -rationale: 'Helps maintain a readable, consistent style in your codebase.' -optionsDescription: |- - - Eight arguments may be optionally provided: - - * `"check-branch"` checks branching statements (`if`/`else`/`for`/`while`) are followed by whitespace. - * `"check-decl"`checks that variable declarations have whitespace around the equals token. - * `"check-operator"` checks for whitespace around operator tokens. - * `"check-module"` checks for whitespace in import & export statements. - * `"check-separator"` checks for whitespace after separator tokens (`,`/`;`). - * `"check-type"` checks for whitespace before a variable type specification. - * `"check-typecast"` checks for whitespace between a typecast and its target. - * `"check-preblock"` checks for whitespace before the opening brace of a block -options: - type: array - items: - type: string - enum: - - check-branch - - check-decl - - check-operator - - check-module - - check-separator - - check-type - - check-typecast - - check-preblock - minLength: 0 - maxLength: 7 -optionExamples: - - '[true, "check-branch", "check-operator", "check-typecast"]' -type: style -typescriptOnly: false -layout: rule -title: 'Rule: whitespace' -optionsJSON: |- - { - "type": "array", - "items": { - "type": "string", - "enum": [ - "check-branch", - "check-decl", - "check-operator", - "check-module", - "check-separator", - "check-type", - "check-typecast", - "check-preblock" - ] - }, - "minLength": 0, - "maxLength": 7 - } ---- \ No newline at end of file From d119274af72f1f53e1621c1023db6520617057a3 Mon Sep 17 00:00:00 2001 From: Noah Chen Date: Wed, 18 Jan 2017 15:51:32 -0500 Subject: [PATCH 035/131] Fix buildDocs to create directory (#2072) --- scripts/buildDocs.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/buildDocs.ts b/scripts/buildDocs.ts index 937928927b5..3333be4b698 100644 --- a/scripts/buildDocs.ts +++ b/scripts/buildDocs.ts @@ -132,6 +132,9 @@ function buildSingleModuleDocumentation(documentation: IDocumentation, modulePat // Ensure a directory exists and write the module's file. const moduleName = (metadata as any)[documentation.nameMetadataKey]; const fileDirectory = path.join(documentation.subDirectory, moduleName); + if (!fs.existsSync(documentation.subDirectory)) { + fs.mkdirSync(documentation.subDirectory); + } if (!fs.existsSync(fileDirectory)) { fs.mkdirSync(fileDirectory); } From b6171dd731effe4bcc5308a2279901ae6ec11122 Mon Sep 17 00:00:00 2001 From: Klaus Meinhardt Date: Wed, 18 Jan 2017 22:29:06 +0100 Subject: [PATCH 036/131] Allow empty interfaces with 2 or more parent (#2070) Fixes: #2065 --- src/rules/noEmptyInterfaceRule.ts | 7 +++++-- test/rules/no-empty-interface/test.ts.lint | 5 +++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/rules/noEmptyInterfaceRule.ts b/src/rules/noEmptyInterfaceRule.ts index 844ee5611b9..fe29e91b491 100644 --- a/src/rules/noEmptyInterfaceRule.ts +++ b/src/rules/noEmptyInterfaceRule.ts @@ -42,9 +42,12 @@ export class Rule extends Lint.Rules.AbstractRule { class Walker extends Lint.RuleWalker { public visitInterfaceDeclaration(node: ts.InterfaceDeclaration) { - if (node.members.length === 0) { + if (node.members.length === 0 && + (node.heritageClauses === undefined || + node.heritageClauses[0].types === undefined || + // allow interfaces that extend 2 or more interfaces + node.heritageClauses[0].types!.length < 2)) { this.addFailureAtNode(node.name, node.heritageClauses ? Rule.FAILURE_STRING_FOR_EXTENDS : Rule.FAILURE_STRING); } - super.visitInterfaceDeclaration(node); } } diff --git a/test/rules/no-empty-interface/test.ts.lint b/test/rules/no-empty-interface/test.ts.lint index 3fb5872674c..b60398d65c8 100644 --- a/test/rules/no-empty-interface/test.ts.lint +++ b/test/rules/no-empty-interface/test.ts.lint @@ -5,3 +5,8 @@ interface J extends I { } ~ [An interface declaring no members is equivalent to its supertype.] interface K { x: number; } + +interface L extends J, K {} // extending more than one interface is ok, as it can be used instead of intersection types + +interface M extends {} // don't crash on empty extends list + ~ [An interface declaring no members is equivalent to its supertype.] From c9a63d9a116803b2d0152b1ed31dc4d660aadb6c Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Wed, 18 Jan 2017 19:59:14 -0800 Subject: [PATCH 037/131] Added no-floating-promises rule (#1632) It keeps a small blacklist of places functions that return Promises aren't allowed to be. --- src/rules/noFloatingPromisesRule.ts | 104 +++++++++++++ .../jquerypromise/test.ts.lint | 17 +++ .../jquerypromise/tslint.json | 8 + .../promises/test.ts.lint | 141 ++++++++++++++++++ .../no-floating-promises/promises/tslint.json | 8 + 5 files changed, 278 insertions(+) create mode 100644 src/rules/noFloatingPromisesRule.ts create mode 100644 test/rules/no-floating-promises/jquerypromise/test.ts.lint create mode 100644 test/rules/no-floating-promises/jquerypromise/tslint.json create mode 100644 test/rules/no-floating-promises/promises/test.ts.lint create mode 100644 test/rules/no-floating-promises/promises/tslint.json diff --git a/src/rules/noFloatingPromisesRule.ts b/src/rules/noFloatingPromisesRule.ts new file mode 100644 index 00000000000..b5612ace3f2 --- /dev/null +++ b/src/rules/noFloatingPromisesRule.ts @@ -0,0 +1,104 @@ +/** + * @license + * Copyright 2016 Palantir Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as ts from "typescript"; + +import * as Lint from "../index"; + +export class Rule extends Lint.Rules.TypedRule { + /* tslint:disable:object-literal-sort-keys */ + public static metadata: Lint.IRuleMetadata = { + ruleName: "no-floating-promises", + description: "Promises returned by functions must be handled appropriately.", + optionsDescription: Lint.Utils.dedent` + A list of \'string\' names of any additional classes that should also be handled as Promises. + `, + options: { + type: "list", + listType: { + type: "array", + items: {type: "string"}, + }, + }, + optionExamples: ["true", `[true, "JQueryPromise"]`], + rationale: "Unhandled Promises can cause unexpected behavior, such as resolving at unexpected times.", + type: "functionality", + typescriptOnly: false, + }; + /* tslint:enable:object-literal-sort-keys */ + + public static FAILURE_STRING = "Promises must be handled appropriately"; + + public applyWithProgram(sourceFile: ts.SourceFile, langSvc: ts.LanguageService): Lint.RuleFailure[] { + const walker = new NoFloatingPromisesWalker(sourceFile, this.getOptions(), langSvc.getProgram()); + + for (const className of this.getOptions().ruleArguments) { + walker.addPromiseClass(className); + } + + return this.applyWithWalker(walker); + } +} + +class NoFloatingPromisesWalker extends Lint.ProgramAwareRuleWalker { + private static barredParentKinds: { [x: number]: boolean } = { + [ts.SyntaxKind.Block]: true, + [ts.SyntaxKind.ExpressionStatement]: true, + [ts.SyntaxKind.SourceFile]: true, + }; + + private promiseClasses = ["Promise"]; + + public addPromiseClass(className: string) { + this.promiseClasses.push(className); + } + + public visitCallExpression(node: ts.CallExpression): void { + this.checkNode(node); + super.visitCallExpression(node); + } + + public visitExpressionStatement(node: ts.ExpressionStatement): void { + this.checkNode(node); + super.visitExpressionStatement(node); + } + + private checkNode(node: ts.CallExpression | ts.ExpressionStatement) { + if (node.parent && this.kindCanContainPromise(node.parent.kind)) { + return; + } + + const typeChecker = this.getTypeChecker(); + const type = typeChecker.getTypeAtLocation(node); + + if (this.symbolIsPromise(type.symbol)) { + this.addFailure(this.createFailure(node.getStart(), node.getWidth(), Rule.FAILURE_STRING)); + } + } + + private symbolIsPromise(symbol?: ts.Symbol) { + if (!symbol) { + return false; + } + + return this.promiseClasses.indexOf(symbol.name) !== -1; + } + + private kindCanContainPromise(kind: ts.SyntaxKind) { + return !NoFloatingPromisesWalker.barredParentKinds[kind]; + } +} diff --git a/test/rules/no-floating-promises/jquerypromise/test.ts.lint b/test/rules/no-floating-promises/jquerypromise/test.ts.lint new file mode 100644 index 00000000000..4d6096e18c1 --- /dev/null +++ b/test/rules/no-floating-promises/jquerypromise/test.ts.lint @@ -0,0 +1,17 @@ +class Promise { } +class JQueryPromise { } +class NotAPromise { } + +const returnsPromise = () => new Promise(); +const returnsJQueryPromise = () => new JQueryPromise(); +const returnsNotAPromise = () => new NotAPromise(); + +returnsPromise(); +~~~~~~~~~~~~~~~~ [0] + +returnsJQueryPromise(); +~~~~~~~~~~~~~~~~~~~~~~ [0] + +returnsNotAPromise(); + +[0]: Promises must be handled appropriately diff --git a/test/rules/no-floating-promises/jquerypromise/tslint.json b/test/rules/no-floating-promises/jquerypromise/tslint.json new file mode 100644 index 00000000000..f207ba50df6 --- /dev/null +++ b/test/rules/no-floating-promises/jquerypromise/tslint.json @@ -0,0 +1,8 @@ +{ + "linterOptions": { + "typeCheck": true + }, + "rules": { + "no-floating-promises": [true, "JQueryPromise"] + } +} diff --git a/test/rules/no-floating-promises/promises/test.ts.lint b/test/rules/no-floating-promises/promises/test.ts.lint new file mode 100644 index 00000000000..baeb2b5c3dd --- /dev/null +++ b/test/rules/no-floating-promises/promises/test.ts.lint @@ -0,0 +1,141 @@ +class Promise { + then(): Promise; +} + +function returnsPromiseFunction() { + return new Promise(); +} + +const returnsPromiseVariable = () => new Promise(); + +class ReturnsPromiseClass { + returnsPromiseMemberFunction() { + return new Promise(); + } + + returnsPromiseMemberVariable = () => new Promise(); + + static returnsPromiseStaticFunction = () => new Promise(); +} + +let a = returnsPromiseFunction(); +let b = returnsPromiseVariable(); +let c = new ReturnsPromiseClass().returnsPromiseMemberFunction(); +let d = new ReturnsPromiseClass().returnsPromiseMemberVariable(); +let e = ReturnsPromiseClass.returnsPromiseStaticFunction(); + +returnsPromiseFunction(); +~~~~~~~~~~~~~~~~~~~~~~~~ [0] + +returnsPromiseFunction().then(); +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + +returnsPromiseVariable(); +~~~~~~~~~~~~~~~~~~~~~~~~ [0] + +returnsPromiseVariable().then(); +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + +new ReturnsPromiseClass().returnsPromiseMemberFunction(); +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + +new ReturnsPromiseClass().returnsPromiseMemberFunction().then(); +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + +new ReturnsPromiseClass().returnsPromiseMemberVariable(); +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + +new ReturnsPromiseClass().returnsPromiseMemberVariable().then(); +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + +ReturnsPromiseClass.returnsPromiseStaticFunction(); +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + +ReturnsPromiseClass.returnsPromiseStaticFunction().then(); +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + +(function () { + let a = returnsPromiseFunction(); + + returnsPromiseFunction(); + ~~~~~~~~~~~~~~~~~~~~~~~~ [0] + + returnsPromiseFunction().then(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + + new ReturnsPromiseClass().returnsPromiseMemberFunction(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + + new ReturnsPromiseClass().returnsPromiseMemberFunction().then(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + + new ReturnsPromiseClass().returnsPromiseMemberVariable(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + + new ReturnsPromiseClass().returnsPromiseMemberVariable().then(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + + ReturnsPromiseClass.returnsPromiseStaticFunction(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + + ReturnsPromiseClass.returnsPromiseStaticFunction().then(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] +})(); + +(() => { + let a = returnsPromiseFunction(); + + returnsPromiseFunction(); + ~~~~~~~~~~~~~~~~~~~~~~~~ [0] + + returnsPromiseFunction().then(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + + new ReturnsPromiseClass().returnsPromiseMemberFunction(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + + new ReturnsPromiseClass().returnsPromiseMemberFunction().then(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + + new ReturnsPromiseClass().returnsPromiseMemberVariable(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + + new ReturnsPromiseClass().returnsPromiseMemberVariable().then(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + + ReturnsPromiseClass.returnsPromiseStaticFunction(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + + ReturnsPromiseClass.returnsPromiseStaticFunction().then(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] +})(); + +[].push(returnsPromiseFunction()); + +[].push(returnsPromiseFunction().then()); + +[].push(ReturnsPromiseClass.returnsPromiseStaticFunction()); + +[].push(ReturnsPromiseClass.returnsPromiseStaticFunction().then()); + +while (returnsPromiseFunction()); + +while (returnsPromiseFunction().then()); + +for (let i = 0; i < returnsPromiseFunction(); i += 1); + +for (let i = 0; i < returnsPromiseFunction().then(); i += 1); + +for (let i in returnsPromiseFunction()); + +for (let i in returnsPromiseFunction().then()); + +for (const promise of returnsPromiseFunction()); + +for (const promise of returnsPromiseFunction().then()); + +let promise = Math.random() > .5 + ? returnsPromiseFunction() + : returnsPromiseFunction().then(); + +[0]: Promises must be handled appropriately \ No newline at end of file diff --git a/test/rules/no-floating-promises/promises/tslint.json b/test/rules/no-floating-promises/promises/tslint.json new file mode 100644 index 00000000000..2f3715da4c5 --- /dev/null +++ b/test/rules/no-floating-promises/promises/tslint.json @@ -0,0 +1,8 @@ +{ + "linterOptions": { + "typeCheck": true + }, + "rules": { + "no-floating-promises": true + } +} \ No newline at end of file From a1072794b7ac6c62824c0042b828830401a39a9e Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Wed, 18 Jan 2017 20:00:28 -0800 Subject: [PATCH 038/131] Add `no-unsafe-any` rule (#2047) --- src/rules/noUnsafeAnyRule.ts | 119 ++++++++++++++++++++++++++ test/rules/no-unsafe-any/test.ts.lint | 54 ++++++++++++ test/rules/no-unsafe-any/tslint.json | 8 ++ 3 files changed, 181 insertions(+) create mode 100644 src/rules/noUnsafeAnyRule.ts create mode 100644 test/rules/no-unsafe-any/test.ts.lint create mode 100644 test/rules/no-unsafe-any/tslint.json diff --git a/src/rules/noUnsafeAnyRule.ts b/src/rules/noUnsafeAnyRule.ts new file mode 100644 index 00000000000..0fb0344b30d --- /dev/null +++ b/src/rules/noUnsafeAnyRule.ts @@ -0,0 +1,119 @@ +/** + * @license + * Copyright 2017 Palantir Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as ts from "typescript"; +import * as Lint from "../index"; + +export class Rule extends Lint.Rules.TypedRule { + /* tslint:disable:object-literal-sort-keys */ + public static metadata: Lint.IRuleMetadata = { + ruleName: "no-unsafe-any", + description: Lint.Utils.dedent` + Warns when using an expression of type 'any' in an unsafe way. + Type casts and tests are allowed. + Expressions that work on all values (such as '"" + x') are allowed.`, + optionsDescription: "Not configurable.", + options: null, + optionExamples: ["true"], + type: "functionality", + typescriptOnly: true, + requiresTypeInfo: true, + }; + /* tslint:enable:object-literal-sort-keys */ + + public static FAILURE_STRING = "Unsafe use of expression of type 'any'."; + + public applyWithProgram(srcFile: ts.SourceFile, langSvc: ts.LanguageService): Lint.RuleFailure[] { + return this.applyWithWalker(new Walker(srcFile, this.getOptions(), langSvc.getProgram())); + } +} + +class Walker extends Lint.ProgramAwareRuleWalker { + public visitNode(node: ts.Node) { + if (ts.isExpression(node) && isAny(this.getType(node)) && !this.isAllowedLocation(node)) { + this.addFailureAtNode(node, Rule.FAILURE_STRING); + } else { + super.visitNode(node); + } + } + + private isAllowedLocation(node: ts.Expression): boolean { + const parent = node.parent!; + switch (parent.kind) { + case ts.SyntaxKind.ExpressionStatement: // Allow unused expression + case ts.SyntaxKind.Parameter: // Allow to declare a parameter of type 'any' + case ts.SyntaxKind.TypeOfExpression: // Allow test + case ts.SyntaxKind.TemplateSpan: // Allow stringification (works on all values) + // Allow casts + case ts.SyntaxKind.TypeAssertionExpression: + case ts.SyntaxKind.AsExpression: + return true; + + // OK to pass 'any' to a function that takes 'any' as its argument + case ts.SyntaxKind.CallExpression: + case ts.SyntaxKind.NewExpression: + return isAny(this.getTypeChecker().getContextualType(node)); + + case ts.SyntaxKind.BinaryExpression: + const { left, right, operatorToken } = parent as ts.BinaryExpression; + switch (operatorToken.kind) { + case ts.SyntaxKind.InstanceOfKeyword: // Allow test + // Allow equality since all values support equality. + case ts.SyntaxKind.EqualsEqualsToken: + case ts.SyntaxKind.EqualsEqualsEqualsToken: + case ts.SyntaxKind.ExclamationEqualsToken: + case ts.SyntaxKind.ExclamationEqualsEqualsToken: + return true; + case ts.SyntaxKind.PlusToken: // Allow stringification + return node === left ? this.isStringLike(right) : this.isStringLike(left); + case ts.SyntaxKind.PlusEqualsToken: // Allow stringification in `str += x;`, but not `x += str;`. + return node === right && this.isStringLike(left); + default: + return false; + } + + // Allow `const x = foo;`, but not `const x: Foo = foo;`. + case ts.SyntaxKind.VariableDeclaration: + return Lint.hasModifier(parent.parent!.parent!.modifiers, ts.SyntaxKind.DeclareKeyword) || + (parent as ts.VariableDeclaration).type === undefined; + + case ts.SyntaxKind.PropertyAccessExpression: + // Don't warn for right hand side; this is redundant if we warn for the left-hand side. + return (parent as ts.PropertyAccessExpression).name === node; + + default: + return false; + } + } + + private isStringLike(node: ts.Expression) { + return Lint.isTypeFlagSet(this.getType(node), ts.TypeFlags.StringLike); + } + + private getType(node: ts.Expression): ts.Type { + return this.getTypeChecker().getTypeAtLocation(node); + } +} + +function isAny(type: ts.Type | undefined): boolean { + return type !== undefined && Lint.isTypeFlagSet(type, ts.TypeFlags.Any); +} + +// This is marked @internal, but we need it! +declare module "typescript" { + export function isExpression(node: ts.Node): node is ts.Expression; +} diff --git a/test/rules/no-unsafe-any/test.ts.lint b/test/rules/no-unsafe-any/test.ts.lint new file mode 100644 index 00000000000..7222f2ddb10 --- /dev/null +++ b/test/rules/no-unsafe-any/test.ts.lint @@ -0,0 +1,54 @@ +declare const x: any; + +function f(x: any) { + x.foo; + ~ [0] + x(0); + ~ [0] + x``; + ~ [0] + x + 3; + ~ [0] + + // OK to pass it to a function that takes `any` + g(x); + // Not OK to pass to any other function. + [].map(x); + ~ [0] + + // Same for constructors + new X(x); + new Y(x); + ~ [0] +} + +class X { constructor(x: any) {} } +class Y { constructor(x: number) {} } + +function g(x: any): string { + if (x === undefined) { + return "undefined"; + } + if (typeof x === "string") { + return `string: ${x}`; + } + if (x instanceof Array) { + // Once it's been tested, OK to use it as an array. + return `Array, length ${x.length}`; + } + if (Math.random() > 0.5) { + // Allow explicit cast + return ( x).toLowerCase() + (x as string).toUpperCase(); + } + + "" + x; + x + ""; + let s = ""; + s += x; + x += s; + ~ [0] + + return `other: ${x}`; +} + +[0]: Unsafe use of expression of type 'any'. diff --git a/test/rules/no-unsafe-any/tslint.json b/test/rules/no-unsafe-any/tslint.json new file mode 100644 index 00000000000..85a9290c2e6 --- /dev/null +++ b/test/rules/no-unsafe-any/tslint.json @@ -0,0 +1,8 @@ +{ + "linterOptions": { + "typeCheck": true + }, + "rules": { + "no-unsafe-any": true + } +} From 2ce15c7b83cd8581d0f0d6763e6a12d4e0b91886 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Wed, 18 Jan 2017 22:23:44 -0800 Subject: [PATCH 039/131] Add `strict-type-predicates` rule (#2046) --- src/rules/strictTypePredicatesRule.ts | 280 ++++++++++++++++++ .../rules/strict-type-predicates/test.ts.lint | 186 ++++++++++++ .../strict-type-predicates/tsconfig.json | 5 + test/rules/strict-type-predicates/tslint.json | 8 + 4 files changed, 479 insertions(+) create mode 100644 src/rules/strictTypePredicatesRule.ts create mode 100644 test/rules/strict-type-predicates/test.ts.lint create mode 100644 test/rules/strict-type-predicates/tsconfig.json create mode 100644 test/rules/strict-type-predicates/tslint.json diff --git a/src/rules/strictTypePredicatesRule.ts b/src/rules/strictTypePredicatesRule.ts new file mode 100644 index 00000000000..8dbe8da5f68 --- /dev/null +++ b/src/rules/strictTypePredicatesRule.ts @@ -0,0 +1,280 @@ +/** + * @license + * Copyright 2017 Palantir Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as ts from "typescript"; +import * as Lint from "../index"; + +// tslint:disable:no-bitwise + +export class Rule extends Lint.Rules.TypedRule { + /* tslint:disable:object-literal-sort-keys */ + public static metadata: Lint.IRuleMetadata = { + ruleName: "strict-type-predicates", + description: Lint.Utils.dedent` + Warns for type predicates that are always true or always false. + Works for 'typeof' comparisons to constants (e.g. 'typeof foo === "string"'), and equality comparison to 'null'/'undefined'. + (TypeScript won't let you compare '1 === 2', but it has an exception for '1 === undefined'.) + Does not yet work for 'instanceof'. + Does *not* warn for 'if (x.y)' where 'x.y' is always truthy. For that, see strict-boolean-expressions.`, + optionsDescription: "Not configurable.", + options: null, + optionExamples: ["true"], + type: "functionality", + typescriptOnly: true, + requiresTypeInfo: true, + }; + /* tslint:enable:object-literal-sort-keys */ + + public static FAILURE_STRING_BAD_TYPEOF = "Bad comparison for 'typeof'."; + + public static FAILURE_STRING(value: boolean): string { + return `Expression is always ${value}.`; + } + + public static FAILURE_STRICT_PREFER_STRICT_EQUALS(value: "null" | "undefined", isPositive: boolean) { + return `Use '${isPositive ? "===" : "!=="} ${value}' instead.`; + } + + public applyWithProgram(srcFile: ts.SourceFile, langSvc: ts.LanguageService): Lint.RuleFailure[] { + return this.applyWithWalker(new Walker(srcFile, this.getOptions(), langSvc.getProgram())); + } +} + +class Walker extends Lint.ProgramAwareRuleWalker { + public visitBinaryExpression(node: ts.BinaryExpression) { + const equals = getEquals(node.operatorToken.kind); + if (equals) { + this.checkEquals(node, equals); + } + super.visitBinaryExpression(node); + } + + private checkEquals(node: ts.BinaryExpression, { isStrict, isPositive }: Equals) { + const exprPred = getTypePredicate(node, isStrict); + if (!exprPred) { + return; + } + + const fail = (failure: string) => this.addFailureAtNode(node, failure); + + if (exprPred.kind === TypePredicateKind.TypeofTypo) { + fail(Rule.FAILURE_STRING_BAD_TYPEOF); + return; + } + + const checker = this.getTypeChecker(); + const exprType = checker.getTypeAtLocation(exprPred.expression); + // TODO: could use checker.getBaseConstraintOfType to help with type parameters, but it's not publicly exposed. + if (Lint.isTypeFlagSet(exprType, ts.TypeFlags.Any | ts.TypeFlags.TypeParameter)) { + return; + } + + switch (exprPred.kind) { // tslint:disable-line:switch-default + case TypePredicateKind.Plain: + const { predicate, isNullOrUndefined } = exprPred; + const value = getConstantBoolean(exprType, predicate); + // 'null'/'undefined' are the only two values *not* assignable to '{}'. + if (value !== undefined && (isNullOrUndefined || !isEmptyType(checker, exprType))) { + fail(Rule.FAILURE_STRING(value === isPositive)); + } + break; + + case TypePredicateKind.NonStructNullUndefined: + const result = testNonStrictNullUndefined(exprType); + switch (typeof result) { + case "boolean": + fail(Rule.FAILURE_STRING(result === isPositive)); + break; + + case "string": + fail(Rule.FAILURE_STRICT_PREFER_STRICT_EQUALS(result as "null" | "undefined", isPositive)); + break; + + default: + } + } + + } +} + +/** Detects a type predicate given `left === right`. */ +function getTypePredicate(node: ts.BinaryExpression, isStrictEquals: boolean): TypePredicate | undefined { + const { left, right } = node; + return getTypePredicateOneWay(left, right, isStrictEquals) || getTypePredicateOneWay(right, left, isStrictEquals); +} + +/** Only gets the type predicate if the expression is on the left. */ +function getTypePredicateOneWay(left: ts.Expression, right: ts.Expression, isStrictEquals: boolean): TypePredicate | undefined { + switch (right.kind) { + case ts.SyntaxKind.TypeOfExpression: + const expression = (right as ts.TypeOfExpression).expression; + const kind = left.kind === ts.SyntaxKind.StringLiteral ? (left as ts.StringLiteral).text : ""; + const predicate = getTypePredicateForKind(kind); + return predicate === undefined + ? { kind: TypePredicateKind.TypeofTypo } + : { kind: TypePredicateKind.Plain, expression, predicate, isNullOrUndefined: kind === "undefined" }; + + case ts.SyntaxKind.NullKeyword: + return nullOrUndefined(ts.TypeFlags.Null); + + case ts.SyntaxKind.Identifier: + if ((right as ts.Identifier).text === "undefined") { + return nullOrUndefined(undefinedFlags); + } + + default: + return undefined; + } + + function nullOrUndefined(flags: ts.TypeFlags): TypePredicate { + return isStrictEquals + ? { kind: TypePredicateKind.Plain, expression: left, predicate: flagPredicate(flags), isNullOrUndefined: true } + : { kind: TypePredicateKind.NonStructNullUndefined, expression: left }; + } +} + +function isEmptyType(checker: ts.TypeChecker, type: ts.Type) { + return checker.typeToString(type) === "{}"; +} + +const undefinedFlags = ts.TypeFlags.Undefined | ts.TypeFlags.Void; + +type TypePredicate = PlainTypePredicate | NonStrictNullUndefinedPredicate | { kind: TypePredicateKind.TypeofTypo }; +interface PlainTypePredicate { + kind: TypePredicateKind.Plain; + expression: ts.Expression; + predicate: Predicate; + isNullOrUndefined: boolean; +} +/** For `== null` and the like. */ +interface NonStrictNullUndefinedPredicate { + kind: TypePredicateKind.NonStructNullUndefined; + expression: ts.Expression; +} +const enum TypePredicateKind { + Plain, + NonStructNullUndefined, + TypeofTypo, +} + +type Predicate = (type: ts.Type) => boolean; + +function getTypePredicateForKind(kind: string): Predicate | undefined { + switch (kind) { + case "undefined": + return flagPredicate(undefinedFlags); + case "boolean": + return flagPredicate(ts.TypeFlags.BooleanLike); + case "number": + return flagPredicate(ts.TypeFlags.NumberLike); + case "string": + return flagPredicate(ts.TypeFlags.StringLike); + case "symbol": + return flagPredicate(ts.TypeFlags.ESSymbol); + case "function": + return isFunction; + case "object": + // It's an object if it's not any of the above. + const allFlags = ts.TypeFlags.Undefined | ts.TypeFlags.Void | ts.TypeFlags.BooleanLike | + ts.TypeFlags.NumberLike | ts.TypeFlags.StringLike | ts.TypeFlags.ESSymbol; + return (type) => !Lint.isTypeFlagSet(type, allFlags) && !isFunction(type); + default: + return undefined; + } +} + +function flagPredicate(testedFlag: ts.TypeFlags): Predicate { + return (type) => Lint.isTypeFlagSet(type, testedFlag); +} + +function isFunction(t: ts.Type): boolean { + if (t.getCallSignatures().length !== 0) { + return true; + } + const symbol = t.getSymbol(); + return (symbol && symbol.getName()) === "Function"; +} + +/** Returns a boolean value if that should always be the result of a type predicate. */ +function getConstantBoolean(type: ts.Type, predicate: (t: ts.Type) => boolean): boolean | undefined { + let anyTrue = false; + let anyFalse = false; + for (const ty of unionParts(type)) { + if (predicate(ty)) { + anyTrue = true; + } else { + anyFalse = true; + } + + if (anyTrue && anyFalse) { + return undefined; + } + } + + return anyTrue; +} + +/** Returns bool for always/never true, or a string to recommend strict equality. */ +function testNonStrictNullUndefined(type: ts.Type): boolean | string | undefined { + let anyNull = false; + let anyUndefined = false; + let anyOther = false; + for (const ty of unionParts(type)) { + if (Lint.isTypeFlagSet(ty, ts.TypeFlags.Null)) { + anyNull = true; + } else if (Lint.isTypeFlagSet(ty, undefinedFlags)) { + anyUndefined = true; + } else { + anyOther = true; + } + } + + return !anyOther ? true + : anyNull && anyUndefined ? undefined + : anyNull ? "null" + : anyUndefined ? "undefined" + : false; +} + +interface Equals { + isPositive: boolean; // True for "===" and "==" + isStrict: boolean; // True for "===" and "!==" +} + +function getEquals(kind: ts.SyntaxKind): Equals | undefined { + switch (kind) { + case ts.SyntaxKind.EqualsEqualsToken: + return { isPositive: true, isStrict: false }; + case ts.SyntaxKind.EqualsEqualsEqualsToken: + return { isPositive: true, isStrict: true }; + case ts.SyntaxKind.ExclamationEqualsToken: + return { isPositive: false, isStrict: false }; + case ts.SyntaxKind.ExclamationEqualsEqualsToken: + return { isPositive: false, isStrict: true }; + default: + return undefined; + } +} + +function unionParts(type: ts.Type) { + return isUnionType(type) ? type.types : [type]; +} + +/** Type predicate to test for a union type. */ +function isUnionType(type: ts.Type): type is ts.UnionType { + return Lint.isTypeFlagSet(type, ts.TypeFlags.Union); +} diff --git a/test/rules/strict-type-predicates/test.ts.lint b/test/rules/strict-type-predicates/test.ts.lint new file mode 100644 index 00000000000..fd834651874 --- /dev/null +++ b/test/rules/strict-type-predicates/test.ts.lint @@ -0,0 +1,186 @@ +declare function get(): T; + +// typeof undefined +{ + typeof get() === "undefined"; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [F] + + typeof get() === "undefined"; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [T] + + typeof get() === "undefined"; + + declare const c: any; + typeof get() === "undefined"; + + // 'undefined' is not assignable to '{}' + typeof get<{}>() === "undefined"; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [F] +} + +// typeof boolean +{ + declare const a: boolean; + typeof get() === "boolean"; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [T] + + typeof get() === "boolean"; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [F] + + typeof get() === "boolean"; + + typeof get<{}>() === "boolean"; +} + +// typeof number +{ + enum E {} + + typeof get() === "number"; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [T] + + typeof get() === "number"; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [F] +} + +// typeof string +{ + typeof get<"abc" | "def">() === "string"; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [T] + + typeof get() === "string"; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [F] + + typeof get<"abc" | undefined>() === "string"; +} + +// typeof symbol +{ + typeof get() === "symbol"; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [T] + + typeof get() === "symbol"; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [F] + + typeof get() === "symbol"; +} + +// typeof function +{ + typeof get<() => void>() === "function"; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [T] + + typeof get() === "function"; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [T] + + + typeof get() === "function"; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [F] + + typeof get void)>() === "function"; +} + +// typeof object +{ + typeof get void) | Function>() === "object"; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [F] + + typeof get<{}> === "object"; +} + +// === null / undefined +{ + get() === null; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [F] + + get() === null; + + get() === undefined; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [F] + + get() === undefined; + + // 'null' and 'undefined' are not assignable to '{}' + + get<{}>() === null; + ~~~~~~~~~~~~~~~~~~ [F] + + get<{}>() === undefined; + ~~~~~~~~~~~~~~~~~~~~~~~ [F] + + get() == null; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Use '=== undefined' instead.] + + get() == undefined; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Use '=== null' instead.] + + get() == null; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Use '=== null' instead.] + + get() == undefined; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Use '=== undefined' instead.] + + get() != null; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Use '!== undefined' instead.] + + get<{}>() == null; + ~~~~~~~~~~~~~~~~~ [F] + + get() == null; + get() != undefined; + + get() == null; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [T] + get() != undefined; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [F] +} + +// negation +{ + get() !== null; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [T] + + get() !== null; + + get() !== undefined; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [T] + + get() !== undefined; + + typeof get() !== "string"; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [F] +} + +// reverse left/right +{ + "string" === typeof get(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [F] + + undefined === get(); + ~~~~~~~~~~~~~~~~~~~~~~~~~ [T] +} + +// type parameters +{ + function f(t: T) { + typeof t === "boolean"; + } + + // TODO: Would be nice to catch this. + function g(t: T) { + typeof t === "boolean"; + } +} + +// Detects bad typeof +{ + typeof get() === true; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [typeof] + + typeof get() === "orbject"; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [typeof] +} + +[T]: Expression is always true. +[F]: Expression is always false. +[typeof]: Bad comparison for 'typeof'. diff --git a/test/rules/strict-type-predicates/tsconfig.json b/test/rules/strict-type-predicates/tsconfig.json new file mode 100644 index 00000000000..1cc3bc85ee9 --- /dev/null +++ b/test/rules/strict-type-predicates/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "strictNullChecks": true + } +} \ No newline at end of file diff --git a/test/rules/strict-type-predicates/tslint.json b/test/rules/strict-type-predicates/tslint.json new file mode 100644 index 00000000000..268b470e4be --- /dev/null +++ b/test/rules/strict-type-predicates/tslint.json @@ -0,0 +1,8 @@ +{ + "linterOptions": { + "typeCheck": true + }, + "rules": { + "strict-type-predicates": true + } +} From 18e760481438404ccc6dc6edef0a6b4bc8e9a9e9 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Wed, 18 Jan 2017 22:24:06 -0800 Subject: [PATCH 040/131] member-ordering: Support categories (#2041) --- src/rules/memberOrderingRule.ts | 442 +++++++++++------- .../custom-categories/test.ts.lint | 10 + .../custom-categories/tslint.json | 18 + .../{order => }/custom/test.ts.lint | 0 .../{order => }/custom/tslint.json | 0 .../{order => }/fields-first/test.ts.lint | 0 .../{order => }/fields-first/tslint.json | 0 .../instance-sandwich/test.ts.lint | 0 .../{order => }/instance-sandwich/tslint.json | 0 .../omit-access-modifier/test.ts.lint | 12 + .../omit-access-modifier/tslint.json | 9 + .../member-ordering/private/test.ts.lint | 10 - .../public-before-private/test.ts.lint | 10 + .../tslint.json | 0 .../static-before-instance/test.ts.lint | 10 + .../tslint.json | 0 .../rules/member-ordering/static/test.ts.lint | 10 - .../{order => }/statics-first/test.ts.lint | 0 .../{order => }/statics-first/tslint.json | 0 .../test.ts.lint | 3 +- .../tslint.json | 0 21 files changed, 347 insertions(+), 187 deletions(-) create mode 100644 test/rules/member-ordering/custom-categories/test.ts.lint create mode 100644 test/rules/member-ordering/custom-categories/tslint.json rename test/rules/member-ordering/{order => }/custom/test.ts.lint (100%) rename test/rules/member-ordering/{order => }/custom/tslint.json (100%) rename test/rules/member-ordering/{order => }/fields-first/test.ts.lint (100%) rename test/rules/member-ordering/{order => }/fields-first/tslint.json (100%) rename test/rules/member-ordering/{order => }/instance-sandwich/test.ts.lint (100%) rename test/rules/member-ordering/{order => }/instance-sandwich/tslint.json (100%) create mode 100644 test/rules/member-ordering/omit-access-modifier/test.ts.lint create mode 100644 test/rules/member-ordering/omit-access-modifier/tslint.json delete mode 100644 test/rules/member-ordering/private/test.ts.lint create mode 100644 test/rules/member-ordering/public-before-private/test.ts.lint rename test/rules/member-ordering/{private => public-before-private}/tslint.json (100%) create mode 100644 test/rules/member-ordering/static-before-instance/test.ts.lint rename test/rules/member-ordering/{static => static-before-instance}/tslint.json (100%) delete mode 100644 test/rules/member-ordering/static/test.ts.lint rename test/rules/member-ordering/{order => }/statics-first/test.ts.lint (100%) rename test/rules/member-ordering/{order => }/statics-first/tslint.json (100%) rename test/rules/member-ordering/{method => variables-before-functions}/test.ts.lint (88%) rename test/rules/member-ordering/{method => variables-before-functions}/tslint.json (100%) diff --git a/src/rules/memberOrderingRule.ts b/src/rules/memberOrderingRule.ts index fa699086a88..eb26ce6e170 100644 --- a/src/rules/memberOrderingRule.ts +++ b/src/rules/memberOrderingRule.ts @@ -18,16 +18,105 @@ import * as ts from "typescript"; import * as Lint from "../index"; -/* start old options */ -const OPTION_VARIABLES_BEFORE_FUNCTIONS = "variables-before-functions"; -const OPTION_STATIC_BEFORE_INSTANCE = "static-before-instance"; -const OPTION_PUBLIC_BEFORE_PRIVATE = "public-before-private"; -/* end old options */ - -/* start new options */ const OPTION_ORDER = "order"; -const PRESET_ORDERS: { [preset: string]: string[] } = { - "fields-first": [ + +enum MemberKind { + publicStaticField, + publicStaticMethod, + protectedStaticField, + protectedStaticMethod, + privateStaticField, + privateStaticMethod, + publicInstanceField, + protectedInstanceField, + privateInstanceField, + publicConstructor, + protectedConstructor, + privateConstructor, + publicInstanceMethod, + protectedInstanceMethod, + privateInstanceMethod, +} + +const PRESETS = new Map([ + ["variables-before-functions", [ + { + kinds: [ + "public-static-field", + "protected-static-field", + "private-static-field", + "public-instance-field", + "protected-instance-field", + "private-instance-field", + ], + name: "field", + }, + { + kinds: [ + "constructor", + "public-static-method", + "protected-static-method", + "private-static-method", + "public-instance-method", + "protected-instance-method", + "private-instance-method", + ], + name: "method", + }, + ]], + ["static-before-instance", [ + { + kinds: [ + "public-static-field", + "public-static-method", + "protected-static-field", + "protected-static-method", + "private-static-field", + "private-static-method", + ], + name: "static member", + }, + { + kinds: [ + "public-instance-field", + "protected-instance-field", + "private-instance-field", + "constructor", + "public-instance-method", + "protected-instance-method", + "private-instance-method", + ], + name: "instance member", + }, + ]], + ["public-before-private", [ + { + kinds: [ + "public-static-field", + "protected-static-field", + "public-instance-field", + "protected-instance-field", + "public-instance-method", + "protected-instance-method", + "public-static-method", + "protected-static-method", + "public-constructor", + "protected-constructor", + ], + name: "public member", + }, + { + kinds: [ + "private-static-field", + "private-instance-field", + "private-instance-method", + "private-static-method", + "private-constructor", + ], + name: "private member", + }, + ]], + ["fields-first", [ "public-static-field", "protected-static-field", "private-static-field", @@ -41,8 +130,8 @@ const PRESET_ORDERS: { [preset: string]: string[] } = { "public-instance-method", "protected-instance-method", "private-instance-method", - ], - "instance-sandwich": [ + ]], + ["instance-sandwich", [ "public-static-field", "protected-static-field", "private-static-field", @@ -56,8 +145,8 @@ const PRESET_ORDERS: { [preset: string]: string[] } = { "public-static-method", "protected-static-method", "private-static-method", - ], - "statics-first": [ + ]], + ["statics-first", [ "public-static-field", "public-static-method", "protected-static-field", @@ -71,9 +160,42 @@ const PRESET_ORDERS: { [preset: string]: string[] } = { "public-instance-method", "protected-instance-method", "private-instance-method", - ], -}; -/* end new options */ + ]], +]); +const PRESET_NAMES = Array.from(PRESETS.keys()); + +const allMemberKindNames = mapDefined(Object.keys(MemberKind), (key) => { + const mk = (MemberKind as any)[key]; + return typeof mk === "number" ? MemberKind[mk].replace(/[A-Z]/g, (cap) => "-" + cap.toLowerCase()) : undefined; +}); + +function namesMarkdown(names: string[]): string { + return names.map((name) => "* `" + name + "`").join("\n "); +} + +const optionsDescription = Lint.Utils.dedent` + One argument, which is an object, must be provided. It should contain an \`order\` property. + The \`order\` property should have a value of one of the following strings: + + ${namesMarkdown(PRESET_NAMES)} + + Alternatively, the value for \`order\` maybe be an array consisting of the following strings: + + ${namesMarkdown(allMemberKindNames)} + + You can also omit the access modifier to refer to "public-", "protected-", and "private-" all at once; for example, "static-field". + + You can also make your own categories by using an object instead of a string: + + { + "name": "static non-private", + "kinds": [ + "public-static-field", + "protected-static-field", + "public-static-method", + "protected-static-method" + ] + }`; export class Rule extends Lint.Rules.AbstractRule { /* tslint:disable:object-literal-sort-keys */ @@ -81,43 +203,19 @@ export class Rule extends Lint.Rules.AbstractRule { ruleName: "member-ordering", description: "Enforces member ordering.", rationale: "A consistent ordering for class members can make classes easier to read, navigate, and edit.", - optionsDescription: Lint.Utils.dedent` - One argument, which is an object, must be provided. It should contain an \`order\` property. - The \`order\` property should have a value of one of the following strings: - - * \`fields-first\` - * \`statics-first\` - * \`instance-sandwich\` - - Alternatively, the value for \`order\` maybe be an array consisting of the following strings: - - * \`public-static-field\` - * \`protected-static-field\` - * \`private-static-field\` - * \`public-instance-field\` - * \`protected-instance-field\` - * \`private-instance-field\` - * \`constructor\` - * \`public-static-method\` - * \`protected-static-method\` - * \`private-static-method\` - * \`public-instance-method\` - * \`protected-instance-method\` - * \`private-instance-method\` - - This is useful if one of the preset orders does not meet your needs.`, + optionsDescription, options: { type: "object", properties: { order: { oneOf: [{ type: "string", - enum: ["fields-first", "statics-first", "instance-sandwich"], + enum: PRESET_NAMES, }, { type: "array", items: { type: "string", - enum: PRESET_ORDERS["statics-first"], + enum: allMemberKindNames, }, maxLength: 13, }], @@ -125,7 +223,35 @@ export class Rule extends Lint.Rules.AbstractRule { }, additionalProperties: false, }, - optionExamples: ['[true, { "order": "fields-first" }]'], + optionExamples: [ + '[true, { "order": "fields-first" }]', + Lint.Utils.dedent` + [true, { + "order": [ + "static-field", + "instance-field", + "constructor", + "public-instance-method", + "protected-instance-method", + "private-instance-method" + ] + }]`, + Lint.Utils.dedent` + [true, { + "order": [ + { + "name": "static non-private", + "kinds": [ + "public-static-field", + "protected-static-field", + "public-static-method", + "protected-static-method" + ] + }, + "constructor" + ] + }]`, + ], type: "typescript", typescriptOnly: true, }; @@ -136,11 +262,11 @@ export class Rule extends Lint.Rules.AbstractRule { } export class MemberOrderingWalker extends Lint.RuleWalker { - private readonly order: string[] | undefined; + private readonly order: MemberCategory[]; constructor(sourceFile: ts.SourceFile, options: Lint.IOptions) { super(sourceFile, options); - this.order = this.getOrder(); + this.order = getOrder(this.getOptions()); } public visitClassDeclaration(node: ts.ClassDeclaration) { @@ -163,37 +289,7 @@ export class MemberOrderingWalker extends Lint.RuleWalker { super.visitTypeLiteral(node); } - private getOrder(): string[] | undefined { - const allOptions = this.getOptions(); - if (allOptions == null || allOptions.length === 0) { - return undefined; - } - - const firstOption = allOptions[0]; - if (firstOption == null || typeof firstOption !== "object") { - return undefined; - } - - const orderOption = firstOption[OPTION_ORDER]; - if (Array.isArray(orderOption)) { - return orderOption; - } else if (typeof orderOption === "string") { - return PRESET_ORDERS[orderOption] || PRESET_ORDERS["default"]; - } else { - return undefined; - } - } - private visitMembers(members: Member[]) { - if (this.order === undefined) { - this.checkUsingOldOptions(members); - } else { - this.checkUsingNewOptions(members); - } - } - - /* start new code */ - private checkUsingNewOptions(members: Member[]) { let prevRank = -1; for (const member of members) { const rank = this.memberRank(member); @@ -232,122 +328,138 @@ export class MemberOrderingWalker extends Lint.RuleWalker { } private memberRank(member: Member): Rank | -1 { - const optionName = getOptionName(member); - return optionName === undefined ? -1 : this.order!.indexOf(optionName); + const optionName = getMemberKind(member); + if (optionName === undefined) { + return -1; + } + return this.order.findIndex((category) => category.has(optionName)); } private rankName(rank: Rank): string { - return this.order![rank].replace(/-/g, " "); + return this.order[rank].name; } - /* end new code */ +} - /* start old code */ - private checkUsingOldOptions(members: Member[]) { - let previousModifiers: IModifiers | undefined; - for (const member of members) { - const modifiers = getModifiers(member); - if (previousModifiers !== undefined && !this.canAppearAfter(previousModifiers, modifiers)) { - this.addFailureAtNode(member, - `Declaration of ${toString(modifiers)} not allowed to appear after declaration of ${toString(previousModifiers)}`); - } - previousModifiers = modifiers; - } - } +function memberKindForConstructor(access: Access): MemberKind { + return (MemberKind as any)[access + "Constructor"]; +} - private canAppearAfter(previousMember: IModifiers | undefined, currentMember: IModifiers) { - if (previousMember === undefined) { - return true; - } +function memberKindForMethodOrField(access: Access, membership: "Static" | "Instance", kind: "Method" | "Field"): MemberKind { + return (MemberKind as any)[access + membership + kind]; +} - if (this.hasOption(OPTION_VARIABLES_BEFORE_FUNCTIONS) && previousMember.isMethod !== currentMember.isMethod) { - return Number(previousMember.isMethod) < Number(currentMember.isMethod); - } +const allAccess: Access[] = ["public", "protected", "private"]; - if (this.hasOption(OPTION_STATIC_BEFORE_INSTANCE) && previousMember.isInstance !== currentMember.isInstance) { - return Number(previousMember.isInstance) < Number(currentMember.isInstance); - } +function memberKindFromName(name: string): MemberKind[] { + const kind = (MemberKind as any)[Lint.Utils.camelize(name)]; + return typeof kind === "number" ? [kind as MemberKind] : allAccess.map(addModifier); - if (this.hasOption(OPTION_PUBLIC_BEFORE_PRIVATE) && previousMember.isPrivate !== currentMember.isPrivate) { - return Number(previousMember.isPrivate) < Number(currentMember.isPrivate); + function addModifier(modifier: string) { + const modifiedKind = (MemberKind as any)[Lint.Utils.camelize(modifier + "-" + name)]; + if (typeof modifiedKind !== "number") { + throw new Error(`Bad member kind: ${name}`); } - - return true; + return modifiedKind; } - /* end old code */ } -/* start code supporting old options (i.e. "public-before-private") */ -interface IModifiers { - isMethod: boolean; - isPrivate: boolean; - isInstance: boolean; -} - -function getModifiers(member: Member): IModifiers { - return { - isInstance: !Lint.hasModifier(member.modifiers, ts.SyntaxKind.StaticKeyword), - isMethod: isMethodOrConstructor(member), - isPrivate: Lint.hasModifier(member.modifiers, ts.SyntaxKind.PrivateKeyword), - }; -} - -function toString(modifiers: IModifiers): string { - return [ - modifiers.isPrivate ? "private" : "public", - modifiers.isInstance ? "instance" : "static", - "member", - modifiers.isMethod ? "function" : "variable", - ].join(" "); -} -/* end old code */ +function getMemberKind(member: Member): MemberKind | undefined { + const accessLevel = hasModifier(ts.SyntaxKind.PrivateKeyword) ? "private" + : hasModifier(ts.SyntaxKind.ProtectedKeyword) ? "protected" + : "public"; -/* start new code */ -const enum MemberKind { Method, Constructor, Field, Ignore } -function getMemberKind(member: Member): MemberKind { switch (member.kind) { case ts.SyntaxKind.Constructor: case ts.SyntaxKind.ConstructSignature: - return MemberKind.Constructor; - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.MethodSignature: - return MemberKind.Method; + return memberKindForConstructor(accessLevel); + case ts.SyntaxKind.PropertyDeclaration: case ts.SyntaxKind.PropertySignature: - const { initializer } = member as ts.PropertyDeclaration; - const isFunction = initializer !== undefined && - (initializer.kind === ts.SyntaxKind.ArrowFunction || initializer.kind === ts.SyntaxKind.FunctionExpression); - return isFunction ? MemberKind.Method : MemberKind.Field; - default: - return MemberKind.Ignore; - } -} + return methodOrField(isFunctionLiteral((member as ts.PropertyDeclaration).initializer)); -function isMethodOrConstructor(member: Member) { - const kind = getMemberKind(member); - return kind === MemberKind.Method || kind === MemberKind.Constructor; -} + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.MethodSignature: + return methodOrField(true); -/** Returns e.g. "public-static-field". */ -function getOptionName(member: Member): string | undefined { - const memberKind = getMemberKind(member); - switch (memberKind) { - case MemberKind.Constructor: - return "constructor"; - case MemberKind.Ignore: - return undefined; default: - const accessLevel = hasModifier(ts.SyntaxKind.PrivateKeyword) ? "private" - : hasModifier(ts.SyntaxKind.ProtectedKeyword) ? "protected" - : "public"; - const membership = hasModifier(ts.SyntaxKind.StaticKeyword) ? "static" : "instance"; - const kind = memberKind === MemberKind.Method ? "method" : "field"; - return `${accessLevel}-${membership}-${kind}`; + return undefined; } + + function methodOrField(isMethod: boolean) { + const membership = hasModifier(ts.SyntaxKind.StaticKeyword) ? "Static" : "Instance"; + return memberKindForMethodOrField(accessLevel, membership, isMethod ? "Method" : "Field"); + } + function hasModifier(kind: ts.SyntaxKind) { return Lint.hasModifier(member.modifiers, kind); } } +type MemberCategoryJson = { name: string, kinds: string[] } | string; +class MemberCategory { + constructor(readonly name: string, private readonly kinds: Set) {} + public has(kind: MemberKind) { return this.kinds.has(kind); } +} + type Member = ts.TypeElement | ts.ClassElement; type Rank = number; -/* end new code */ + +type Access = "public" | "protected" | "private"; + +function getOrder(options: any[]): MemberCategory[] { + return getOrderJson(options).map((cat) => typeof cat === "string" + ? new MemberCategory(cat.replace(/-/g, " "), new Set(memberKindFromName(cat))) + : new MemberCategory(cat.name, new Set(flatMap(cat.kinds, memberKindFromName)))); +} +function getOrderJson(allOptions: any[]): MemberCategoryJson[] { + if (allOptions == null || allOptions.length === 0 || allOptions[0] == null) { + throw new Error("Got empty options"); + } + + const firstOption = allOptions[0]; + if (typeof firstOption !== "object") { + return categoryFromOption(firstOption); + } + + return categoryFromOption(firstOption[OPTION_ORDER]); +} +function categoryFromOption(orderOption: {}): MemberCategoryJson[] { + if (Array.isArray(orderOption)) { + return orderOption; + } + + const preset = PRESETS.get(orderOption as string); + if (!preset) { + throw new Error(`Bad order: ${JSON.stringify(orderOption)}`); + } + return preset; +} + +function isFunctionLiteral(node: ts.Node | undefined) { + switch (node && node.kind) { + case ts.SyntaxKind.ArrowFunction: + case ts.SyntaxKind.FunctionExpression: + return true; + default: + return false; + } +} + +function mapDefined(inputs: T[], getOutput: (input: T) => U | undefined): U[] { + const out = []; + for (const input of inputs) { + const output = getOutput(input); + if (output !== undefined) { + out.push(output); + } + } + return out; +} + +function flatMap(inputs: T[], getOutputs: (input: T) => U[]): U[] { + const out = []; + for (const input of inputs) { + out.push(...getOutputs(input)); + } + return out; +} diff --git a/test/rules/member-ordering/custom-categories/test.ts.lint b/test/rules/member-ordering/custom-categories/test.ts.lint new file mode 100644 index 00000000000..6df80c3273e --- /dev/null +++ b/test/rules/member-ordering/custom-categories/test.ts.lint @@ -0,0 +1,10 @@ +class C { + static foo() {} + protected static bar = 0; + static baz(); + + constructor(); + + static bang(); + ~~~~~~~~~~~~~~ [Declaration of static non-private not allowed after declaration of constructor. Instead, this should come at the beginning of the class/interface.] +} diff --git a/test/rules/member-ordering/custom-categories/tslint.json b/test/rules/member-ordering/custom-categories/tslint.json new file mode 100644 index 00000000000..ddacc962238 --- /dev/null +++ b/test/rules/member-ordering/custom-categories/tslint.json @@ -0,0 +1,18 @@ +{ + "rules": { + "member-ordering": [true, { + "order": [ + { + "name": "static non-private", + "kinds": [ + "public-static-field", + "protected-static-field", + "public-static-method", + "protected-static-method" + ] + }, + "constructor" + ] + }] + } +} diff --git a/test/rules/member-ordering/order/custom/test.ts.lint b/test/rules/member-ordering/custom/test.ts.lint similarity index 100% rename from test/rules/member-ordering/order/custom/test.ts.lint rename to test/rules/member-ordering/custom/test.ts.lint diff --git a/test/rules/member-ordering/order/custom/tslint.json b/test/rules/member-ordering/custom/tslint.json similarity index 100% rename from test/rules/member-ordering/order/custom/tslint.json rename to test/rules/member-ordering/custom/tslint.json diff --git a/test/rules/member-ordering/order/fields-first/test.ts.lint b/test/rules/member-ordering/fields-first/test.ts.lint similarity index 100% rename from test/rules/member-ordering/order/fields-first/test.ts.lint rename to test/rules/member-ordering/fields-first/test.ts.lint diff --git a/test/rules/member-ordering/order/fields-first/tslint.json b/test/rules/member-ordering/fields-first/tslint.json similarity index 100% rename from test/rules/member-ordering/order/fields-first/tslint.json rename to test/rules/member-ordering/fields-first/tslint.json diff --git a/test/rules/member-ordering/order/instance-sandwich/test.ts.lint b/test/rules/member-ordering/instance-sandwich/test.ts.lint similarity index 100% rename from test/rules/member-ordering/order/instance-sandwich/test.ts.lint rename to test/rules/member-ordering/instance-sandwich/test.ts.lint diff --git a/test/rules/member-ordering/order/instance-sandwich/tslint.json b/test/rules/member-ordering/instance-sandwich/tslint.json similarity index 100% rename from test/rules/member-ordering/order/instance-sandwich/tslint.json rename to test/rules/member-ordering/instance-sandwich/tslint.json diff --git a/test/rules/member-ordering/omit-access-modifier/test.ts.lint b/test/rules/member-ordering/omit-access-modifier/test.ts.lint new file mode 100644 index 00000000000..fdfbf683047 --- /dev/null +++ b/test/rules/member-ordering/omit-access-modifier/test.ts.lint @@ -0,0 +1,12 @@ +class C { + private static x = 0; + static y = 1; + + x = 0; + private y = 1; + + constructor() {} + + static z = 2; + ~~~~~~~~~~~~~ [Declaration of static field not allowed after declaration of constructor. Instead, this should come at the beginning of the class/interface.] +} diff --git a/test/rules/member-ordering/omit-access-modifier/tslint.json b/test/rules/member-ordering/omit-access-modifier/tslint.json new file mode 100644 index 00000000000..c88314eb612 --- /dev/null +++ b/test/rules/member-ordering/omit-access-modifier/tslint.json @@ -0,0 +1,9 @@ +{ + "rules": { + "member-ordering": [true, { "order": [ + "static-field", + "instance-field", + "constructor" + ]}] + } +} diff --git a/test/rules/member-ordering/private/test.ts.lint b/test/rules/member-ordering/private/test.ts.lint deleted file mode 100644 index 24f637796f7..00000000000 --- a/test/rules/member-ordering/private/test.ts.lint +++ /dev/null @@ -1,10 +0,0 @@ -class Foo { - private x: number; - private bar(): any { - var bla: { a: string } = {a: '1'}; - } - y: number; - ~~~~~~~~~~ [0] -} - -[0]: Declaration of public instance member variable not allowed to appear after declaration of private instance member function diff --git a/test/rules/member-ordering/public-before-private/test.ts.lint b/test/rules/member-ordering/public-before-private/test.ts.lint new file mode 100644 index 00000000000..c26be55d8ac --- /dev/null +++ b/test/rules/member-ordering/public-before-private/test.ts.lint @@ -0,0 +1,10 @@ +class Foo { + private x: number; + private bar(): any { + var bla: { a: string } = {a: '1'}; + } + y: number; + ~~~~~~~~~~ [0] +} + +[0]: Declaration of public member not allowed after declaration of private member. Instead, this should come at the beginning of the class/interface. diff --git a/test/rules/member-ordering/private/tslint.json b/test/rules/member-ordering/public-before-private/tslint.json similarity index 100% rename from test/rules/member-ordering/private/tslint.json rename to test/rules/member-ordering/public-before-private/tslint.json diff --git a/test/rules/member-ordering/static-before-instance/test.ts.lint b/test/rules/member-ordering/static-before-instance/test.ts.lint new file mode 100644 index 00000000000..532440d9837 --- /dev/null +++ b/test/rules/member-ordering/static-before-instance/test.ts.lint @@ -0,0 +1,10 @@ +class Foo { + x: number; + static y: number; + ~~~~~~~~~~~~~~~~~ [0] + constructor() { + // nothing to do + } +} + +[0]: Declaration of static member not allowed after declaration of instance member. Instead, this should come at the beginning of the class/interface. diff --git a/test/rules/member-ordering/static/tslint.json b/test/rules/member-ordering/static-before-instance/tslint.json similarity index 100% rename from test/rules/member-ordering/static/tslint.json rename to test/rules/member-ordering/static-before-instance/tslint.json diff --git a/test/rules/member-ordering/static/test.ts.lint b/test/rules/member-ordering/static/test.ts.lint deleted file mode 100644 index b5396b81a18..00000000000 --- a/test/rules/member-ordering/static/test.ts.lint +++ /dev/null @@ -1,10 +0,0 @@ -class Foo { - x: number; - static y: number; - ~~~~~~~~~~~~~~~~~ [0] - constructor() { - // nothing to do - } -} - -[0]: Declaration of public static member variable not allowed to appear after declaration of public instance member variable diff --git a/test/rules/member-ordering/order/statics-first/test.ts.lint b/test/rules/member-ordering/statics-first/test.ts.lint similarity index 100% rename from test/rules/member-ordering/order/statics-first/test.ts.lint rename to test/rules/member-ordering/statics-first/test.ts.lint diff --git a/test/rules/member-ordering/order/statics-first/tslint.json b/test/rules/member-ordering/statics-first/tslint.json similarity index 100% rename from test/rules/member-ordering/order/statics-first/tslint.json rename to test/rules/member-ordering/statics-first/tslint.json diff --git a/test/rules/member-ordering/method/test.ts.lint b/test/rules/member-ordering/variables-before-functions/test.ts.lint similarity index 88% rename from test/rules/member-ordering/method/test.ts.lint rename to test/rules/member-ordering/variables-before-functions/test.ts.lint index 044904e4395..e657b567d72 100644 --- a/test/rules/member-ordering/method/test.ts.lint +++ b/test/rules/member-ordering/variables-before-functions/test.ts.lint @@ -68,5 +68,4 @@ const o = { } } -[0]: Declaration of public instance member variable not allowed to appear after declaration of public instance member function - +[0]: Declaration of field not allowed after declaration of method. Instead, this should come at the beginning of the class/interface. diff --git a/test/rules/member-ordering/method/tslint.json b/test/rules/member-ordering/variables-before-functions/tslint.json similarity index 100% rename from test/rules/member-ordering/method/tslint.json rename to test/rules/member-ordering/variables-before-functions/tslint.json From ec3df324edaa2737c8fedddc19d2eec97c05bbf7 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Wed, 18 Jan 2017 22:38:38 -0800 Subject: [PATCH 041/131] strict-boolean-expressions: Add 'allow-null-union', 'allow-undefined-union', 'allow-string', and 'allow-number' options (#2033) --- src/language/utils.ts | 10 +- src/rules/restrictPlusOperandsRule.ts | 8 +- src/rules/strictBooleanExpressionsRule.ts | 357 ++++++++++++++---- .../allow-mix/test.ts.lint | 20 + .../allow-mix/tsconfig.json | 5 + .../allow-mix/tslint.json | 8 + .../allow-null-union/test.ts.lint | 12 + .../allow-null-union/tsconfig.json | 5 + .../allow-null-union/tslint.json | 8 + .../allow-number/test.ts.lint | 19 + .../allow-number/tslint.json | 8 + .../allow-string/test.ts.lint | 14 + .../allow-string/tslint.json | 8 + .../allow-undefined-union/test.ts.lint | 28 ++ .../allow-undefined-union/tsconfig.json | 5 + .../allow-undefined-union/tslint.json | 8 + .../default/test.ts.lint | 181 +++++++++ .../{ => default}/tslint.json | 0 .../no-allow-mix/test.ts.lint | 28 ++ .../no-allow-mix/tsconfig.json | 5 + .../no-allow-mix/tslint.json | 8 + .../strict-boolean-expressions/test.ts.lint | 179 --------- 22 files changed, 662 insertions(+), 262 deletions(-) create mode 100644 test/rules/strict-boolean-expressions/allow-mix/test.ts.lint create mode 100644 test/rules/strict-boolean-expressions/allow-mix/tsconfig.json create mode 100644 test/rules/strict-boolean-expressions/allow-mix/tslint.json create mode 100644 test/rules/strict-boolean-expressions/allow-null-union/test.ts.lint create mode 100644 test/rules/strict-boolean-expressions/allow-null-union/tsconfig.json create mode 100644 test/rules/strict-boolean-expressions/allow-null-union/tslint.json create mode 100644 test/rules/strict-boolean-expressions/allow-number/test.ts.lint create mode 100644 test/rules/strict-boolean-expressions/allow-number/tslint.json create mode 100644 test/rules/strict-boolean-expressions/allow-string/test.ts.lint create mode 100644 test/rules/strict-boolean-expressions/allow-string/tslint.json create mode 100644 test/rules/strict-boolean-expressions/allow-undefined-union/test.ts.lint create mode 100644 test/rules/strict-boolean-expressions/allow-undefined-union/tsconfig.json create mode 100644 test/rules/strict-boolean-expressions/allow-undefined-union/tslint.json create mode 100644 test/rules/strict-boolean-expressions/default/test.ts.lint rename test/rules/strict-boolean-expressions/{ => default}/tslint.json (100%) create mode 100644 test/rules/strict-boolean-expressions/no-allow-mix/test.ts.lint create mode 100644 test/rules/strict-boolean-expressions/no-allow-mix/tsconfig.json create mode 100644 test/rules/strict-boolean-expressions/no-allow-mix/tslint.json delete mode 100644 test/rules/strict-boolean-expressions/test.ts.lint diff --git a/src/language/utils.ts b/src/language/utils.ts index f9d31bec4c0..fd122a9910a 100644 --- a/src/language/utils.ts +++ b/src/language/utils.ts @@ -256,9 +256,9 @@ export type FilterCallback = (node: ts.Node) => boolean; /** * Iterate over all tokens of `node` - * + * * @description JsDoc comments are treated like regular comments and only visited if `skipTrivia` === false. - * + * * @param node The node whose tokens should be visited * @param skipTrivia If set to false all trivia preceeding `node` or any of its children is included * @param cb Is called for every token of `node`. It gets the full text of the SourceFile and the position of the token within that text. @@ -310,7 +310,7 @@ export function forEachToken(node: ts.Node, skipTrivia: boolean, cb: ForEachToke function createTriviaHandler(sourceFile: ts.SourceFile, cb: ForEachTokenCallback) { const fullText = sourceFile.text; const scanner = ts.createScanner(sourceFile.languageVersion, false, sourceFile.languageVariant, fullText); - /** + /** * Scan the specified range to get all trivia tokens. * This includes trailing trivia of the last token and the leading trivia of the current token */ @@ -404,9 +404,9 @@ function canHaveTrailingTrivia(tokenKind: ts.SyntaxKind, parent: ts.Node): boole return true; } -/** +/** * Checks if there are any comments between `position` and the next non-trivia token - * + * * @param text The text to scan * @param position The position inside `text` where to start scanning. Make sure that this is a valid start position. * This value is typically obtained from `node.getFullStart()` or `node.getEnd()` diff --git a/src/rules/restrictPlusOperandsRule.ts b/src/rules/restrictPlusOperandsRule.ts index 249bd6c7522..6f303d1fa48 100644 --- a/src/rules/restrictPlusOperandsRule.ts +++ b/src/rules/restrictPlusOperandsRule.ts @@ -66,8 +66,8 @@ function getBaseTypeOfLiteralType(type: ts.Type): "string" | "number" | "invalid return "string"; } else if (isTypeFlagSet(type, ts.TypeFlags.NumberLiteral) || isTypeFlagSet(type, ts.TypeFlags.Number)) { return "number"; - } else if (isTypeFlagSet(type, ts.TypeFlags.Union) && !isTypeFlagSet(type, ts.TypeFlags.Enum)) { - const types = (type as ts.UnionType).types.map(getBaseTypeOfLiteralType); + } else if (isUnionType(type) && !isTypeFlagSet(type, ts.TypeFlags.Enum)) { + const types = type.types.map(getBaseTypeOfLiteralType); return allSame(types) ? types[0] : "invalid"; } else if (isTypeFlagSet(type, ts.TypeFlags.EnumLiteral)) { return getBaseTypeOfLiteralType((type as ts.EnumLiteralType).baseType); @@ -78,3 +78,7 @@ function getBaseTypeOfLiteralType(type: ts.Type): "string" | "number" | "invalid function allSame(array: string[]) { return array.every((value) => value === array[0]); } + +function isUnionType(type: ts.Type): type is ts.UnionType { + return Lint.isTypeFlagSet(type, ts.TypeFlags.Union); +} diff --git a/src/rules/strictBooleanExpressionsRule.ts b/src/rules/strictBooleanExpressionsRule.ts index 157f662792b..a00c809af12 100644 --- a/src/rules/strictBooleanExpressionsRule.ts +++ b/src/rules/strictBooleanExpressionsRule.ts @@ -17,138 +17,343 @@ import * as ts from "typescript"; import * as Lint from "../index"; -import * as utils from "../language/utils"; + +const OPTION_ALLOW_NULL_UNION = "allow-null-union"; +const OPTION_ALLOW_UNDEFINED_UNION = "allow-undefined-union"; +const OPTION_ALLOW_STRING = "allow-string"; +const OPTION_ALLOW_NUMBER = "allow-number"; +const OPTION_ALLOW_MIX = "allow-mix"; + +// tslint:disable:switch-default export class Rule extends Lint.Rules.TypedRule { /* tslint:disable:object-literal-sort-keys */ public static metadata: Lint.IRuleMetadata = { ruleName: "strict-boolean-expressions", - description: `Usage of && or || operators should be with boolean operands and -expressions in If, Do, While and For statements should be of type boolean`, - optionsDescription: "Not configurable.", - options: null, - optionExamples: ["true"], + description: Lint.Utils.dedent` + Restricts the types allowed in boolean expressions. By default only booleans are allowed. + + The following nodes are checked: + * Arguments to the '!', '&&', and '||' operators + * The condition in a conditional expression ('cond ? x : y') + * Conditions for 'if', 'for', 'while', and 'do-while' statements.`, + optionsDescription: Lint.Utils.dedent` + These options may be provided: + + * '${OPTION_ALLOW_NULL_UNION} allows union types containing 'null'. + - It does *not* allow 'null' itself. + * '${OPTION_ALLOW_UNDEFINED_UNION} allows union types containing 'undefined'. + - It does *not* allow 'undefined' itself. + * '${OPTION_ALLOW_STRING} allows strings. + - It does *not* allow unions containing 'string'. + - It does *not* allow string literal types. + * '${OPTION_ALLOW_NUMBER} allows numbers. + - It does *not* allow unions containing 'number'. + - It does *not* allow enums or number literal types. + * '${OPTION_ALLOW_MIX} allow multiple of the above to appear together. + - For example, 'string | number' or 'RegExp | null | undefined' would normally not be allowed. + - A type like '"foo" | "bar" | undefined' is always allowed, because it has only one way to be false.`, + options: { + type: "array", + items: { + type: "string", + enum: [OPTION_ALLOW_NULL_UNION, OPTION_ALLOW_UNDEFINED_UNION, OPTION_ALLOW_STRING, OPTION_ALLOW_NUMBER], + }, + minLength: 0, + maxLength: 5, + }, + optionExamples: [ + "true", + `[true, ${OPTION_ALLOW_NULL_UNION}, ${OPTION_ALLOW_UNDEFINED_UNION}, ${OPTION_ALLOW_STRING}, ${OPTION_ALLOW_NUMBER}]`, + ], type: "functionality", typescriptOnly: true, requiresTypeInfo: true, }; /* tslint:enable:object-literal-sort-keys */ - public static BINARY_EXPRESSION_ERROR = "Operands for the && or || operator should be of type boolean"; - public static UNARY_EXPRESSION_ERROR = "Operand for the ! operator should be of type boolean"; - public static STATEMENT_ERROR = "statement condition needs to be a boolean expression or literal"; - public static CONDITIONAL_EXPRESSION_ERROR = "Condition needs to be a boolean expression or literal"; + public static FAILURE_STRING(locationDescription: string, ty: TypeFailure, isUnionType: boolean, expectedTypes: string[]): string { + const expected = expectedTypes.length === 1 + ? `Only ${expectedTypes[0]}s are allowed` + : `Allowed types are ${stringOr(expectedTypes)}`; + return `This type is not allowed in the ${locationDescription} because it ${this.tyFailure(ty, isUnionType)}. ${expected}.`; + } + + private static tyFailure(ty: TypeFailure, isUnionType: boolean) { + const is = isUnionType ? "could be" : "is"; + switch (ty) { + case TypeFailure.AlwaysTruthy: return "is always truthy"; + case TypeFailure.AlwaysFalsy: return "is always falsy"; + case TypeFailure.String: return `${is} a string`; + case TypeFailure.Number: return `${is} a number`; + case TypeFailure.Null: return `${is} null`; + case TypeFailure.Undefined: return `${is} undefined`; + case TypeFailure.Enum: return `${is} an enum`; + case TypeFailure.Mixes: return "unions more than one truthy/falsy type"; + } + } public applyWithProgram(srcFile: ts.SourceFile, langSvc: ts.LanguageService): Lint.RuleFailure[] { - return this.applyWithWalker(new StrictBooleanExpressionsRule(srcFile, this.getOptions(), langSvc.getProgram())); + return this.applyWithWalker(new Walker(srcFile, this.getOptions(), langSvc.getProgram())); } } -type StatementType = ts.IfStatement|ts.DoStatement|ts.WhileStatement; - -class StrictBooleanExpressionsRule extends Lint.ProgramAwareRuleWalker { - private checker: ts.TypeChecker; - - constructor(srcFile: ts.SourceFile, lintOptions: Lint.IOptions, program: ts.Program) { - super(srcFile, lintOptions, program); - this.checker = this.getTypeChecker(); - } +class Walker extends Lint.ProgramAwareRuleWalker { + private allowNullUnion = this.hasOption(OPTION_ALLOW_NULL_UNION); + private allowUndefinedUnion = this.hasOption(OPTION_ALLOW_UNDEFINED_UNION); + private allowString = this.hasOption(OPTION_ALLOW_STRING); + private allowNumber = this.hasOption(OPTION_ALLOW_NUMBER); + private allowMix = this.hasOption(OPTION_ALLOW_MIX); public visitBinaryExpression(node: ts.BinaryExpression) { - const isAndAndBinaryOperator = node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken; - const isOrOrBinaryOperator = node.operatorToken.kind === ts.SyntaxKind.BarBarToken; - if (isAndAndBinaryOperator || isOrOrBinaryOperator) { - const lhsExpression = node.left; - const lhsType = this.checker.getTypeAtLocation(lhsExpression); - const rhsExpression = node.right; - const rhsType = this.checker.getTypeAtLocation(rhsExpression); - if (!this.isBooleanType(lhsType)) { - if (lhsExpression.kind !== ts.SyntaxKind.BinaryExpression) { - this.addFailureAtNode(lhsExpression, Rule.BINARY_EXPRESSION_ERROR); - } else { - this.visitBinaryExpression(lhsExpression as ts.BinaryExpression); - } - } - if (!this.isBooleanType(rhsType)) { - if (rhsExpression.kind !== ts.SyntaxKind.BinaryExpression) { - this.addFailureAtNode(rhsExpression, Rule.BINARY_EXPRESSION_ERROR); - } else { - this.visitBinaryExpression(rhsExpression as ts.BinaryExpression); + const op = binaryBooleanExpressionKind(node); + if (op !== undefined) { + const checkHalf = (expr: ts.Expression) => { + // If it's another boolean binary expression, we'll check it when recursing. + if (!isBooleanBinaryExpression(expr)) { + this.checkExpression(expr, `operand for the '${op}' operator`); } - } + }; + checkHalf(node.left); + checkHalf(node.right); } super.visitBinaryExpression(node); } public visitPrefixUnaryExpression(node: ts.PrefixUnaryExpression) { - const isExclamationOperator = node.operator === ts.SyntaxKind.ExclamationToken; - if (isExclamationOperator) { - const expr = node.operand; - const expType = this.checker.getTypeAtLocation(expr); - if (!this.isBooleanType(expType)) { - this.addFailureAtNode(node, Rule.UNARY_EXPRESSION_ERROR); - } + if (node.operator === ts.SyntaxKind.ExclamationToken) { + this.checkExpression(node.operand, "operand for the '!' operator"); } super.visitPrefixUnaryExpression(node); } public visitIfStatement(node: ts.IfStatement) { - this.checkStatement(node); + this.checkStatement(node, "'if' condition"); super.visitIfStatement(node); } public visitWhileStatement(node: ts.WhileStatement) { - this.checkStatement(node); + this.checkStatement(node, "'while' condition"); super.visitWhileStatement(node); } public visitDoStatement(node: ts.DoStatement) { - this.checkStatement(node); + this.checkStatement(node, "'do-while' condition"); super.visitDoStatement(node); } public visitConditionalExpression(node: ts.ConditionalExpression) { - const cexp = node.condition; - const expType = this.checker.getTypeAtLocation(cexp); - if (!this.isBooleanType(expType)) { - this.addFailureAtNode(cexp, Rule.CONDITIONAL_EXPRESSION_ERROR); - } + this.checkExpression(node.condition, "condition"); super.visitConditionalExpression(node); } public visitForStatement(node: ts.ForStatement) { - const forCondition = node.condition; - if (forCondition !== undefined) { - const expType = this.checker.getTypeAtLocation(forCondition); - if (!this.isBooleanType(expType)) { - this.addFailureAtNode(forCondition, `For ${Rule.STATEMENT_ERROR}`); - } + if (node.condition !== undefined) { + this.checkExpression(node.condition, "'for' condition"); } super.visitForStatement(node); } - private checkStatement(node: StatementType) { - const bexp = node.expression; - const expType = this.checker.getTypeAtLocation(bexp); - if (!this.isBooleanType(expType)) { - this.addFailureAtNode(bexp, `${failureTextForKind(node.kind)} ${Rule.STATEMENT_ERROR}`); + private checkStatement(node: ts.IfStatement | ts.DoStatement | ts.WhileStatement, locationDescription: string) { + // If it's a boolean binary expression, we'll check it when recursing. + if (!isBooleanBinaryExpression(node.expression)) { + this.checkExpression(node.expression, locationDescription); + } + } + + private checkExpression(node: ts.Expression, locationDescription: string): void { + const type = this.getTypeChecker().getTypeAtLocation(node); + const failure = this.getTypeFailure(type); + if (failure !== undefined) { + this.addFailureAtNode(node, Rule.FAILURE_STRING(locationDescription, failure, isUnionType(type), this.expectedTypes())); + } + } + + private getTypeFailure(type: ts.Type): TypeFailure | undefined { + if (isUnionType(type)) { + return this.handleUnion(type); + } + + const kind = getKind(type); + const failure = this.failureForKind(kind, /*isInUnion*/false); + if (failure !== undefined) { + return failure; + } + + switch (triState(kind)) { + case true: + return TypeFailure.AlwaysTruthy; + case false: + return TypeFailure.AlwaysFalsy; + case undefined: + return undefined; } } - private isBooleanType(btype: ts.Type): boolean { - return utils.isTypeFlagSet(btype, ts.TypeFlags.BooleanLike); + /** Fails if a kind of falsiness is not allowed. */ + private failureForKind(kind: TypeKind, isInUnion: boolean): TypeFailure | undefined { + switch (kind) { + case TypeKind.String: + case TypeKind.FalseStringLiteral: + return this.allowString ? undefined : TypeFailure.String; + case TypeKind.Number: + case TypeKind.FalseNumberLiteral: + return this.allowNumber ? undefined : TypeFailure.Number; + case TypeKind.Enum: + return TypeFailure.Enum; + case TypeKind.Null: + return isInUnion && !this.allowNullUnion ? TypeFailure.Null : undefined; + case TypeKind.Undefined: + return isInUnion && !this.allowUndefinedUnion ? TypeFailure.Undefined : undefined; + default: + return undefined; + } } + + private handleUnion(type: ts.UnionType): TypeFailure | undefined { + // Tracks whether it's possibly truthy. + let anyTruthy = false; + // Counts falsy kinds to see if there's a mix. Also tracks whether it's possibly falsy. + let seenFalsy = 0; + + for (const ty of type.types) { + const kind = getKind(ty); + const failure = this.failureForKind(kind, /*isInUnion*/true); + if (failure !== undefined) { + return failure; + } + + switch (triState(kind)) { + case true: + anyTruthy = true; + break; + case false: + seenFalsy++; + break; + default: + anyTruthy = true; + seenFalsy++; + } + } + + return seenFalsy === 0 ? TypeFailure.AlwaysTruthy + : !anyTruthy ? TypeFailure.AlwaysFalsy + : !this.allowMix && seenFalsy > 1 ? TypeFailure.Mixes : undefined; + } + + private expectedTypes(): string[] { + const parts = ["boolean"]; + if (this.allowNullUnion) { parts.push("null-union"); } + if (this.allowUndefinedUnion) { parts.push("undefined-union"); } + if (this.allowString) { parts.push("string"); } + if (this.allowNumber) { parts.push("number"); } + return parts; + } +} + +export const enum TypeFailure { + AlwaysTruthy, + AlwaysFalsy, + String, + Number, + Null, + Undefined, + Enum, + Mixes, } -function failureTextForKind(kind: ts.SyntaxKind) { +const enum TypeKind { + String, + FalseStringLiteral, + Number, + FalseNumberLiteral, + Boolean, + FalseBooleanLiteral, + Null, + Undefined, + Enum, + AlwaysTruthy, +} + +/** Divides a type into always true, always false, or unknown. */ +function triState(kind: TypeKind): boolean | undefined { switch (kind) { - case ts.SyntaxKind.IfStatement: - return "If"; - case ts.SyntaxKind.DoStatement: - return "Do-While"; - case ts.SyntaxKind.WhileStatement: - return "While"; + case TypeKind.String: + case TypeKind.Number: + case TypeKind.Boolean: + case TypeKind.Enum: + return undefined; + + case TypeKind.Null: + case TypeKind.Undefined: + case TypeKind.FalseNumberLiteral: + case TypeKind.FalseStringLiteral: + case TypeKind.FalseBooleanLiteral: + return false; + + case TypeKind.AlwaysTruthy: + return true; + } +} + +function getKind(type: ts.Type): TypeKind { + return is(ts.TypeFlags.String) ? TypeKind.String + : is(ts.TypeFlags.Number) ? TypeKind.Number + : is(ts.TypeFlags.Boolean) ? TypeKind.Boolean + : is(ts.TypeFlags.Null) ? TypeKind.Null + : is(ts.TypeFlags.Undefined | ts.TypeFlags.Void) ? TypeKind.Undefined // tslint:disable-line:no-bitwise + : is(ts.TypeFlags.EnumLike) ? TypeKind.Enum + : is(ts.TypeFlags.NumberLiteral) ? + ((type as ts.LiteralType).text === "0" ? TypeKind.FalseNumberLiteral : TypeKind.AlwaysTruthy) + : is(ts.TypeFlags.StringLiteral) ? + ((type as ts.LiteralType).text === "" ? TypeKind.FalseStringLiteral : TypeKind.AlwaysTruthy) + : is(ts.TypeFlags.BooleanLiteral) ? + ((type as ts.IntrinsicType).intrinsicName === "true" ? TypeKind.AlwaysTruthy : TypeKind.FalseBooleanLiteral) + : TypeKind.AlwaysTruthy; + + function is(flags: ts.TypeFlags) { + return Lint.isTypeFlagSet(type, flags); + } +} + +/** Matches `&&` and `||` operators. */ +function isBooleanBinaryExpression(node: ts.Expression): boolean { + return node.kind === ts.SyntaxKind.BinaryExpression && binaryBooleanExpressionKind(node as ts.BinaryExpression) !== undefined; +} + +function binaryBooleanExpressionKind(node: ts.BinaryExpression): "&&" | "||" | undefined { + switch (node.operatorToken.kind) { + case ts.SyntaxKind.AmpersandAmpersandToken: + return "&&"; + case ts.SyntaxKind.BarBarToken: + return "||"; default: - throw new Error("Unknown Syntax Kind"); + return undefined; + } +} + +function stringOr(parts: string[]): string { + switch (parts.length) { + case 1: + return parts[0]; + case 2: + return parts[0] + " or " + parts[1]; + default: + let res = ""; + for (let i = 0; i < parts.length - 1; i++) { + res += parts[i] + ", "; + } + return res + "or " + parts[parts.length - 1]; + } +} + +function isUnionType(type: ts.Type): type is ts.UnionType { + return Lint.isTypeFlagSet(type, ts.TypeFlags.Union); +} + +declare module "typescript" { + // No other way to distinguish boolean literal true from boolean literal false + export interface IntrinsicType extends ts.Type { + intrinsicName: string; } } diff --git a/test/rules/strict-boolean-expressions/allow-mix/test.ts.lint b/test/rules/strict-boolean-expressions/allow-mix/test.ts.lint new file mode 100644 index 00000000000..33de775911f --- /dev/null +++ b/test/rules/strict-boolean-expressions/allow-mix/test.ts.lint @@ -0,0 +1,20 @@ +declare function get(): T; + +if (get()) {} + ~~~~~~~~~~~~~~~~~~~~~~ [This type is not allowed in the 'if' condition because it could be a number. Allowed types are boolean, undefined-union, or string.] + +if (get()) {} + +if (get()) {} + +if (get()) {} +if (get()) {} +if (get()) {} + +if (get<"" | "foo">()) {} + +// Mix of truthy values OK +if (get<"foo" | "bar" | undefined>()) {} + +// Mix of falsy values not OK +if (get<"" | boolean>()) {} diff --git a/test/rules/strict-boolean-expressions/allow-mix/tsconfig.json b/test/rules/strict-boolean-expressions/allow-mix/tsconfig.json new file mode 100644 index 00000000000..1cc3bc85ee9 --- /dev/null +++ b/test/rules/strict-boolean-expressions/allow-mix/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "strictNullChecks": true + } +} \ No newline at end of file diff --git a/test/rules/strict-boolean-expressions/allow-mix/tslint.json b/test/rules/strict-boolean-expressions/allow-mix/tslint.json new file mode 100644 index 00000000000..725d18c2e5b --- /dev/null +++ b/test/rules/strict-boolean-expressions/allow-mix/tslint.json @@ -0,0 +1,8 @@ +{ + "linterOptions": { + "typeCheck": true + }, + "rules": { + "strict-boolean-expressions": [true, "allow-string", "allow-undefined-union", "allow-mix"] + } +} diff --git a/test/rules/strict-boolean-expressions/allow-null-union/test.ts.lint b/test/rules/strict-boolean-expressions/allow-null-union/test.ts.lint new file mode 100644 index 00000000000..f74cc3614b9 --- /dev/null +++ b/test/rules/strict-boolean-expressions/allow-null-union/test.ts.lint @@ -0,0 +1,12 @@ +declare function get(): T; + +if (get()) {} + +if (get()) {} + ~~~~~~~~~~~~~~~~~~~~~ [This type is not allowed in the 'if' condition because it unions more than one truthy/falsy type. Allowed types are boolean or null-union.] + +if (get()) {} + ~~~~~~~~~~~ [This type is not allowed in the 'if' condition because it is always falsy. Allowed types are boolean or null-union.] + +if (get()) {} + ~~~~~~~~~~~~~~~~~~~~~~~~~ [This type is not allowed in the 'if' condition because it could be undefined. Allowed types are boolean or null-union.] diff --git a/test/rules/strict-boolean-expressions/allow-null-union/tsconfig.json b/test/rules/strict-boolean-expressions/allow-null-union/tsconfig.json new file mode 100644 index 00000000000..1cc3bc85ee9 --- /dev/null +++ b/test/rules/strict-boolean-expressions/allow-null-union/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "strictNullChecks": true + } +} \ No newline at end of file diff --git a/test/rules/strict-boolean-expressions/allow-null-union/tslint.json b/test/rules/strict-boolean-expressions/allow-null-union/tslint.json new file mode 100644 index 00000000000..64303ce8a51 --- /dev/null +++ b/test/rules/strict-boolean-expressions/allow-null-union/tslint.json @@ -0,0 +1,8 @@ +{ + "linterOptions": { + "typeCheck": true + }, + "rules": { + "strict-boolean-expressions": [true, "allow-null-union"] + } +} diff --git a/test/rules/strict-boolean-expressions/allow-number/test.ts.lint b/test/rules/strict-boolean-expressions/allow-number/test.ts.lint new file mode 100644 index 00000000000..4252ea97086 --- /dev/null +++ b/test/rules/strict-boolean-expressions/allow-number/test.ts.lint @@ -0,0 +1,19 @@ +declare function get(): T; + +if (get()) {} + +if (get()) {} + ~~~~~~~~~~~~~~~~~~~~~~~ [This type is not allowed in the 'if' condition because it unions more than one truthy/falsy type. Allowed types are boolean or number.] + +enum E {} +if (get()) {} + ~~~~~~~~ [This type is not allowed in the 'if' condition because it is an enum. Allowed types are boolean or number.] + +if (get<1 | 2>()) {} + ~~~~~~~~~~~~ [This type is not allowed in the 'if' condition because it is always truthy. Allowed types are boolean or number.] + +if (get<0>()) {} + ~~~~~~~~ [This type is not allowed in the 'if' condition because it is always falsy. Allowed types are boolean or number.] + +if (get<0 | 1>()) {} + diff --git a/test/rules/strict-boolean-expressions/allow-number/tslint.json b/test/rules/strict-boolean-expressions/allow-number/tslint.json new file mode 100644 index 00000000000..df204cead30 --- /dev/null +++ b/test/rules/strict-boolean-expressions/allow-number/tslint.json @@ -0,0 +1,8 @@ +{ + "linterOptions": { + "typeCheck": true + }, + "rules": { + "strict-boolean-expressions": [true, "allow-number"] + } +} diff --git a/test/rules/strict-boolean-expressions/allow-string/test.ts.lint b/test/rules/strict-boolean-expressions/allow-string/test.ts.lint new file mode 100644 index 00000000000..89930bf3052 --- /dev/null +++ b/test/rules/strict-boolean-expressions/allow-string/test.ts.lint @@ -0,0 +1,14 @@ +declare function get(): T; + +if (get()) {} + +if (get()) {} + ~~~~~~~~~~~~~~~~~~~~~~~ [This type is not allowed in the 'if' condition because it unions more than one truthy/falsy type. Allowed types are boolean or string.] + +if (get<"foo" | "bar">()) {} + ~~~~~~~~~~~~~~~~~~~~ [This type is not allowed in the 'if' condition because it is always truthy. Allowed types are boolean or string.] + +if (get<"">()) {} + ~~~~~~~~~ [This type is not allowed in the 'if' condition because it is always falsy. Allowed types are boolean or string.] + +if (get<"foo" | "">()) {} diff --git a/test/rules/strict-boolean-expressions/allow-string/tslint.json b/test/rules/strict-boolean-expressions/allow-string/tslint.json new file mode 100644 index 00000000000..7b9da6c84fc --- /dev/null +++ b/test/rules/strict-boolean-expressions/allow-string/tslint.json @@ -0,0 +1,8 @@ +{ + "linterOptions": { + "typeCheck": true + }, + "rules": { + "strict-boolean-expressions": [true, "allow-string"] + } +} diff --git a/test/rules/strict-boolean-expressions/allow-undefined-union/test.ts.lint b/test/rules/strict-boolean-expressions/allow-undefined-union/test.ts.lint new file mode 100644 index 00000000000..12ff9344f46 --- /dev/null +++ b/test/rules/strict-boolean-expressions/allow-undefined-union/test.ts.lint @@ -0,0 +1,28 @@ +declare function get(): T; + +if (get()) {} + +declare const bu: boolean | undefined; +if (get()) {} + ~~~~~~~~~~~~~~~~~~~~~~~~~~ [This type is not allowed in the 'if' condition because it unions more than one truthy/falsy type. Allowed types are boolean or undefined-union.] + +// If it's always undefined, testing for it is wrong. +if (get()) {} + ~~~~~~~~~~~~~~~~ [This type is not allowed in the 'if' condition because it is always falsy. Allowed types are boolean or undefined-union.] + +if (get()) {} + ~~~~~~~~~~~ [This type is not allowed in the 'if' condition because it is always falsy. Allowed types are boolean or undefined-union.] + +if (get()) {} + ~~~~~~~~~~~~~~~~~~~~ [This type is not allowed in the 'if' condition because it could be null. Allowed types are boolean or undefined-union.] + +// Type of the condition is actually boolean | RegExp, but OK since we check each part separately. +if (get() || get()) {} + +// This still fails of course! +if (get() || get()) {} + ~~~~~~~~~~~~~ [This type is not allowed in the operand for the '||' operator because it is always truthy. Allowed types are boolean or undefined-union.] + ~~~~~~~~~~~~~ [This type is not allowed in the operand for the '||' operator because it is always truthy. Allowed types are boolean or undefined-union.] + +if (get()) {} + ~~~~~~~~~~~~~~~~~~~~~~~~~ [This type is not allowed in the 'if' condition because it could be a number. Allowed types are boolean or undefined-union.] diff --git a/test/rules/strict-boolean-expressions/allow-undefined-union/tsconfig.json b/test/rules/strict-boolean-expressions/allow-undefined-union/tsconfig.json new file mode 100644 index 00000000000..1cc3bc85ee9 --- /dev/null +++ b/test/rules/strict-boolean-expressions/allow-undefined-union/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "strictNullChecks": true + } +} \ No newline at end of file diff --git a/test/rules/strict-boolean-expressions/allow-undefined-union/tslint.json b/test/rules/strict-boolean-expressions/allow-undefined-union/tslint.json new file mode 100644 index 00000000000..b7dc81fb3b8 --- /dev/null +++ b/test/rules/strict-boolean-expressions/allow-undefined-union/tslint.json @@ -0,0 +1,8 @@ +{ + "linterOptions": { + "typeCheck": true + }, + "rules": { + "strict-boolean-expressions": [true, "allow-undefined-union"] + } +} diff --git a/test/rules/strict-boolean-expressions/default/test.ts.lint b/test/rules/strict-boolean-expressions/default/test.ts.lint new file mode 100644 index 00000000000..8c1ebe087ad --- /dev/null +++ b/test/rules/strict-boolean-expressions/default/test.ts.lint @@ -0,0 +1,181 @@ +class C { } +enum MyEnum { + A, B, C +} +let anyType: {}; +let boolType: boolean; +let boolType2: boolean; +let bwrapType: Boolean; +let numType = 9; +let strType = "string"; +let objType = new Object(); +let classType = new C(); +let enumType = MyEnum.A; +let boolFn = () => { return true; }; +let strFn = () => { return strType; }; +let numFn = () => { return numType; }; +let boolExpr = (strType !== undefined); + +/*** Binary Expressions ***/ +/*** Invalid Boolean Expressions ***/ +classType && boolType; +~~~~~~~~~ [This type is not allowed in the operand for the '&&' operator because it is always truthy. Only booleans are allowed.] +anyType && boolType; +~~~~~~~ [This type is not allowed in the operand for the '&&' operator because it is always truthy. Only booleans are allowed.] +numType && boolType; +~~~~~~~ [This type is not allowed in the operand for the '&&' operator because it is a number. Only booleans are allowed.] +boolType && strType; + ~~~~~~~ [This type is not allowed in the operand for the '&&' operator because it is a string. Only booleans are allowed.] +boolType && objType && enumType; + ~~~~~~~ [This type is not allowed in the operand for the '&&' operator because it is always truthy. Only booleans are allowed.] + ~~~~~~~~ [This type is not allowed in the operand for the '&&' operator because it could be an enum. Only booleans are allowed.] +bwrapType && boolType; +~~~~~~~~~ [This type is not allowed in the operand for the '&&' operator because it is always truthy. Only booleans are allowed.] + +boolType || classType; + ~~~~~~~~~ [This type is not allowed in the operand for the '||' operator because it is always truthy. Only booleans are allowed.] +boolType || anyType; + ~~~~~~~ [This type is not allowed in the operand for the '||' operator because it is always truthy. Only booleans are allowed.] +boolType || numType; + ~~~~~~~ [This type is not allowed in the operand for the '||' operator because it is a number. Only booleans are allowed.] +strType || boolType; +~~~~~~~ [This type is not allowed in the operand for the '||' operator because it is a string. Only booleans are allowed.] +bwrapType || boolType; +~~~~~~~~~ [This type is not allowed in the operand for the '||' operator because it is always truthy. Only booleans are allowed.] +objType || boolType || enumType; +~~~~~~~ [This type is not allowed in the operand for the '||' operator because it is always truthy. Only booleans are allowed.] + ~~~~~~~~ [This type is not allowed in the operand for the '||' operator because it could be an enum. Only booleans are allowed.] + +boolExpr && strType; + ~~~~~~~ [This type is not allowed in the operand for the '&&' operator because it is a string. Only booleans are allowed.] +numType || boolExpr; +~~~~~~~ [This type is not allowed in the operand for the '||' operator because it is a number. Only booleans are allowed.] +numType && boolExpr || strType; +~~~~~~~ [This type is not allowed in the operand for the '&&' operator because it is a number. Only booleans are allowed.] + ~~~~~~~ [This type is not allowed in the operand for the '||' operator because it is a string. Only booleans are allowed.] +bwrapType || boolExpr && bwrapType; +~~~~~~~~~ [This type is not allowed in the operand for the '||' operator because it is always truthy. Only booleans are allowed.] + ~~~~~~~~~ [This type is not allowed in the operand for the '&&' operator because it is always truthy. Only booleans are allowed.] + +/*** Valid Boolean Expressions ***/ +boolType && boolType2; +boolExpr || boolType; +(numType > 0) && boolFn(); +(strType !== "bool") && boolExpr; +(numType > 0) && (strType !== "bool"); +(strType !== undefined) || (numType < 0); + +/*** ConditionalExpression ***/ +/*** Invalid ***/ +strType ? strType : numType; +~~~~~~~ [This type is not allowed in the condition because it is a string. Only booleans are allowed.] +numType ? numType : numFn(); +~~~~~~~ [This type is not allowed in the condition because it is a number. Only booleans are allowed.] +objType ? objType : boolExpr; +~~~~~~~ [This type is not allowed in the condition because it is always truthy. Only booleans are allowed.] +classType ? strType : undefined; +~~~~~~~~~ [This type is not allowed in the condition because it is always truthy. Only booleans are allowed.] +bwrapType ? 1 : 0; +~~~~~~~~~ [This type is not allowed in the condition because it is always truthy. Only booleans are allowed.] +enumType ? 0 : 1; +~~~~~~~~ [This type is not allowed in the condition because it could be an enum. Only booleans are allowed.] + +/*** Valid ***/ +boolFn() ? numType : strType; +boolType ? strType : undefined; + +/*** PrefixUnary Expressions ***/ +/*** Invalid ***/ +!!numType; + ~~~~~~~ [This type is not allowed in the operand for the '!' operator because it is a number. Only booleans are allowed.] +!strType; + ~~~~~~~ [This type is not allowed in the operand for the '!' operator because it is a string. Only booleans are allowed.] +!objType; + ~~~~~~~ [This type is not allowed in the operand for the '!' operator because it is always truthy. Only booleans are allowed.] +!enumType; + ~~~~~~~~ [This type is not allowed in the operand for the '!' operator because it could be an enum. Only booleans are allowed.] +!!classType; + ~~~~~~~~~ [This type is not allowed in the operand for the '!' operator because it is always truthy. Only booleans are allowed.] +!bwrapType; + ~~~~~~~~~ [This type is not allowed in the operand for the '!' operator because it is always truthy. Only booleans are allowed.] +!!undefined; + ~~~~~~~~~~ [This type is not allowed in the operand for the '!' operator because it is always truthy. Only booleans are allowed.] + ~~~~~~~~~ [This type is not allowed in the operand for the '!' operator because it is always falsy. Only booleans are allowed.] + +/*** Valid ***/ +!!boolFn(); +!boolExpr; +!!boolType; + +/*** If Statement ***/ +/*** Invalid ***/ +if (numType) { /* statements */ } + ~~~~~~~ [This type is not allowed in the 'if' condition because it is a number. Only booleans are allowed.] +if (objType) { /* statements */ } + ~~~~~~~ [This type is not allowed in the 'if' condition because it is always truthy. Only booleans are allowed.] +if (strType) { /* statements */ } + ~~~~~~~ [This type is not allowed in the 'if' condition because it is a string. Only booleans are allowed.] +if (bwrapType) { /* statements */ } + ~~~~~~~~~ [This type is not allowed in the 'if' condition because it is always truthy. Only booleans are allowed.] +if (strFn()) { /* statements */ } + ~~~~~~~ [This type is not allowed in the 'if' condition because it is a string. Only booleans are allowed.] +if (MyEnum.A) { /* statements */ } + ~~~~~~~~ [This type is not allowed in the 'if' condition because it is an enum. Only booleans are allowed.] +if (classType) { /* statements */ } + ~~~~~~~~~ [This type is not allowed in the 'if' condition because it is always truthy. Only booleans are allowed.] + +/*** Valid ***/ +if (boolFn()) { /* statements */ } +if (boolExpr) { /* statements */ } +if (boolType) { /* statements */ } + +/*** While Statement ***/ +/*** Invalid ***/ +while (numType) { /* statements */ } + ~~~~~~~ [This type is not allowed in the 'while' condition because it is a number. Only booleans are allowed.] +while (objType) { /* statements */ } + ~~~~~~~ [This type is not allowed in the 'while' condition because it is always truthy. Only booleans are allowed.] +while (strType) { /* statements */ } + ~~~~~~~ [This type is not allowed in the 'while' condition because it is a string. Only booleans are allowed.] +while (strFn()) { /* statements */ } + ~~~~~~~ [This type is not allowed in the 'while' condition because it is a string. Only booleans are allowed.] +while (bwrapType) { /* statements */ } + ~~~~~~~~~ [This type is not allowed in the 'while' condition because it is always truthy. Only booleans are allowed.] +while (MyEnum.A) { /* statements */ } + ~~~~~~~~ [This type is not allowed in the 'while' condition because it is an enum. Only booleans are allowed.] +while (classType) { /* statements */ } + ~~~~~~~~~ [This type is not allowed in the 'while' condition because it is always truthy. Only booleans are allowed.] + +/*** Valid ***/ +while (boolFn()) { /* statements */ } +while (boolExpr) { /* statements */ } +while (boolType) { /* statements */ } + +/*** Do Statement ***/ +/*** Invalid ***/ +do { /* statements */ } while (numType); + ~~~~~~~ [This type is not allowed in the 'do-while' condition because it is a number. Only booleans are allowed.] +do { /* statements */ } while (objType); + ~~~~~~~ [This type is not allowed in the 'do-while' condition because it is always truthy. Only booleans are allowed.] +do { /* statements */ } while (strType); + ~~~~~~~ [This type is not allowed in the 'do-while' condition because it is a string. Only booleans are allowed.] +do { /* statements */ } while (bwrapType); + ~~~~~~~~~ [This type is not allowed in the 'do-while' condition because it is always truthy. Only booleans are allowed.] +do { /* statements */ } while (strFn()); + ~~~~~~~ [This type is not allowed in the 'do-while' condition because it is a string. Only booleans are allowed.] +do { /* statements */ } while (MyEnum.A); + ~~~~~~~~ [This type is not allowed in the 'do-while' condition because it is an enum. Only booleans are allowed.] +do { /* statements */ } while (classType); + ~~~~~~~~~ [This type is not allowed in the 'do-while' condition because it is always truthy. Only booleans are allowed.] + +/*** Valid ***/ +do { /* statements */ } while (boolFn()); +do { /* statements */ } while (boolExpr); +do { /* statements */ } while (boolType); + +/*** For Statement ***/ +/*** Invalid ***/ +for (let j = 0; j; j++) { /* statements */ } + ~ [This type is not allowed in the 'for' condition because it is a number. Only booleans are allowed.] +/*** Valid ***/ +for (let j = 0; j > numType; j++) { /* statements */ } \ No newline at end of file diff --git a/test/rules/strict-boolean-expressions/tslint.json b/test/rules/strict-boolean-expressions/default/tslint.json similarity index 100% rename from test/rules/strict-boolean-expressions/tslint.json rename to test/rules/strict-boolean-expressions/default/tslint.json diff --git a/test/rules/strict-boolean-expressions/no-allow-mix/test.ts.lint b/test/rules/strict-boolean-expressions/no-allow-mix/test.ts.lint new file mode 100644 index 00000000000..884aa5fcaa1 --- /dev/null +++ b/test/rules/strict-boolean-expressions/no-allow-mix/test.ts.lint @@ -0,0 +1,28 @@ +declare function get(): T; + +if (get()) {} + ~~~~~~~~~~~~~~~~~~~~~~ [This type is not allowed in the 'if' condition because it could be a number. Allowed types are boolean, undefined-union, or string.] + +if (get()) {} + ~~~~~~~~~~~~~~~~~~~~~~~ [mix] + +if (get()) {} + ~~~~~~~~~~~~~~~~~~~~~~~~~ [mix] + +if (get()) {} +if (get()) {} +if (get()) {} + +if (get<"" | "foo">()) {} + +// Mix of truthy values OK +if (get<"foo" | "bar" | undefined>()) {} + +// Mix of falsy values not OK +if (get<"" | boolean>()) {} + ~~~~~~~~~~~~~~~~~~~ [mix] + +// Mix of 1 falsy with any number of truthy OK +if (get()) {} + +[mix]: This type is not allowed in the 'if' condition because it unions more than one truthy/falsy type. Allowed types are boolean, undefined-union, or string. diff --git a/test/rules/strict-boolean-expressions/no-allow-mix/tsconfig.json b/test/rules/strict-boolean-expressions/no-allow-mix/tsconfig.json new file mode 100644 index 00000000000..1cc3bc85ee9 --- /dev/null +++ b/test/rules/strict-boolean-expressions/no-allow-mix/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "strictNullChecks": true + } +} \ No newline at end of file diff --git a/test/rules/strict-boolean-expressions/no-allow-mix/tslint.json b/test/rules/strict-boolean-expressions/no-allow-mix/tslint.json new file mode 100644 index 00000000000..ffbe242f4c9 --- /dev/null +++ b/test/rules/strict-boolean-expressions/no-allow-mix/tslint.json @@ -0,0 +1,8 @@ +{ + "linterOptions": { + "typeCheck": true + }, + "rules": { + "strict-boolean-expressions": [true, "allow-string", "allow-undefined-union"] + } +} diff --git a/test/rules/strict-boolean-expressions/test.ts.lint b/test/rules/strict-boolean-expressions/test.ts.lint deleted file mode 100644 index 0a308367281..00000000000 --- a/test/rules/strict-boolean-expressions/test.ts.lint +++ /dev/null @@ -1,179 +0,0 @@ -class C { } -enum MyEnum { - A, B, C -} -let anyType: {}; -let boolType: boolean; -let bwrapType: Boolean; -let numType = 9; -let strType = "string"; -let objType = new Object(); -let classType = new C(); -let enumType = MyEnum.A; -let boolFn = () => { return true; }; -let strFn = () => { return strType; }; -let numFn = () => { return numType; }; -let boolExpr = (strType !== undefined); - -/*** Binary Expressions ***/ -/*** Invalid Boolean Expressions ***/ -classType && boolType; -~~~~~~~~~ [Operands for the && or || operator should be of type boolean] -anyType && boolType; -~~~~~~~ [Operands for the && or || operator should be of type boolean] -numType && boolType; -~~~~~~~ [Operands for the && or || operator should be of type boolean] -boolType && strType; - ~~~~~~~ [Operands for the && or || operator should be of type boolean] -boolType && objType && enumType; - ~~~~~~~ [Operands for the && or || operator should be of type boolean] - ~~~~~~~~ [Operands for the && or || operator should be of type boolean] -bwrapType && boolType; -~~~~~~~~~ [Operands for the && or || operator should be of type boolean] - -boolType || classType; - ~~~~~~~~~ [Operands for the && or || operator should be of type boolean] -boolType || anyType; - ~~~~~~~ [Operands for the && or || operator should be of type boolean] -boolType || numType; - ~~~~~~~ [Operands for the && or || operator should be of type boolean] -strType || boolType; -~~~~~~~ [Operands for the && or || operator should be of type boolean] -bwrapType || boolType; -~~~~~~~~~ [Operands for the && or || operator should be of type boolean] -objType || boolType || enumType; -~~~~~~~ [Operands for the && or || operator should be of type boolean] - ~~~~~~~~ [Operands for the && or || operator should be of type boolean] - -boolExpr && strType; - ~~~~~~~ [Operands for the && or || operator should be of type boolean] -numType || boolExpr; -~~~~~~~ [Operands for the && or || operator should be of type boolean] -numType && boolExpr || strType; -~~~~~~~ [Operands for the && or || operator should be of type boolean] - ~~~~~~~ [Operands for the && or || operator should be of type boolean] -bwrapType || boolExpr && bwrapType; -~~~~~~~~~ [Operands for the && or || operator should be of type boolean] - ~~~~~~~~~ [Operands for the && or || operator should be of type boolean] - -/*** Valid Boolean Expressions ***/ -boolType && boolType; -boolExpr || boolType; -(numType > 0) && boolFn(); -(strType !== "bool") && boolExpr; -(numType > 0) && (strType !== "bool"); -(strType !== undefined) || (numType < 0); - -/*** ConditionalExpression ***/ -/*** Invalid ***/ -strType ? strType : numType; -~~~~~~~ [Condition needs to be a boolean expression or literal] -numType ? numType : numFn(); -~~~~~~~ [Condition needs to be a boolean expression or literal] -objType ? objType : boolExpr; -~~~~~~~ [Condition needs to be a boolean expression or literal] -classType ? strType : undefined; -~~~~~~~~~ [Condition needs to be a boolean expression or literal] -bwrapType ? 1 : 0; -~~~~~~~~~ [Condition needs to be a boolean expression or literal] -enumType ? 0 : 1; -~~~~~~~~ [Condition needs to be a boolean expression or literal] - -/*** Valid ***/ -boolFn() ? numType : strType; -boolType ? strType : undefined; - -/*** PrefixUnary Expressions ***/ -/*** Invalid ***/ -!!numType; - ~~~~~~~~ [Operand for the ! operator should be of type boolean] -!strType; -~~~~~~~~ [Operand for the ! operator should be of type boolean] -!objType; -~~~~~~~~ [Operand for the ! operator should be of type boolean] -!enumType; -~~~~~~~~~ [Operand for the ! operator should be of type boolean] -!!classType; - ~~~~~~~~~~ [Operand for the ! operator should be of type boolean] -!bwrapType; -~~~~~~~~~~ [Operand for the ! operator should be of type boolean] -!!undefined; - ~~~~~~~~~~ [Operand for the ! operator should be of type boolean] - -/*** Valid ***/ -!!boolFn(); -!boolExpr; -!!boolType; - -/*** If Statement ***/ -/*** Invalid ***/ -if (numType) { /* statements */ } - ~~~~~~~ [If statement condition needs to be a boolean expression or literal] -if (objType) { /* statements */ } - ~~~~~~~ [If statement condition needs to be a boolean expression or literal] -if (strType) { /* statements */ } - ~~~~~~~ [If statement condition needs to be a boolean expression or literal] -if (bwrapType) { /* statements */ } - ~~~~~~~~~ [If statement condition needs to be a boolean expression or literal] -if (strFn()) { /* statements */ } - ~~~~~~~ [If statement condition needs to be a boolean expression or literal] -if (MyEnum.A) { /* statements */ } - ~~~~~~~~ [If statement condition needs to be a boolean expression or literal] -if (classType) { /* statements */ } - ~~~~~~~~~ [If statement condition needs to be a boolean expression or literal] - -/*** Valid ***/ -if (boolFn()) { /* statements */ } -if (boolExpr) { /* statements */ } -if (boolType) { /* statements */ } - -/*** While Statement ***/ -/*** Invalid ***/ -while (numType) { /* statements */ } - ~~~~~~~ [While statement condition needs to be a boolean expression or literal] -while (objType) { /* statements */ } - ~~~~~~~ [While statement condition needs to be a boolean expression or literal] -while (strType) { /* statements */ } - ~~~~~~~ [While statement condition needs to be a boolean expression or literal] -while (strFn()) { /* statements */ } - ~~~~~~~ [While statement condition needs to be a boolean expression or literal] -while (bwrapType) { /* statements */ } - ~~~~~~~~~ [While statement condition needs to be a boolean expression or literal] -while (MyEnum.A) { /* statements */ } - ~~~~~~~~ [While statement condition needs to be a boolean expression or literal] -while (classType) { /* statements */ } - ~~~~~~~~~ [While statement condition needs to be a boolean expression or literal] - -/*** Valid ***/ -while (boolFn()) { /* statements */ } -while (boolExpr) { /* statements */ } -while (boolType) { /* statements */ } - -/*** Do Statement ***/ -/*** Invalid ***/ -do { /* statements */ } while (numType); - ~~~~~~~ [Do-While statement condition needs to be a boolean expression or literal] -do { /* statements */ } while (objType); - ~~~~~~~ [Do-While statement condition needs to be a boolean expression or literal] -do { /* statements */ } while (strType); - ~~~~~~~ [Do-While statement condition needs to be a boolean expression or literal] -do { /* statements */ } while (bwrapType); - ~~~~~~~~~ [Do-While statement condition needs to be a boolean expression or literal] -do { /* statements */ } while (strFn()); - ~~~~~~~ [Do-While statement condition needs to be a boolean expression or literal] -do { /* statements */ } while (MyEnum.A); - ~~~~~~~~ [Do-While statement condition needs to be a boolean expression or literal] -do { /* statements */ } while (classType); - ~~~~~~~~~ [Do-While statement condition needs to be a boolean expression or literal] - -/*** Valid ***/ -do { /* statements */ } while (boolFn()); -do { /* statements */ } while (boolExpr); -do { /* statements */ } while (boolType); - -/*** For Statement ***/ -/*** Invalid ***/ -for (let j = 0; j; j++) { /* statements */ } - ~ [For statement condition needs to be a boolean expression or literal] -/*** Valid ***/ -for (let j = 0; j > numType; j++) { /* statements */ } \ No newline at end of file From 8207effccbdd2ceeb5914a4d7463942425f0f6f5 Mon Sep 17 00:00:00 2001 From: Noah Chen Date: Thu, 19 Jan 2017 10:45:48 -0500 Subject: [PATCH 042/131] Remove "enable CircleCI" from template checklist --- .github/PULL_REQUEST_TEMPLATE.md | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 756f4b44388..d21f050c4e8 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -4,7 +4,6 @@ - [ ] New feature, bugfix, or enhancement - [ ] Includes tests - [ ] Documentation update -- [ ] Enable CircleCI for your fork (https://circleci.com/add-projects) #### What changes did you make? From 2aba847f23f459fa1d8bf1997ee88fbe4be372bc Mon Sep 17 00:00:00 2001 From: Noah Chen Date: Thu, 19 Jan 2017 23:15:15 -0500 Subject: [PATCH 043/131] Make `strict-boolean-expressions` test consistent w/ ts 2.0 (#2082) --- src/rules/strictBooleanExpressionsRule.ts | 2 +- .../strict-boolean-expressions/default/test.ts.lint | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/rules/strictBooleanExpressionsRule.ts b/src/rules/strictBooleanExpressionsRule.ts index a00c809af12..e48e0a84ff0 100644 --- a/src/rules/strictBooleanExpressionsRule.ts +++ b/src/rules/strictBooleanExpressionsRule.ts @@ -348,7 +348,7 @@ function stringOr(parts: string[]): string { } function isUnionType(type: ts.Type): type is ts.UnionType { - return Lint.isTypeFlagSet(type, ts.TypeFlags.Union); + return Lint.isTypeFlagSet(type, ts.TypeFlags.Union) && !Lint.isTypeFlagSet(type, ts.TypeFlags.Enum); } declare module "typescript" { diff --git a/test/rules/strict-boolean-expressions/default/test.ts.lint b/test/rules/strict-boolean-expressions/default/test.ts.lint index 8c1ebe087ad..17d3f2ede85 100644 --- a/test/rules/strict-boolean-expressions/default/test.ts.lint +++ b/test/rules/strict-boolean-expressions/default/test.ts.lint @@ -28,7 +28,7 @@ boolType && strType; ~~~~~~~ [This type is not allowed in the operand for the '&&' operator because it is a string. Only booleans are allowed.] boolType && objType && enumType; ~~~~~~~ [This type is not allowed in the operand for the '&&' operator because it is always truthy. Only booleans are allowed.] - ~~~~~~~~ [This type is not allowed in the operand for the '&&' operator because it could be an enum. Only booleans are allowed.] + ~~~~~~~~ [This type is not allowed in the operand for the '&&' operator because it is an enum. Only booleans are allowed.] bwrapType && boolType; ~~~~~~~~~ [This type is not allowed in the operand for the '&&' operator because it is always truthy. Only booleans are allowed.] @@ -44,7 +44,7 @@ bwrapType || boolType; ~~~~~~~~~ [This type is not allowed in the operand for the '||' operator because it is always truthy. Only booleans are allowed.] objType || boolType || enumType; ~~~~~~~ [This type is not allowed in the operand for the '||' operator because it is always truthy. Only booleans are allowed.] - ~~~~~~~~ [This type is not allowed in the operand for the '||' operator because it could be an enum. Only booleans are allowed.] + ~~~~~~~~ [This type is not allowed in the operand for the '||' operator because it is an enum. Only booleans are allowed.] boolExpr && strType; ~~~~~~~ [This type is not allowed in the operand for the '&&' operator because it is a string. Only booleans are allowed.] @@ -78,7 +78,7 @@ classType ? strType : undefined; bwrapType ? 1 : 0; ~~~~~~~~~ [This type is not allowed in the condition because it is always truthy. Only booleans are allowed.] enumType ? 0 : 1; -~~~~~~~~ [This type is not allowed in the condition because it could be an enum. Only booleans are allowed.] +~~~~~~~~ [This type is not allowed in the condition because it is an enum. Only booleans are allowed.] /*** Valid ***/ boolFn() ? numType : strType; @@ -93,7 +93,7 @@ boolType ? strType : undefined; !objType; ~~~~~~~ [This type is not allowed in the operand for the '!' operator because it is always truthy. Only booleans are allowed.] !enumType; - ~~~~~~~~ [This type is not allowed in the operand for the '!' operator because it could be an enum. Only booleans are allowed.] + ~~~~~~~~ [This type is not allowed in the operand for the '!' operator because it is an enum. Only booleans are allowed.] !!classType; ~~~~~~~~~ [This type is not allowed in the operand for the '!' operator because it is always truthy. Only booleans are allowed.] !bwrapType; @@ -178,4 +178,4 @@ do { /* statements */ } while (boolType); for (let j = 0; j; j++) { /* statements */ } ~ [This type is not allowed in the 'for' condition because it is a number. Only booleans are allowed.] /*** Valid ***/ -for (let j = 0; j > numType; j++) { /* statements */ } \ No newline at end of file +for (let j = 0; j > numType; j++) { /* statements */ } From 23f5f0004e6498736d5b55d1e314fe746404a251 Mon Sep 17 00:00:00 2001 From: Noah Chen Date: Fri, 20 Jan 2017 00:51:41 -0500 Subject: [PATCH 044/131] Make test output color with --test (#2083) --- src/test.ts | 3 +++ test/ruleTestRunner.ts | 3 --- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test.ts b/src/test.ts index d55952af6f8..ba657040dc1 100644 --- a/src/test.ts +++ b/src/test.ts @@ -45,6 +45,9 @@ export interface TestResult { } export function runTest(testDirectory: string, rulesDirectory?: string | string[]): TestResult { + // needed to get colors to show up when passing through Grunt + (colors as any).enabled = true; + const filesToLint = glob.sync(path.join(testDirectory, `**/*${MARKUP_FILE_EXTENSION}`)); const tslintConfig = Linter.findConfiguration(path.join(testDirectory, "tslint.json"), "").results; const tsConfig = path.join(testDirectory, "tsconfig.json"); diff --git a/test/ruleTestRunner.ts b/test/ruleTestRunner.ts index 250cfe30d1a..2f03216870a 100644 --- a/test/ruleTestRunner.ts +++ b/test/ruleTestRunner.ts @@ -20,9 +20,6 @@ import * as path from "path"; import {consoleTestResultHandler, runTest} from "../src/test"; -// needed to get colors to show up when passing through Grunt -(colors as any).enabled = true; - /* tslint:disable:no-console */ console.log(); console.log(colors.underline("Testing Lint Rules:")); From 85f539f7ad4ad7379ccff2b69154e32923877365 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Thu, 19 Jan 2017 23:03:31 -0800 Subject: [PATCH 045/131] Changed --test to take multiple directories (#2079) Fixes https://github.com/palantir/tslint/issues/2064. --- README.md | 9 +++++---- src/runner.ts | 6 +++--- src/test.ts | 17 +++++++++++++++++ src/tslint-cli.ts | 9 +++++---- 4 files changed, 30 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 70b6bbd648d..20292c0e56a 100644 --- a/README.md +++ b/README.md @@ -192,10 +192,11 @@ tslint accepts the following command-line options: option is set. --test: - Runs tslint on the specified directory and checks if tslint's output matches - the expected output in .lint files. Automatically loads the tslint.json file in the - specified directory as the configuration file for the tests. See the - full tslint documentation for more details on how this can be used to test custom rules. + Runs tslint on matched directories and checks if tslint outputs + match the expected output in .lint files. Automatically loads the + tslint.json files in the directories as the configuration file for + the tests. See the full tslint documentation for more details on how + this can be used to test custom rules. --project: The location of a tsconfig.json file that will be used to determine which diff --git a/src/runner.ts b/src/runner.ts index 49160371f33..5e767d3e651 100644 --- a/src/runner.ts +++ b/src/runner.ts @@ -27,7 +27,7 @@ import { } from "./configuration"; import { FatalError } from "./error"; import * as Linter from "./linter"; -import { consoleTestResultHandler, runTest } from "./test"; +import { consoleTestResultsHandler, runTests } from "./test"; import { updateNotifierCheck } from "./updateNotifier"; export interface IRunnerOptions { @@ -130,8 +130,8 @@ export class Runner { } if (this.options.test) { - const results = runTest(this.options.test, this.options.rulesDirectory); - const didAllTestsPass = consoleTestResultHandler(results); + const results = runTests(this.options.test, this.options.rulesDirectory); + const didAllTestsPass = consoleTestResultsHandler(results); onComplete(didAllTestsPass ? 0 : 1); return; } diff --git a/src/test.ts b/src/test.ts index ba657040dc1..9c4887352aa 100644 --- a/src/test.ts +++ b/src/test.ts @@ -44,6 +44,11 @@ export interface TestResult { }; } +export function runTests(pattern: string, rulesDirectory?: string | string[]): TestResult[] { + return glob.sync(`${pattern}/tslint.json`) + .map((directory: string): TestResult => runTest(path.dirname(directory), rulesDirectory)); +} + export function runTest(testDirectory: string, rulesDirectory?: string | string[]): TestResult { // needed to get colors to show up when passing through Grunt (colors as any).enabled = true; @@ -162,6 +167,18 @@ export function runTest(testDirectory: string, rulesDirectory?: string | string[ return results; } +export function consoleTestResultsHandler(testResults: TestResult[]): boolean { + let didAllTestsPass = true; + + for (const testResult of testResults) { + if (!consoleTestResultHandler(testResult)) { + didAllTestsPass = false; + } + } + + return didAllTestsPass; +} + export function consoleTestResultHandler(testResult: TestResult): boolean { let didAllTestsPass = true; diff --git a/src/tslint-cli.ts b/src/tslint-cli.ts index 4d4bfd1cc0e..8fbfbb6b87f 100644 --- a/src/tslint-cli.ts +++ b/src/tslint-cli.ts @@ -180,10 +180,11 @@ tslint accepts the following commandline options: option is set. --test: - Runs tslint on the specified directory and checks if tslint's output matches - the expected output in .lint files. Automatically loads the tslint.json file in the - specified directory as the configuration file for the tests. See the - full tslint documentation for more details on how this can be used to test custom rules. + Runs tslint on matched directories and checks if tslint outputs + match the expected output in .lint files. Automatically loads the + tslint.json files in the directories as the configuration file for + the tests. See the full tslint documentation for more details on how + this can be used to test custom rules. --project: The location of a tsconfig.json file that will be used to determine which From 6194bb11aa22ac67c74556a40166cc87e906a45a Mon Sep 17 00:00:00 2001 From: Klaus Meinhardt Date: Fri, 20 Jan 2017 14:57:11 +0100 Subject: [PATCH 046/131] Fix no-trailing-whitespace for comments and EOF (#2060) Detect trailing whitespace in comments. Detect trailing whitespace before EOF. Add fixer. Fixes: #2049 --- src/rules/noTrailingWhitespaceRule.ts | 41 +++++++++++++++++-- test/rules/no-trailing-whitespace/test.js.fix | 10 +++++ test/rules/no-trailing-whitespace/test.ts.fix | 20 +++++++++ .../rules/no-trailing-whitespace/test.ts.lint | 26 +++++++++--- 4 files changed, 88 insertions(+), 9 deletions(-) create mode 100644 test/rules/no-trailing-whitespace/test.js.fix create mode 100644 test/rules/no-trailing-whitespace/test.ts.fix diff --git a/src/rules/noTrailingWhitespaceRule.ts b/src/rules/noTrailingWhitespaceRule.ts index ca5a52985c7..6cc85e2eacd 100644 --- a/src/rules/noTrailingWhitespaceRule.ts +++ b/src/rules/noTrailingWhitespaceRule.ts @@ -26,6 +26,7 @@ export class Rule extends Lint.Rules.AbstractRule { description: "Disallows trailing whitespace at the end of a line.", rationale: "Keeps version control diffs clean as it prevents accidental whitespace from being committed.", optionsDescription: "Not configurable.", + hasFix: true, options: null, optionExamples: ["true"], type: "maintainability", @@ -44,18 +45,52 @@ class NoTrailingWhitespaceWalker extends Lint.RuleWalker { public visitSourceFile(node: ts.SourceFile) { let lastSeenWasWhitespace = false; let lastSeenWhitespacePosition = 0; - Lint.forEachToken(node, false, (_text, kind, pos) => { - if (kind === ts.SyntaxKind.NewLineTrivia) { + Lint.forEachToken(node, false, (fullText, kind, pos) => { + if (kind === ts.SyntaxKind.NewLineTrivia || kind === ts.SyntaxKind.EndOfFileToken) { if (lastSeenWasWhitespace) { - this.addFailureFromStartToEnd(lastSeenWhitespacePosition, pos.tokenStart, Rule.FAILURE_STRING); + this.reportFailure(lastSeenWhitespacePosition, pos.tokenStart); } lastSeenWasWhitespace = false; } else if (kind === ts.SyntaxKind.WhitespaceTrivia) { lastSeenWasWhitespace = true; lastSeenWhitespacePosition = pos.tokenStart; } else { + if (kind === ts.SyntaxKind.SingleLineCommentTrivia) { + const commentText = fullText.substring(pos.tokenStart + 2, pos.end); + const match = /\s+$/.exec(commentText); + if (match !== null) { + this.reportFailure(pos.end - match[0].length, pos.end); + } + } else if (kind === ts.SyntaxKind.MultiLineCommentTrivia) { + let startPos = pos.tokenStart + 2; + const commentText = fullText.substring(startPos, pos.end - 2); + const lines = commentText.split("\n"); + // we don't want to check the content of the last comment line, as it is always followed by */ + const len = lines.length - 1; + for (let i = 0; i < len; ++i) { + let line = lines[i]; + // remove carriage return at the end, it is does not account to trailing whitespace + if (line.endsWith("\r")) { + line = line.substr(0, line.length - 1); + } + const start = line.search(/\s+$/); + if (start !== -1) { + this.reportFailure(startPos + start, startPos + line.length); + } + startPos += lines[i].length + 1; + } + } lastSeenWasWhitespace = false; } }); } + + private reportFailure(start: number, end: number) { + this.addFailureFromStartToEnd( + start, + end, + Rule.FAILURE_STRING, + this.createFix(this.deleteText(start, end - start)), + ); + } } diff --git a/test/rules/no-trailing-whitespace/test.js.fix b/test/rules/no-trailing-whitespace/test.js.fix new file mode 100644 index 00000000000..c2b659fb154 --- /dev/null +++ b/test/rules/no-trailing-whitespace/test.js.fix @@ -0,0 +1,10 @@ +class Clazz { + public funcxion() { + console.log("test") ; + } + + + private foobar() { + } +} + diff --git a/test/rules/no-trailing-whitespace/test.ts.fix b/test/rules/no-trailing-whitespace/test.ts.fix new file mode 100644 index 00000000000..9ca5d289dde --- /dev/null +++ b/test/rules/no-trailing-whitespace/test.ts.fix @@ -0,0 +1,20 @@ +class Clazz { + public funcxion() { + console.log("test") ; + } + + + private foobar() { + } +} +// single line comment without trailing whitespace +// single line comment with trailing whitespace + /* single line multiline comment */ +/* whitespace after comment */ +/* first line has trailing whitespace + second line is ok + last line is not checked */ +/* + */ + +// following line checks for trailing whitespace before EOF diff --git a/test/rules/no-trailing-whitespace/test.ts.lint b/test/rules/no-trailing-whitespace/test.ts.lint index 6ef8b5e18b4..7067008dc5d 100644 --- a/test/rules/no-trailing-whitespace/test.ts.lint +++ b/test/rules/no-trailing-whitespace/test.ts.lint @@ -1,16 +1,30 @@ class Clazz { public funcxion() { - ~~~~ [0] + ~~~~ [trailing whitespace] console.log("test") ; - ~~~~ [0] + ~~~~ [trailing whitespace] } -~~~~ [0] +~~~~ [trailing whitespace] -~~~~ [0] +~~~~ [trailing whitespace] private foobar() { } } - ~~~~ [0] + ~~~~ [trailing whitespace] +// single line comment without trailing whitespace +// single line comment with trailing whitespace + ~~~ [trailing whitespace] + /* single line multiline comment */ +/* whitespace after comment */ + ~ [trailing whitespace] +/* first line has trailing whitespace + ~~ [trailing whitespace] + second line is ok + last line is not checked */ +/* + */ -[0]: trailing whitespace +// following line checks for trailing whitespace before EOF + +~~~ [trailing whitespace] \ No newline at end of file From d65114566808959c3d7ce423b68b4474f4edd232 Mon Sep 17 00:00:00 2001 From: Klaus Meinhardt Date: Fri, 20 Jan 2017 14:57:47 +0100 Subject: [PATCH 047/131] Enhance NoEmptyRule (#2061) --- src/language/utils.ts | 2 +- src/rules/noEmptyRule.ts | 25 ++++++++++++++----------- test/rules/no-empty/test.ts.lint | 18 ++++++++++++++++++ 3 files changed, 33 insertions(+), 12 deletions(-) diff --git a/src/language/utils.ts b/src/language/utils.ts index fd122a9910a..93ee4091a3e 100644 --- a/src/language/utils.ts +++ b/src/language/utils.ts @@ -413,5 +413,5 @@ function canHaveTrailingTrivia(tokenKind: ts.SyntaxKind, parent: ts.Node): boole */ export function hasCommentAfterPosition(text: string, position: number): boolean { return ts.getTrailingCommentRanges(text, position) !== undefined || - ts.getTrailingCommentRanges(text, position) !== undefined; + ts.getLeadingCommentRanges(text, position) !== undefined; } diff --git a/src/rules/noEmptyRule.ts b/src/rules/noEmptyRule.ts index 1b384227b40..bd1bef5ce0e 100644 --- a/src/rules/noEmptyRule.ts +++ b/src/rules/noEmptyRule.ts @@ -43,17 +43,13 @@ export class Rule extends Lint.Rules.AbstractRule { class BlockWalker extends Lint.RuleWalker { public visitBlock(node: ts.Block) { - if (node.statements.length === 0 && !isConstructorWithParameterProperties(node.parent!)) { + if (node.statements.length === 0 && !isExcludedConstructor(node.parent!)) { const sourceFile = this.getSourceFile(); - const children = node.getChildren(sourceFile); - const openBrace = children[0]; - const closeBrace = children[children.length - 1]; - const sourceFileText = sourceFile.text; - - if (ts.getLeadingCommentRanges(sourceFileText, closeBrace.getFullStart()) === undefined && - ts.getTrailingCommentRanges(sourceFileText, openBrace.getEnd()) === undefined) { - - this.addFailureAtNode(node, Rule.FAILURE_STRING); + const start = node.getStart(sourceFile); + // Block always starts with open brace. Adding 1 to its start gives us the end of the brace, + // which can be used to conveniently check for comments between braces + if (!Lint.hasCommentAfterPosition(sourceFile.text, start + 1)) { + this.addFailureFromStartToEnd(start , node.getEnd(), Rule.FAILURE_STRING); } } @@ -61,8 +57,15 @@ class BlockWalker extends Lint.RuleWalker { } } -function isConstructorWithParameterProperties(node: ts.Node): boolean { +function isExcludedConstructor(node: ts.Node): boolean { if (node.kind === ts.SyntaxKind.Constructor) { + if (Lint.hasModifier(node.modifiers, ts.SyntaxKind.PrivateKeyword, ts.SyntaxKind.ProtectedKeyword)) { + /* If constructor is private or protected, the block is allowed to be empty. + The constructor is there on purpose to disallow instantiation from outside the class */ + /* The public modifier does not serve a purpose here. It can only be used to allow instantiation of a base class where + the super constructor is protected. But then the block would not be empty, because of the call to super() */ + return true; + } for (const parameter of (node as ts.ConstructorDeclaration).parameters) { if (Lint.hasModifier(parameter.modifiers, ts.SyntaxKind.PrivateKeyword, diff --git a/test/rules/no-empty/test.ts.lint b/test/rules/no-empty/test.ts.lint index 9abc44e9d88..1846cc5ff6f 100644 --- a/test/rules/no-empty/test.ts.lint +++ b/test/rules/no-empty/test.ts.lint @@ -23,6 +23,11 @@ for (var x = 0; x < 1; ++x) { } for (var y = 0; y < 1; ++y) { // empty here } +{ // empty block allowed +} +{ + /* this block is also empty, but allowed to be */ +} class testClass { constructor(private allowed: any, private alsoAllowed: any) { @@ -45,3 +50,16 @@ class testClass4 { constructor(readonly allowed: any) { } } + +class PrivateClassConstructor { + private constructor() {} +} + +class ProtectedClassConstructor { + protected constructor() {} +} + +class PublicClassConstructor { + public constructor() {} + ~~ [block is empty] +} From 8fbcf3e63c966b2a12a4e98da7b3dde27f06b057 Mon Sep 17 00:00:00 2001 From: Noah Chen Date: Fri, 20 Jan 2017 20:41:23 -0500 Subject: [PATCH 048/131] Fail tests if CIRCLE_NODE_TOTAL is < 4 (#2084) --- circle.yml | 1 + scripts/assertMinCircleNodes.js | 8 ++++++++ 2 files changed, 9 insertions(+) create mode 100644 scripts/assertMinCircleNodes.js diff --git a/circle.yml b/circle.yml index 4b2d8cf7528..358b6f4aff7 100644 --- a/circle.yml +++ b/circle.yml @@ -4,6 +4,7 @@ general: - gh-pages dependencies: pre: + - node ./scripts/assertMinCircleNodes.js $CIRCLE_NODE_TOTAL - case $CIRCLE_NODE_INDEX in 0) nvm use 4.2 ;; 1) nvm use 5.7 ;; [2-3]) nvm use 6.1 ;; esac test: override: diff --git a/scripts/assertMinCircleNodes.js b/scripts/assertMinCircleNodes.js new file mode 100644 index 00000000000..449864e72e7 --- /dev/null +++ b/scripts/assertMinCircleNodes.js @@ -0,0 +1,8 @@ +var requiredNodes = 4; +var nodes = parseInt(process.argv[2], 10); +if (requiredNodes != null && requiredNodes > nodes) { + console.error("ERROR: You must run CircleCI with 4 parallel nodes"); + console.error(" This ensures that different environments are tested for TSLint compatibility"); + console.error(" https://circleci.com/gh//tslint/edit#parallel-builds"); + process.exit(1); +} From 176afbe893cda2c291d041adf76082a4e8136eae Mon Sep 17 00:00:00 2001 From: Krati Ahuja Date: Fri, 20 Jan 2017 17:47:24 -0800 Subject: [PATCH 049/131] Fix README to correctly show example of using tslint as library (#2043) Explicitly call `findConfiguration` before calling `lint` API so that `extends` key is resolved correctly. --- README.md | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 20292c0e56a..64ad94193e1 100644 --- a/README.md +++ b/README.md @@ -216,26 +216,21 @@ tslint accepts the following command-line options: #### Library ```js -import { Linter } from "tslint"; +import { Linter, Configuration } from "tslint"; import * as fs from "fs"; -const fileName = "Specify file name"; -const configuration = { - rules: { - "variable-name": true, - "quotemark": [true, "double"] - } -}; +const fileName = "Specify input file name"; +const configurationFilename = "Specify configuration file name"; const options = { formatter: "json", - configuration: configuration, rulesDirectory: "customRules/", formattersDirectory: "customFormatters/" }; const fileContents = fs.readFileSync(fileName, "utf8"); -const linter = new Linter(fileName, fileContents, options); -const result = linter.lint(); +const linter = new Linter(options); +const configLoad = Configuration.findConfiguration(configurationFilename, filename); +const result = linter.lint(fileName, fileContents, configLoad.results); ``` #### Type Checking @@ -315,10 +310,10 @@ TSLint ships with a set of core rules that can be configured. However, users are Let us take the example of how to write a new rule to forbid all import statements (you know, *for science*). Let us name the rule file `noImportsRule.ts`. Rules are referenced in `tslint.json` with their kebab-cased identifer, so `"no-imports": true` would configure the rule. -__Important conventions__: +__Important conventions__: * Rule identifiers are always kebab-cased. * Rule files are always camel-cased (`camelCasedRule.ts`). -* Rule files *must* contain the suffix `Rule`. +* Rule files *must* contain the suffix `Rule`. * The exported class must always be named `Rule` and extend from `Lint.Rules.AbstractRule`. Now, let us first write the rule in TypeScript: From 595b1dbe332218d8c7ecadab9ee1a9f486233217 Mon Sep 17 00:00:00 2001 From: Klaus Meinhardt Date: Sat, 21 Jan 2017 03:02:57 +0100 Subject: [PATCH 050/131] Rewrite and fix EnableDisableRulesWalker (#2062) * Clarify docs * Rewrite EnableDisableRulesWalker * Simplify regex --- docs/usage/rule-flags/index.md | 2 +- src/enableDisableRules.ts | 170 +++++++++--------- src/linter.ts | 8 +- .../_integration/enable-disable/test.ts.lint | 50 +++++- .../_integration/enable-disable/tslint.json | 6 +- 5 files changed, 135 insertions(+), 101 deletions(-) diff --git a/docs/usage/rule-flags/index.md b/docs/usage/rule-flags/index.md index bf2d5739d97..2237dd673d4 100644 --- a/docs/usage/rule-flags/index.md +++ b/docs/usage/rule-flags/index.md @@ -15,7 +15,7 @@ You can enable/disable TSLint or a subset of rules within a file with the follow * `// tslint:disable-next-line:rule1 rule2 rule3...` - Disables the listed rules for the next line * etc. -Rules flags enable or disable rules as they are parsed. Disabling an already disabled rule or enabling an already enabled rule has no effect. +Rules flags enable or disable rules as they are parsed. Disabling an already disabled rule or enabling an already enabled rule has no effect. Enabling a rule that is not present or disabled in `tslint.json` has also no effect. For example, imagine the directive `/* tslint:disable */` on the first line of a file, `/* tslint:enable:ban class-name */` on the 10th line and `/* tslint:enable */` on the 20th. No rules will be checked between the 1st and 10th lines, only the `ban` and `class-name` rules will be checked between the 10th and 20th, and all rules will be checked for the remainder of the file. diff --git a/src/enableDisableRules.ts b/src/enableDisableRules.ts index 6c819f7ff44..031ad1af737 100644 --- a/src/enableDisableRules.ts +++ b/src/enableDisableRules.ts @@ -18,20 +18,20 @@ import * as ts from "typescript"; import {AbstractRule} from "./language/rule/abstractRule"; -import {IOptions} from "./language/rule/rule"; import {forEachComment, TokenPosition} from "./language/utils"; -import {RuleWalker} from "./language/walker/ruleWalker"; import {IEnableDisablePosition} from "./ruleLoader"; -export class EnableDisableRulesWalker extends RuleWalker { - public enableDisableRuleMap: {[rulename: string]: IEnableDisablePosition[]} = {}; - - constructor(sourceFile: ts.SourceFile, options: IOptions, rules: {[name: string]: any}) { - super(sourceFile, options); +export class EnableDisableRulesWalker { + private enableDisableRuleMap: {[rulename: string]: IEnableDisablePosition[]}; + private enabledRules: string[]; + constructor(private sourceFile: ts.SourceFile, rules: {[name: string]: any}) { + this.enableDisableRuleMap = {}; + this.enabledRules = []; if (rules) { - for (const rule in rules) { - if (rules.hasOwnProperty(rule) && AbstractRule.isRuleEnabled(rules[rule])) { + for (const rule of Object.keys(rules)) { + if (AbstractRule.isRuleEnabled(rules[rule])) { + this.enabledRules.push(rule); this.enableDisableRuleMap[rule] = [{ isEnabled: true, position: 0, @@ -41,24 +41,35 @@ export class EnableDisableRulesWalker extends RuleWalker { } } - public visitSourceFile(node: ts.SourceFile) { - forEachComment(node, (fullText, _kind, pos) => { - return this.handlePossibleTslintSwitch(fullText.substring(pos.tokenStart, pos.end), node, pos); + public getEnableDisableRuleMap() { + forEachComment(this.sourceFile, (fullText, kind, pos) => { + const commentText = kind === ts.SyntaxKind.SingleLineCommentTrivia + ? fullText.substring(pos.tokenStart + 2, pos.end) + : fullText.substring(pos.tokenStart + 2, pos.end - 2); + return this.handleComment(commentText, pos); }); + + return this.enableDisableRuleMap; } - private getStartOfLinePosition(node: ts.SourceFile, position: number, lineOffset = 0) { - const line = ts.getLineAndCharacterOfPosition(node, position).line + lineOffset; - const lineStarts = node.getLineStarts(); + private getStartOfLinePosition(position: number, lineOffset = 0) { + const line = ts.getLineAndCharacterOfPosition(this.sourceFile, position).line + lineOffset; + const lineStarts = this.sourceFile.getLineStarts(); if (line >= lineStarts.length) { // next line ends with eof or there is no next line - return node.getFullWidth(); + // undefined switches the rule until the end and avoids an extra array entry + return undefined; } return lineStarts[line]; } private switchRuleState(ruleName: string, isEnabled: boolean, start: number, end?: number): void { const ruleStateMap = this.enableDisableRuleMap[ruleName]; + if (ruleStateMap === undefined || // skip switches for unknown or disabled rules + isEnabled === ruleStateMap[ruleStateMap.length - 1].isEnabled // no need to add switch points if there is no change + ) { + return; + } ruleStateMap.push({ isEnabled, @@ -66,7 +77,7 @@ export class EnableDisableRulesWalker extends RuleWalker { }); if (end) { - // switchRuleState method is only called when rule state changes therefore we can safely use opposite state + // we only get here when rule state changes therefore we can safely use opposite state ruleStateMap.push({ isEnabled: !isEnabled, position: end, @@ -74,83 +85,62 @@ export class EnableDisableRulesWalker extends RuleWalker { } } - private getLatestRuleState(ruleName: string): boolean { - const ruleStateMap = this.enableDisableRuleMap[ruleName]; + private handleComment(commentText: string, pos: TokenPosition) { + // regex is: start of string followed by any amount of whitespace + // followed by tslint and colon + // followed by either "enable" or "disable" + // followed optionally by -line or -next-line + // followed by either colon, whitespace or end of string + const match = /^\s*tslint:(enable|disable)(?:-(line|next-line))?(:|\s|$)/.exec(commentText); + if (match !== null) { + // remove everything matched by the previous regex to get only the specified rules + // split at whitespaces + // filter empty items coming from whitespaces at start, at end or empty list + let rulesList = commentText.substr(match[0].length) + .split(/\s+/) + .filter((rule) => !!rule); + if (rulesList.length === 0 && match[3] === ":") { + // nothing to do here: an explicit separator was specified but no rules to switch + return; + } + if (rulesList.length === 0 || + rulesList.indexOf("all") !== -1) { + // if list is empty we default to all enabled rules + // if `all` is specified we ignore the other rules and take all enabled rules + rulesList = this.enabledRules; + } - return ruleStateMap[ruleStateMap.length - 1].isEnabled; + this.handleTslintLineSwitch(rulesList, match[1] === "enable", match[2], pos); + } } - private handlePossibleTslintSwitch(commentText: string, node: ts.SourceFile, pos: TokenPosition) { - // regex is: start of string followed by "/*" or "//" followed by any amount of whitespace followed by "tslint:" - if (commentText.match(/^(\/\*|\/\/)\s*tslint:/)) { - const commentTextParts = commentText.split(":"); - // regex is: start of string followed by either "enable" or "disable" - // followed optionally by -line or -next-line - // followed by either whitespace or end of string - const enableOrDisableMatch = commentTextParts[1].match(/^(enable|disable)(-(line|next-line))?(\s|$)/); - - if (enableOrDisableMatch != null) { - const isEnabled = enableOrDisableMatch[1] === "enable"; - const isCurrentLine = enableOrDisableMatch[3] === "line"; - const isNextLine = enableOrDisableMatch[3] === "next-line"; - - let rulesList = ["all"]; - - if (commentTextParts.length === 2) { - // an implicit whitespace separator is used for the rules list. - rulesList = commentTextParts[1].split(/\s+/).slice(1); - - // remove empty items and potential comment end. - rulesList = rulesList.filter((item) => !!item && !item.includes("*/")); - - // potentially there were no items, so default to `all`. - rulesList = rulesList.length > 0 ? rulesList : ["all"]; - } else if (commentTextParts.length > 2) { - // an explicit separator was specified for the rules list. - rulesList = commentTextParts[2].split(/\s+/); - } - - if (rulesList.indexOf("all") !== -1) { - // iterate over all enabled rules - rulesList = Object.keys(this.enableDisableRuleMap); - } - - for (const ruleToSwitch of rulesList) { - if (!(ruleToSwitch in this.enableDisableRuleMap)) { - // all rules enabled in configuration are already in map - skip switches for disabled rules - continue; - } - - const previousState = this.getLatestRuleState(ruleToSwitch); - - if (previousState === isEnabled) { - // no need to add switch points if there is no change in rule state - continue; - } - - let start: number; - let end: number | undefined; - - if (isCurrentLine) { - // start at the beginning of the current line - start = this.getStartOfLinePosition(node, pos.tokenStart); - // end at the beginning of the next line - end = pos.end + 1; - } else if (isNextLine) { - // start at the current position - start = pos.tokenStart; - // end at the beginning of the line following the next line - end = this.getStartOfLinePosition(node, pos.tokenStart, 2); - } else { - // disable rule for the rest of the file - // start at the current position, but skip end position - start = pos.tokenStart; - end = undefined; - } - - this.switchRuleState(ruleToSwitch, isEnabled, start, end); - } + private handleTslintLineSwitch(rules: string[], isEnabled: boolean, modifier: string, pos: TokenPosition) { + let start: number | undefined; + let end: number | undefined; + + if (modifier === "line") { + // start at the beginning of the line where comment starts + start = this.getStartOfLinePosition(pos.tokenStart)!; + // end at the beginning of the line following the comment + end = this.getStartOfLinePosition(pos.end, 1); + } else if (modifier === "next-line") { + // start at the beginning of the line following the comment + start = this.getStartOfLinePosition(pos.end, 1); + if (start === undefined) { + // no need to switch anything, there is no next line + return; } + // end at the beginning of the line following the next line + end = this.getStartOfLinePosition(pos.end, 2); + } else { + // switch rule for the rest of the file + // start at the current position, but skip end position + start = pos.tokenStart; + end = undefined; + } + + for (const ruleToSwitch of rules) { + this.switchRuleState(ruleToSwitch, isEnabled, start, end); } } } diff --git a/src/linter.ts b/src/linter.ts index 322a2d0f915..e1a7c007176 100644 --- a/src/linter.ts +++ b/src/linter.ts @@ -180,13 +180,7 @@ class Linter { const configurationRules = isJs ? configuration.jsRules : configuration.rules; // walk the code first to find all the intervals where rules are disabled - const rulesWalker = new EnableDisableRulesWalker(sourceFile, { - disabledIntervals: [], - ruleArguments: [], - ruleName: "", - }, configurationRules); - rulesWalker.walk(sourceFile); - const enableDisableRuleMap = rulesWalker.enableDisableRuleMap; + const enableDisableRuleMap = new EnableDisableRulesWalker(sourceFile, configurationRules).getEnableDisableRuleMap(); const rulesDirectories = arrayify(this.options.rulesDirectory) .concat(arrayify(configuration.rulesDirectory)); diff --git a/test/rules/_integration/enable-disable/test.ts.lint b/test/rules/_integration/enable-disable/test.ts.lint index fd0dd3694df..98d9462aca7 100644 --- a/test/rules/_integration/enable-disable/test.ts.lint +++ b/test/rules/_integration/enable-disable/test.ts.lint @@ -65,11 +65,11 @@ var AAAaA = 'test' /* tslint:disable:quotemark */ var s = 'xxx'; -//Test case for issue #1624 +// Test case for issue #1624 // tslint:disable:quotemark variable-name var AAAaA = 'test' // tslint:disable-line:quotemark // tslint:disable-next-line:variable-name -var AAAaA = 'test' //previously `quotemark` rule was enabled after previous line +var AAAaA = 'test' // previously `quotemark` rule was enabled after previous line var AAAaA = 'test' // previously both `quotemark` and `variable-name` rules were enabled after previous lines @@ -80,3 +80,49 @@ var AAAaA = 'test' // ensure that disable-line rule correctly handles previous ` // tslint:enable:no-var-keyword var AAAaA = 'test' // ensure that disabled in config rule isn't enabled + +/* tslint:enable-next-line: */ +var AAAaA = 'test' // ensure that nothing is enabled with explicit separator and empty list of rules + +/* tslint:enable-next-line quotemark*/ +var AAAaA = 'test' // ensure that comment end is handled correctly with implicit separator + ~~~~~~ [' should be "] + +/* tslint:enable-next-line:quotemark*/ +var AAAaA = 'test' // ensure that comment end is handled correctly with explicit separator + ~~~~~~ [' should be "] + +// ensure that line switches switch the whole line +var AAAaA = 'test'; /* tslint:enable-line quotemark */ var AAAaA = 'test' + ~~~~~~ [' should be "] + ~~~~~~ [' should be "] +var AAAaA = 'test' // line should not be affected + +// ensure that next-line switch only switches next line +var AAAaA = 'test'; /*tslint:enable-next-line*/ var AAAaA = 'test' +var AAAaA = 'test' + ~~~~~ [variable name must be in camelcase or uppercase] + ~~~~~~ [' should be "] +var AAAaA = 'test' +var AAAaA = 'test'; //tslint:enable-next-line + + +// ensure that enable/disable switch switches at start of comment +var AAAaA = 'test'; //tslint:enable + ~~~~~~~~~~~~~ [comment must start with a space] +var AAAaA = 'test'; //tslint:disable + ~~~~~ [variable name must be in camelcase or uppercase] + ~~~~~~ [' should be "] +var AAAaA = 'test' + +// ensure that "all" switches all rules +var AAAaA = 'test'; //tslint:enable-line all + ~~~~~ [variable name must be in camelcase or uppercase] + ~~~~~~ [' should be "] + ~~~~~~~~~~~~~~~~~~~~~~ [comment must start with a space] + +// ensure that "all" switches all rules even if others are in the list +var AAAaA = 'test'; //tslint:enable-line quotemark all + ~~~~~ [variable name must be in camelcase or uppercase] + ~~~~~~ [' should be "] + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [comment must start with a space] diff --git a/test/rules/_integration/enable-disable/tslint.json b/test/rules/_integration/enable-disable/tslint.json index 6153bd7842b..7038f339ba2 100644 --- a/test/rules/_integration/enable-disable/tslint.json +++ b/test/rules/_integration/enable-disable/tslint.json @@ -2,6 +2,10 @@ "rules": { "quotemark": [true, "double"], "variable-name": true, - "no-var-keyword": false + "no-var-keyword": false, + "comment-format": [ + true, + "check-space" + ] } } From c2c00546aba8805c5c78e7a66b9f5d6156fb23df Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Fri, 20 Jan 2017 18:04:02 -0800 Subject: [PATCH 051/131] Added visitNumericLiteral to SyntaxWalkre (#2075) Fixes https://github.com/palantir/tslint/issues/2074. --- src/language/walker/syntaxWalker.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/language/walker/syntaxWalker.ts b/src/language/walker/syntaxWalker.ts index c5ec8aace5a..acee72b0e09 100644 --- a/src/language/walker/syntaxWalker.ts +++ b/src/language/walker/syntaxWalker.ts @@ -234,6 +234,10 @@ export class SyntaxWalker { this.walkChildren(node); } + protected visitNumericLiteral(node: ts.NumericLiteral) { + this.walkChildren(node); + } + protected visitObjectLiteralExpression(node: ts.ObjectLiteralExpression) { this.walkChildren(node); } @@ -556,6 +560,10 @@ export class SyntaxWalker { this.visitNewExpression(node as ts.NewExpression); break; + case ts.SyntaxKind.NumericLiteral: + this.visitNumericLiteral(node as ts.NumericLiteral); + break; + case ts.SyntaxKind.ObjectBindingPattern: this.visitBindingPattern(node as ts.BindingPattern); break; From 48b0c597f9257712c7d1f04b55ed0aa60e333f6a Mon Sep 17 00:00:00 2001 From: Martin Probst Date: Fri, 20 Jan 2017 18:05:01 -0800 Subject: [PATCH 052/131] noInferrableTypes: check property declarations. (#2081) --- src/rules/noInferrableTypesRule.ts | 7 ++++++- test/rules/no-inferrable-types/default/test.ts.lint | 4 ++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/rules/noInferrableTypesRule.ts b/src/rules/noInferrableTypesRule.ts index 617724d2a72..1a5c8c6b32c 100644 --- a/src/rules/noInferrableTypesRule.ts +++ b/src/rules/noInferrableTypesRule.ts @@ -69,7 +69,12 @@ class NoInferrableTypesWalker extends Lint.RuleWalker { super.visitParameterDeclaration(node); } - private checkDeclaration(node: ts.ParameterDeclaration | ts.VariableDeclaration) { + public visitPropertyDeclaration(node: ts.PropertyDeclaration) { + this.checkDeclaration(node); + super.visitPropertyDeclaration(node); + } + + private checkDeclaration(node: ts.ParameterDeclaration | ts.VariableDeclaration | ts.PropertyDeclaration) { if (node.type != null && node.initializer != null) { let failure: string | null = null; diff --git a/test/rules/no-inferrable-types/default/test.ts.lint b/test/rules/no-inferrable-types/default/test.ts.lint index 37553cfd3ba..22dbc0281df 100644 --- a/test/rules/no-inferrable-types/default/test.ts.lint +++ b/test/rules/no-inferrable-types/default/test.ts.lint @@ -5,6 +5,10 @@ let y: boolean = false; ~~~~~~~ [boolean] let z: string = "foo"; ~~~~~~ [string] +class C { + x: number = 1; + ~~~~~~ [number] +} // errors, types are inferrable function foo (a: number = 5, b: boolean = true, c: string = "bah") { } From ef7f091b4f9bbb9b25cd5430dc2e68e23afcc68e Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Fri, 20 Jan 2017 18:35:21 -0800 Subject: [PATCH 053/131] Clean up code and add `getEqualsKind` helper (#2090) --- src/language/utils.ts | 20 ++++++ src/rules/noBooleanLiteralCompareRule.ts | 21 ++---- src/rules/noUnsafeAnyRule.ts | 9 ++- src/rules/strictTypePredicatesRule.ts | 26 +------- src/rules/tripleEqualsRule.ts | 41 ++++-------- src/rules/typeofCompareRule.ts | 84 ++++++++++-------------- test/rules/typeof-compare/test.js.lint | 30 +++++---- test/rules/typeof-compare/test.ts.lint | 30 +++++---- 8 files changed, 110 insertions(+), 151 deletions(-) diff --git a/src/language/utils.ts b/src/language/utils.ts index 93ee4091a3e..a31b93159c7 100644 --- a/src/language/utils.ts +++ b/src/language/utils.ts @@ -415,3 +415,23 @@ export function hasCommentAfterPosition(text: string, position: number): boolean return ts.getTrailingCommentRanges(text, position) !== undefined || ts.getLeadingCommentRanges(text, position) !== undefined; } + +export interface EqualsKind { + isPositive: boolean; // True for "===" and "==" + isStrict: boolean; // True for "===" and "!==" +} + +export function getEqualsKind(node: ts.BinaryOperatorToken): EqualsKind | undefined { + switch (node.kind) { + case ts.SyntaxKind.EqualsEqualsToken: + return { isPositive: true, isStrict: false }; + case ts.SyntaxKind.EqualsEqualsEqualsToken: + return { isPositive: true, isStrict: true }; + case ts.SyntaxKind.ExclamationEqualsToken: + return { isPositive: false, isStrict: false }; + case ts.SyntaxKind.ExclamationEqualsEqualsToken: + return { isPositive: false, isStrict: true }; + default: + return undefined; + } +} diff --git a/src/rules/noBooleanLiteralCompareRule.ts b/src/rules/noBooleanLiteralCompareRule.ts index 73a776c3053..6cc43df13bd 100644 --- a/src/rules/noBooleanLiteralCompareRule.ts +++ b/src/rules/noBooleanLiteralCompareRule.ts @@ -90,35 +90,22 @@ function needsParenthesesForNegate(node: ts.Expression) { function deconstructComparison(node: ts.BinaryExpression): { negate: boolean, expression: ts.Expression } | undefined { const { left, operatorToken, right } = node; - const operator = operatorKind(operatorToken); - if (operator === undefined) { + const eq = Lint.getEqualsKind(operatorToken); + if (!eq) { return undefined; } const leftValue = booleanFromExpression(left); if (leftValue !== undefined) { - return { negate: leftValue !== operator, expression: right }; + return { negate: leftValue !== eq.isPositive, expression: right }; } const rightValue = booleanFromExpression(right); if (rightValue !== undefined) { - return { negate: rightValue !== operator, expression: left }; + return { negate: rightValue !== eq.isPositive, expression: left }; } return undefined; } -function operatorKind(operatorToken: ts.BinaryOperatorToken): boolean | undefined { - switch (operatorToken.kind) { - case ts.SyntaxKind.EqualsEqualsToken: - case ts.SyntaxKind.EqualsEqualsEqualsToken: - return true; - case ts.SyntaxKind.ExclamationEqualsToken: - case ts.SyntaxKind.ExclamationEqualsEqualsToken: - return false; - default: - return undefined; - } -} - function booleanFromExpression(node: ts.Expression): boolean | undefined { switch (node.kind) { case ts.SyntaxKind.TrueKeyword: diff --git a/src/rules/noUnsafeAnyRule.ts b/src/rules/noUnsafeAnyRule.ts index 0fb0344b30d..90e7fe16f87 100644 --- a/src/rules/noUnsafeAnyRule.ts +++ b/src/rules/noUnsafeAnyRule.ts @@ -70,13 +70,12 @@ class Walker extends Lint.ProgramAwareRuleWalker { case ts.SyntaxKind.BinaryExpression: const { left, right, operatorToken } = parent as ts.BinaryExpression; + // Allow equality since all values support equality. + if (Lint.getEqualsKind(operatorToken) !== undefined) { + return true; + } switch (operatorToken.kind) { case ts.SyntaxKind.InstanceOfKeyword: // Allow test - // Allow equality since all values support equality. - case ts.SyntaxKind.EqualsEqualsToken: - case ts.SyntaxKind.EqualsEqualsEqualsToken: - case ts.SyntaxKind.ExclamationEqualsToken: - case ts.SyntaxKind.ExclamationEqualsEqualsToken: return true; case ts.SyntaxKind.PlusToken: // Allow stringification return node === left ? this.isStringLike(right) : this.isStringLike(left); diff --git a/src/rules/strictTypePredicatesRule.ts b/src/rules/strictTypePredicatesRule.ts index 8dbe8da5f68..b2372d0b6e1 100644 --- a/src/rules/strictTypePredicatesRule.ts +++ b/src/rules/strictTypePredicatesRule.ts @@ -56,14 +56,14 @@ export class Rule extends Lint.Rules.TypedRule { class Walker extends Lint.ProgramAwareRuleWalker { public visitBinaryExpression(node: ts.BinaryExpression) { - const equals = getEquals(node.operatorToken.kind); + const equals = Lint.getEqualsKind(node.operatorToken); if (equals) { this.checkEquals(node, equals); } super.visitBinaryExpression(node); } - private checkEquals(node: ts.BinaryExpression, { isStrict, isPositive }: Equals) { + private checkEquals(node: ts.BinaryExpression, { isStrict, isPositive }: Lint.EqualsKind) { const exprPred = getTypePredicate(node, isStrict); if (!exprPred) { return; @@ -132,7 +132,7 @@ function getTypePredicateOneWay(left: ts.Expression, right: ts.Expression, isStr return nullOrUndefined(ts.TypeFlags.Null); case ts.SyntaxKind.Identifier: - if ((right as ts.Identifier).text === "undefined") { + if ((right as ts.Identifier).originalKeywordKind === ts.SyntaxKind.UndefinedKeyword) { return nullOrUndefined(undefinedFlags); } @@ -250,26 +250,6 @@ function testNonStrictNullUndefined(type: ts.Type): boolean | string | undefined : false; } -interface Equals { - isPositive: boolean; // True for "===" and "==" - isStrict: boolean; // True for "===" and "!==" -} - -function getEquals(kind: ts.SyntaxKind): Equals | undefined { - switch (kind) { - case ts.SyntaxKind.EqualsEqualsToken: - return { isPositive: true, isStrict: false }; - case ts.SyntaxKind.EqualsEqualsEqualsToken: - return { isPositive: true, isStrict: true }; - case ts.SyntaxKind.ExclamationEqualsToken: - return { isPositive: false, isStrict: false }; - case ts.SyntaxKind.ExclamationEqualsEqualsToken: - return { isPositive: false, isStrict: true }; - default: - return undefined; - } -} - function unionParts(type: ts.Type) { return isUnionType(type) ? type.types : [type]; } diff --git a/src/rules/tripleEqualsRule.ts b/src/rules/tripleEqualsRule.ts index 3077b2c9556..9b69f645058 100644 --- a/src/rules/tripleEqualsRule.ts +++ b/src/rules/tripleEqualsRule.ts @@ -22,10 +22,6 @@ import * as Lint from "../index"; const OPTION_ALLOW_NULL_CHECK = "allow-null-check"; const OPTION_ALLOW_UNDEFINED_CHECK = "allow-undefined-check"; -function isUndefinedExpression(expression: ts.Expression) { - return expression.kind === ts.SyntaxKind.Identifier && expression.getText() === "undefined"; -} - export class Rule extends Lint.Rules.AbstractRule { /* tslint:disable:object-literal-sort-keys */ public static metadata: Lint.IRuleMetadata = { @@ -61,36 +57,23 @@ export class Rule extends Lint.Rules.AbstractRule { } class ComparisonWalker extends Lint.RuleWalker { - private static COMPARISON_OPERATOR_WIDTH = 2; + private allowNull = this.hasOption(OPTION_ALLOW_NULL_CHECK); + private allowUndefined = this.hasOption(OPTION_ALLOW_UNDEFINED_CHECK); public visitBinaryExpression(node: ts.BinaryExpression) { - if (!this.isExpressionAllowed(node)) { - const position = node.getChildAt(1).getStart(); - this.handleOperatorToken(position, node.operatorToken.kind); + const eq = Lint.getEqualsKind(node.operatorToken); + if (eq && !eq.isStrict && !this.isExpressionAllowed(node)) { + this.addFailureAtNode(node.operatorToken, eq.isPositive ? Rule.EQ_FAILURE_STRING : Rule.NEQ_FAILURE_STRING); } super.visitBinaryExpression(node); } - private handleOperatorToken(position: number, operator: ts.SyntaxKind) { - switch (operator) { - case ts.SyntaxKind.EqualsEqualsToken: - this.addFailureAt(position, ComparisonWalker.COMPARISON_OPERATOR_WIDTH, Rule.EQ_FAILURE_STRING); - break; - case ts.SyntaxKind.ExclamationEqualsToken: - this.addFailureAt(position, ComparisonWalker.COMPARISON_OPERATOR_WIDTH, Rule.NEQ_FAILURE_STRING); - break; - default: - break; - } - } - - private isExpressionAllowed(node: ts.BinaryExpression) { - const nullKeyword = ts.SyntaxKind.NullKeyword; - - return ( - this.hasOption(OPTION_ALLOW_NULL_CHECK) && (node.left.kind === nullKeyword || node.right.kind === nullKeyword) - ) || ( - this.hasOption(OPTION_ALLOW_UNDEFINED_CHECK) && (isUndefinedExpression(node.left) || isUndefinedExpression(node.right)) - ); + private isExpressionAllowed({ left, right }: ts.BinaryExpression) { + const isAllowed = (n: ts.Expression) => + n.kind === ts.SyntaxKind.NullKeyword ? this.allowNull + : this.allowUndefined && + n.kind === ts.SyntaxKind.Identifier && + (n as ts.Identifier).originalKeywordKind === ts.SyntaxKind.UndefinedKeyword; + return isAllowed(left) || isAllowed(right); } } diff --git a/src/rules/typeofCompareRule.ts b/src/rules/typeofCompareRule.ts index ca062f54b3f..846b74e272c 100644 --- a/src/rules/typeofCompareRule.ts +++ b/src/rules/typeofCompareRule.ts @@ -19,6 +19,8 @@ import * as ts from "typescript"; import * as Lint from "../index"; +const LEGAL_TYPEOF_RESULTS = new Set(["undefined", "string", "boolean", "number", "function", "object", "symbol"]); + export class Rule extends Lint.Rules.AbstractRule { /* tslint:disable:object-literal-sort-keys */ public static metadata: Lint.IRuleMetadata = { @@ -32,65 +34,45 @@ export class Rule extends Lint.Rules.AbstractRule { }; /* tslint:enable:object-literal-sort-keys */ - public static FAILURE_STRING = "typeof must be compared to correct value"; + public static FAILURE_STRING = + `'typeof' expression must be compared to one of: ${Array.from(LEGAL_TYPEOF_RESULTS).map((x) => `"${x}"`).join(", ")}`; public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { - const comparisonWalker = new ComparisonWalker(sourceFile, this.getOptions()); - return this.applyWithWalker(comparisonWalker); + return this.applyWithWalker(new Walker(sourceFile, this.getOptions())); } } -class ComparisonWalker extends Lint.RuleWalker { - private static LEGAL_TYPEOF_RESULTS = ["undefined", "string", "boolean", "number", "function", "object", "symbol"]; - - private static isCompareOperator(node: ts.Node): boolean { - return node.kind === ts.SyntaxKind.EqualsEqualsToken || node.kind === ts.SyntaxKind.EqualsEqualsEqualsToken; - } - - private static isLegalStringLiteral(node: ts.StringLiteral) { - return ComparisonWalker.LEGAL_TYPEOF_RESULTS.indexOf(node.text) > -1; - } - - private static isFaultyOtherSideOfTypeof(node: ts.Node): boolean { - switch (node.kind) { - case ts.SyntaxKind.StringLiteral: - if (!ComparisonWalker.isLegalStringLiteral(node as ts.StringLiteral)) { - return true; - } - break; - case ts.SyntaxKind.Identifier: - if ((node as ts.Identifier).originalKeywordKind === ts.SyntaxKind.UndefinedKeyword) { - return true; - } - break; - case ts.SyntaxKind.NullKeyword: - case ts.SyntaxKind.FirstLiteralToken: - case ts.SyntaxKind.TrueKeyword: - case ts.SyntaxKind.FalseKeyword: - case ts.SyntaxKind.ObjectLiteralExpression: - case ts.SyntaxKind.ArrayLiteralExpression: - return true; - default: break; - } - return false; - } - +class Walker extends Lint.RuleWalker { public visitBinaryExpression(node: ts.BinaryExpression) { - let isFaulty = false; - if (ComparisonWalker.isCompareOperator(node.operatorToken)) { - // typeof is at left - if (node.left.kind === ts.SyntaxKind.TypeOfExpression && ComparisonWalker.isFaultyOtherSideOfTypeof(node.right)) { - isFaulty = true; - } - // typeof is at right - if (node.right.kind === ts.SyntaxKind.TypeOfExpression && ComparisonWalker.isFaultyOtherSideOfTypeof(node.left)) { - isFaulty = true; - } - } - if (isFaulty) { - this.addFailure(this.createFailure(node.getStart(), node.getWidth(), Rule.FAILURE_STRING)); + const { operatorToken, left, right } = node; + if (Lint.getEqualsKind(operatorToken) && (isFaultyTypeof(left, right) || isFaultyTypeof(right, left))) { + this.addFailureAtNode(node, Rule.FAILURE_STRING); } super.visitBinaryExpression(node); } +} + +function isFaultyTypeof(left: ts.Expression, right: ts.Expression) { + return left.kind === ts.SyntaxKind.TypeOfExpression && isFaultyTypeofResult(right); +} +function isFaultyTypeofResult(node: ts.Node): boolean { + switch (node.kind) { + case ts.SyntaxKind.StringLiteral: + return !LEGAL_TYPEOF_RESULTS.has((node as ts.StringLiteral).text); + + case ts.SyntaxKind.Identifier: + return (node as ts.Identifier).originalKeywordKind === ts.SyntaxKind.UndefinedKeyword; + + case ts.SyntaxKind.NullKeyword: + case ts.SyntaxKind.NumericLiteral: + case ts.SyntaxKind.TrueKeyword: + case ts.SyntaxKind.FalseKeyword: + case ts.SyntaxKind.ObjectLiteralExpression: + case ts.SyntaxKind.ArrayLiteralExpression: + return true; + + default: + return false; + } } diff --git a/test/rules/typeof-compare/test.js.lint b/test/rules/typeof-compare/test.js.lint index 8977482dad3..788f0ea6c0a 100644 --- a/test/rules/typeof-compare/test.js.lint +++ b/test/rules/typeof-compare/test.js.lint @@ -2,15 +2,15 @@ var testVariable = 123; function testFunction() { if (typeof x === 'null') { - ~~~~~~~~~~~~~~~~~~~ [typeof must be compared to correct value] + ~~~~~~~~~~~~~~~~~~~ [0] console.log("called"); } const x = typeof foo === 'undefied' - ~~~~~~~~~~~~~~~~~~~~~~~~~ [typeof must be compared to correct value] + ~~~~~~~~~~~~~~~~~~~~~~~~~ [0] } console.log(typeof window == 'object ') - ~~~~~~~~~~~~~~~~~~~~~~~~~~ [typeof must be compared to correct value] + ~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] const string = "string"; typeof bar === string @@ -81,25 +81,29 @@ typeof num != "undefined"; "function" == typeof num; "symbol" == typeof num; "undefined" == typeof num; +"frizzle" == typeof num; +~~~~~~~~~~~~~~~~~~~~~~~ [0] typeof bar === 123 -~~~~~~~~~~~~~~~~~~ [typeof must be compared to correct value] +~~~~~~~~~~~~~~~~~~ [0] typeof bar === undefined -~~~~~~~~~~~~~~~~~~~~~~~~ [typeof must be compared to correct value] +~~~~~~~~~~~~~~~~~~~~~~~~ [0] typeof bar === null -~~~~~~~~~~~~~~~~~~~ [typeof must be compared to correct value] +~~~~~~~~~~~~~~~~~~~ [0] typeof bar === true -~~~~~~~~~~~~~~~~~~~ [typeof must be compared to correct value] +~~~~~~~~~~~~~~~~~~~ [0] typeof bar === false -~~~~~~~~~~~~~~~~~~~~ [typeof must be compared to correct value] +~~~~~~~~~~~~~~~~~~~~ [0] 123 === typeof bar -~~~~~~~~~~~~~~~~~~ [typeof must be compared to correct value] +~~~~~~~~~~~~~~~~~~ [0] undefined === typeof bar -~~~~~~~~~~~~~~~~~~~~~~~~ [typeof must be compared to correct value] +~~~~~~~~~~~~~~~~~~~~~~~~ [0] null === typeof bar -~~~~~~~~~~~~~~~~~~~ [typeof must be compared to correct value] +~~~~~~~~~~~~~~~~~~~ [0] true === typeof bar -~~~~~~~~~~~~~~~~~~~ [typeof must be compared to correct value] +~~~~~~~~~~~~~~~~~~~ [0] false === typeof bar -~~~~~~~~~~~~~~~~~~~~ [typeof must be compared to correct value] \ No newline at end of file +~~~~~~~~~~~~~~~~~~~~ [0] + +[0]: 'typeof' expression must be compared to one of: "undefined", "string", "boolean", "number", "function", "object", "symbol" diff --git a/test/rules/typeof-compare/test.ts.lint b/test/rules/typeof-compare/test.ts.lint index 8977482dad3..788f0ea6c0a 100644 --- a/test/rules/typeof-compare/test.ts.lint +++ b/test/rules/typeof-compare/test.ts.lint @@ -2,15 +2,15 @@ var testVariable = 123; function testFunction() { if (typeof x === 'null') { - ~~~~~~~~~~~~~~~~~~~ [typeof must be compared to correct value] + ~~~~~~~~~~~~~~~~~~~ [0] console.log("called"); } const x = typeof foo === 'undefied' - ~~~~~~~~~~~~~~~~~~~~~~~~~ [typeof must be compared to correct value] + ~~~~~~~~~~~~~~~~~~~~~~~~~ [0] } console.log(typeof window == 'object ') - ~~~~~~~~~~~~~~~~~~~~~~~~~~ [typeof must be compared to correct value] + ~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] const string = "string"; typeof bar === string @@ -81,25 +81,29 @@ typeof num != "undefined"; "function" == typeof num; "symbol" == typeof num; "undefined" == typeof num; +"frizzle" == typeof num; +~~~~~~~~~~~~~~~~~~~~~~~ [0] typeof bar === 123 -~~~~~~~~~~~~~~~~~~ [typeof must be compared to correct value] +~~~~~~~~~~~~~~~~~~ [0] typeof bar === undefined -~~~~~~~~~~~~~~~~~~~~~~~~ [typeof must be compared to correct value] +~~~~~~~~~~~~~~~~~~~~~~~~ [0] typeof bar === null -~~~~~~~~~~~~~~~~~~~ [typeof must be compared to correct value] +~~~~~~~~~~~~~~~~~~~ [0] typeof bar === true -~~~~~~~~~~~~~~~~~~~ [typeof must be compared to correct value] +~~~~~~~~~~~~~~~~~~~ [0] typeof bar === false -~~~~~~~~~~~~~~~~~~~~ [typeof must be compared to correct value] +~~~~~~~~~~~~~~~~~~~~ [0] 123 === typeof bar -~~~~~~~~~~~~~~~~~~ [typeof must be compared to correct value] +~~~~~~~~~~~~~~~~~~ [0] undefined === typeof bar -~~~~~~~~~~~~~~~~~~~~~~~~ [typeof must be compared to correct value] +~~~~~~~~~~~~~~~~~~~~~~~~ [0] null === typeof bar -~~~~~~~~~~~~~~~~~~~ [typeof must be compared to correct value] +~~~~~~~~~~~~~~~~~~~ [0] true === typeof bar -~~~~~~~~~~~~~~~~~~~ [typeof must be compared to correct value] +~~~~~~~~~~~~~~~~~~~ [0] false === typeof bar -~~~~~~~~~~~~~~~~~~~~ [typeof must be compared to correct value] \ No newline at end of file +~~~~~~~~~~~~~~~~~~~~ [0] + +[0]: 'typeof' expression must be compared to one of: "undefined", "string", "boolean", "number", "function", "object", "symbol" From eff3bc882f8af302a1abaaea0cb84804c72240cc Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Fri, 20 Jan 2017 19:18:24 -0800 Subject: [PATCH 054/131] Add `prefer-function-over-method` rule (#2037) --- docs/rules/no-unused-this/index.html | 28 ++++ src/rules/preferFunctionOverMethodRule.ts | 132 ++++++++++++++++++ .../allow-protected/test.ts.lint | 9 ++ .../allow-protected/tslint.json | 5 + .../allow-public-and-protected/test.ts.lint | 8 ++ .../allow-public-and-protected/tslint.json | 5 + .../allow-public/test.ts.lint | 9 ++ .../allow-public/tslint.json | 5 + .../default/test.js.lint | 4 + .../default/test.ts.lint | 51 +++++++ .../default/tslint.json | 8 ++ 11 files changed, 264 insertions(+) create mode 100644 docs/rules/no-unused-this/index.html create mode 100644 src/rules/preferFunctionOverMethodRule.ts create mode 100644 test/rules/prefer-function-over-method/allow-protected/test.ts.lint create mode 100644 test/rules/prefer-function-over-method/allow-protected/tslint.json create mode 100644 test/rules/prefer-function-over-method/allow-public-and-protected/test.ts.lint create mode 100644 test/rules/prefer-function-over-method/allow-public-and-protected/tslint.json create mode 100644 test/rules/prefer-function-over-method/allow-public/test.ts.lint create mode 100644 test/rules/prefer-function-over-method/allow-public/tslint.json create mode 100644 test/rules/prefer-function-over-method/default/test.js.lint create mode 100644 test/rules/prefer-function-over-method/default/test.ts.lint create mode 100644 test/rules/prefer-function-over-method/default/tslint.json diff --git a/docs/rules/no-unused-this/index.html b/docs/rules/no-unused-this/index.html new file mode 100644 index 00000000000..85b84209125 --- /dev/null +++ b/docs/rules/no-unused-this/index.html @@ -0,0 +1,28 @@ +--- +ruleName: no-unused-this +description: Warns for methods that do not use 'this'. +optionsDescription: |- + + "allow-public" excludes public methods. + "allow-protected" excludes protected methods. +options: + type: string + enum: + - allow-public + - allow-protected +optionExamples: + - 'true' + - '[true, "allow-public", "allow-protected"]' +type: style +typescriptOnly: false +layout: rule +title: 'Rule: no-unused-this' +optionsJSON: |- + { + "type": "string", + "enum": [ + "allow-public", + "allow-protected" + ] + } +--- \ No newline at end of file diff --git a/src/rules/preferFunctionOverMethodRule.ts b/src/rules/preferFunctionOverMethodRule.ts new file mode 100644 index 00000000000..a806a7b1c9f --- /dev/null +++ b/src/rules/preferFunctionOverMethodRule.ts @@ -0,0 +1,132 @@ +/** + * @license + * Copyright 2017 Palantir Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as ts from "typescript"; + +import * as Lint from "../index"; + +const OPTION_ALLOW_PUBLIC = "allow-public"; +const OPTION_ALLOW_PROTECTED = "allow-protected"; + +export class Rule extends Lint.Rules.AbstractRule { + /* tslint:disable:object-literal-sort-keys */ + public static metadata: Lint.IRuleMetadata = { + ruleName: "prefer-function-over-method", + description: "Warns for methods that do not use 'this'.", + optionsDescription: Lint.Utils.dedent` + "${OPTION_ALLOW_PUBLIC}" excludes public methods. + "${OPTION_ALLOW_PROTECTED}" excludes protected methods.`, + options: { + type: "string", + enum: [OPTION_ALLOW_PUBLIC, OPTION_ALLOW_PROTECTED], + }, + optionExamples: [ + "true", + `[true, "${OPTION_ALLOW_PUBLIC}", "${OPTION_ALLOW_PROTECTED}"]`, + ], + type: "style", + typescriptOnly: false, + }; + /* tslint:enable:object-literal-sort-keys */ + + public static FAILURE_STRING = "Method does not use 'this'. Use a function instead."; + + public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { + return this.applyWithWalker(new Walker(sourceFile, this.getOptions())); + } +} + +class Walker extends Lint.RuleWalker { + private allowPublic = this.hasOption(OPTION_ALLOW_PUBLIC); + private allowProtected = this.hasOption(OPTION_ALLOW_PROTECTED); + private stack: ThisUsed[] = []; + + public visitNode(node: ts.Node) { + switch (node.kind) { + case ts.SyntaxKind.ThisKeyword: + case ts.SyntaxKind.SuperKeyword: + this.setThisUsed(node); + break; + + case ts.SyntaxKind.MethodDeclaration: + const { name } = node as ts.MethodDeclaration; + const usesThis = this.withThisScope( + name.kind === ts.SyntaxKind.Identifier ? name.text : undefined, + () => super.visitNode(node)); + if (!usesThis && + node.parent!.kind !== ts.SyntaxKind.ObjectLiteralExpression && + this.shouldWarnForModifiers(node as ts.MethodDeclaration)) { + this.addFailureAtNode((node as ts.MethodDeclaration).name, Rule.FAILURE_STRING); + } + break; + + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.FunctionExpression: + this.withThisScope(undefined, () => super.visitNode(node)); + break; + + default: + super.visitNode(node); + } + } + + private setThisUsed(node: ts.Node) { + const cur = this.stack[this.stack.length - 1]; + if (cur && !isRecursiveCall(node, cur)) { + cur.isThisUsed = true; + } + } + + private withThisScope(name: string | undefined, recur: () => void): boolean { + this.stack.push({ name, isThisUsed: false }); + recur(); + return this.stack.pop()!.isThisUsed; + } + + private shouldWarnForModifiers(node: ts.MethodDeclaration): boolean { + if (Lint.hasModifier(node.modifiers, ts.SyntaxKind.StaticKeyword)) { + return false; + } + // TODO: Also return false if it's marked "override" (https://github.com/palantir/tslint/pull/2037) + + switch (methodVisibility(node)) { + case Visibility.Public: + return !this.allowPublic; + case Visibility.Protected: + return !this.allowProtected; + default: + return true; + } + } +} + +interface ThisUsed { readonly name: string | undefined; isThisUsed: boolean; } + +function isRecursiveCall(thisOrSuper: ts.Node, cur: ThisUsed) { + const parent = thisOrSuper.parent!; + return thisOrSuper.kind === ts.SyntaxKind.ThisKeyword && + parent.kind === ts.SyntaxKind.PropertyAccessExpression && + (parent as ts.PropertyAccessExpression).name.text === cur.name; +} + +const enum Visibility { Public, Protected, Private } + +function methodVisibility(node: ts.MethodDeclaration): Visibility { + return Lint.hasModifier(node.modifiers, ts.SyntaxKind.PrivateKeyword) ? Visibility.Private : + Lint.hasModifier(node.modifiers, ts.SyntaxKind.ProtectedKeyword) ? Visibility.Protected : + Visibility.Public; +} diff --git a/test/rules/prefer-function-over-method/allow-protected/test.ts.lint b/test/rules/prefer-function-over-method/allow-protected/test.ts.lint new file mode 100644 index 00000000000..e4df4b33c93 --- /dev/null +++ b/test/rules/prefer-function-over-method/allow-protected/test.ts.lint @@ -0,0 +1,9 @@ +class C { + a() {} + ~ [0] + protected b() {} + private c() {} + ~ [0] +} + +[0]: Method does not use 'this'. Use a function instead. diff --git a/test/rules/prefer-function-over-method/allow-protected/tslint.json b/test/rules/prefer-function-over-method/allow-protected/tslint.json new file mode 100644 index 00000000000..c5c1ff87b7e --- /dev/null +++ b/test/rules/prefer-function-over-method/allow-protected/tslint.json @@ -0,0 +1,5 @@ +{ + "rules": { + "prefer-function-over-method": [true, "allow-protected"] + } +} diff --git a/test/rules/prefer-function-over-method/allow-public-and-protected/test.ts.lint b/test/rules/prefer-function-over-method/allow-public-and-protected/test.ts.lint new file mode 100644 index 00000000000..0642288c026 --- /dev/null +++ b/test/rules/prefer-function-over-method/allow-public-and-protected/test.ts.lint @@ -0,0 +1,8 @@ +class C { + a() {} + protected b() {} + private c() {} + ~ [0] +} + +[0]: Method does not use 'this'. Use a function instead. diff --git a/test/rules/prefer-function-over-method/allow-public-and-protected/tslint.json b/test/rules/prefer-function-over-method/allow-public-and-protected/tslint.json new file mode 100644 index 00000000000..c60d1b2b86e --- /dev/null +++ b/test/rules/prefer-function-over-method/allow-public-and-protected/tslint.json @@ -0,0 +1,5 @@ +{ + "rules": { + "prefer-function-over-method": [true, "allow-public", "allow-protected"] + } +} diff --git a/test/rules/prefer-function-over-method/allow-public/test.ts.lint b/test/rules/prefer-function-over-method/allow-public/test.ts.lint new file mode 100644 index 00000000000..f7f7f363d80 --- /dev/null +++ b/test/rules/prefer-function-over-method/allow-public/test.ts.lint @@ -0,0 +1,9 @@ +class C { + a() {} + protected b() {} + ~ [0] + private c() {} + ~ [0] +} + +[0]: Method does not use 'this'. Use a function instead. diff --git a/test/rules/prefer-function-over-method/allow-public/tslint.json b/test/rules/prefer-function-over-method/allow-public/tslint.json new file mode 100644 index 00000000000..3b9071c84a8 --- /dev/null +++ b/test/rules/prefer-function-over-method/allow-public/tslint.json @@ -0,0 +1,5 @@ +{ + "rules": { + "prefer-function-over-method": [true, "allow-public"] + } +} diff --git a/test/rules/prefer-function-over-method/default/test.js.lint b/test/rules/prefer-function-over-method/default/test.js.lint new file mode 100644 index 00000000000..5fd64ecbc48 --- /dev/null +++ b/test/rules/prefer-function-over-method/default/test.js.lint @@ -0,0 +1,4 @@ +class C { + a() {} + ~ [Method does not use 'this'. Use a function instead.] +} diff --git a/test/rules/prefer-function-over-method/default/test.ts.lint b/test/rules/prefer-function-over-method/default/test.ts.lint new file mode 100644 index 00000000000..48ba5c89222 --- /dev/null +++ b/test/rules/prefer-function-over-method/default/test.ts.lint @@ -0,0 +1,51 @@ +class C { + static s() {} + + plain() {} + ~~~~~ [0] + + thisInFunction() { + ~~~~~~~~~~~~~~ [0] + function() { + this; + } + } + + thisInObjectMethod() { + ~~~~~~~~~~~~~~~~~~ [0] + return { x() { this; } } + } + + recur() { + ~~~~~ [0] + this.recur(); + } + + thisInParameter(this: string) {} + ~~~~~~~~~~~~~~~ [0] + + thisInParameterDefault(x = this) {} + + thisUsed() { + this; + } + + super() { + super; + } + + protected protected() {} + ~~~~~~~~~ [0] + + private private() {} + ~~~~~~~ [0] + + [Symbol.iterator]() {} + ~~~~~~~~~~~~~~~~~ [0] +} + +const o = { + x() {} +} + +[0]: Method does not use 'this'. Use a function instead. diff --git a/test/rules/prefer-function-over-method/default/tslint.json b/test/rules/prefer-function-over-method/default/tslint.json new file mode 100644 index 00000000000..7feb00aed16 --- /dev/null +++ b/test/rules/prefer-function-over-method/default/tslint.json @@ -0,0 +1,8 @@ +{ + "rules": { + "prefer-function-over-method": true + }, + "jsRules": { + "prefer-function-over-method": true + } +} From d349a3097f9ebdbebba378fa214eb7249360af09 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Fri, 20 Jan 2017 19:19:12 -0800 Subject: [PATCH 055/131] Add `no-unbound-method` rule (#2089) --- src/rules/noUnboundMethodRule.ts | 75 +++++++++++++++++++++++ test/rules/no-unbound-method/test.ts.lint | 28 +++++++++ test/rules/no-unbound-method/tslint.json | 8 +++ 3 files changed, 111 insertions(+) create mode 100644 src/rules/noUnboundMethodRule.ts create mode 100644 test/rules/no-unbound-method/test.ts.lint create mode 100644 test/rules/no-unbound-method/tslint.json diff --git a/src/rules/noUnboundMethodRule.ts b/src/rules/noUnboundMethodRule.ts new file mode 100644 index 00000000000..a54549a1836 --- /dev/null +++ b/src/rules/noUnboundMethodRule.ts @@ -0,0 +1,75 @@ +/** + * @license + * Copyright 2017 Palantir Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as ts from "typescript"; +import * as Lint from "../index"; + +export class Rule extends Lint.Rules.TypedRule { + /* tslint:disable:object-literal-sort-keys */ + public static metadata: Lint.IRuleMetadata = { + ruleName: "no-unbound-method", + description: "Warns when a method is used as outside of a method call.", + optionsDescription: "Not configurable.", + options: null, + optionExamples: ["true"], + type: "functionality", + typescriptOnly: true, + requiresTypeInfo: true, + }; + /* tslint:enable:object-literal-sort-keys */ + + public static FAILURE_STRING = "Avoid referencing unbound methods which may cause unintentional scoping of 'this'."; + + public applyWithProgram(srcFile: ts.SourceFile, langSvc: ts.LanguageService): Lint.RuleFailure[] { + return this.applyWithWalker(new Walker(srcFile, this.getOptions(), langSvc.getProgram())); + } +} + +class Walker extends Lint.ProgramAwareRuleWalker { + public visitPropertyAccessExpression(node: ts.PropertyAccessExpression) { + if (!isInCall(node)) { + const symbol = this.getTypeChecker().getSymbolAtLocation(node); + const declaration = symbol && symbol.valueDeclaration; + if (declaration && isMethod(declaration)) { + this.addFailureAtNode(node, Rule.FAILURE_STRING); + } + } + super.visitPropertyAccessExpression(node); + } +} + +function isMethod(node: ts.Node): boolean { + switch (node.kind) { + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.MethodSignature: + return true; + default: + return false; + } +} + +function isInCall(node: ts.Node): boolean { + const parent = node.parent!; + switch (parent.kind) { + case ts.SyntaxKind.CallExpression: + return (parent as ts.CallExpression).expression === node; + case ts.SyntaxKind.TaggedTemplateExpression: + return (parent as ts.TaggedTemplateExpression).tag === node; + default: + return false; + } +} diff --git a/test/rules/no-unbound-method/test.ts.lint b/test/rules/no-unbound-method/test.ts.lint new file mode 100644 index 00000000000..8b435583dd4 --- /dev/null +++ b/test/rules/no-unbound-method/test.ts.lint @@ -0,0 +1,28 @@ +class C { + method(x: number) {} + property: () => void; + template(strs: TemplateStringsArray, x: any) {} +} + +const c = new C(); +[0].forEach(c.method); + ~~~~~~~~ [0] +[0].forEach(x => c.method(x)); +[0].forEach(c.property); + +c.template; +~~~~~~~~~~ [0] +c.template`foo${0}`; +String.raw`${c.template}`; + ~~~~~~~~~~ [0] + +interface I { + foo(): void; + bar: () => void; +} +declare var i: I; +i.foo; +~~~~~ [0] +i.bar; + +[0]: Avoid referencing unbound methods which may cause unintentional scoping of 'this'. diff --git a/test/rules/no-unbound-method/tslint.json b/test/rules/no-unbound-method/tslint.json new file mode 100644 index 00000000000..767d91262bd --- /dev/null +++ b/test/rules/no-unbound-method/tslint.json @@ -0,0 +1,8 @@ +{ + "linterOptions": { + "typeCheck": true + }, + "rules": { + "no-unbound-method": true + } +} From 23a15653c40a4e556ab709a5777e0a58d3543ecb Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Sat, 21 Jan 2017 20:21:29 -0800 Subject: [PATCH 056/131] Added interface descriptors to Replacement (#2092) Fixes https://github.com/palantir/tslint/issues/1788. --- src/language/rule/rule.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/language/rule/rule.ts b/src/language/rule/rule.ts index f1a3035b71b..a6b2ad119cd 100644 --- a/src/language/rule/rule.ts +++ b/src/language/rule/rule.ts @@ -104,6 +104,21 @@ export interface IRule { applyWithWalker(walker: IWalker): RuleFailure[]; } +export interface IRuleFailureJson { + endPosition: IRuleFailurePositionJson; + failure: string; + fix?: Fix; + name: string; + ruleName: string; + startPosition: IRuleFailurePositionJson; +} + +export interface IRuleFailurePositionJson { + character: number; + line: number; + position: number; +} + export class Replacement { public static applyAll(content: string, replacements: Replacement[]) { // sort in reverse so that diffs are properly applied @@ -173,7 +188,7 @@ export class RuleFailurePosition { return this.lineAndCharacter; } - public toJson() { + public toJson(): IRuleFailurePositionJson { return { character: this.lineAndCharacter.character, line: this.lineAndCharacter.line, @@ -242,7 +257,7 @@ export class RuleFailure { return this.rawLines; } - public toJson(): any { + public toJson(): IRuleFailureJson { return { endPosition: this.endPosition.toJson(), failure: this.failure, From 13ab8ee410c8d9f436cfc67fdc04d1d63bc4c006 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Sat, 21 Jan 2017 21:50:53 -0800 Subject: [PATCH 057/131] Added tests for wildcard --test (#2096) --- test/executable/executableTests.ts | 15 +++++++++++++++ test/files/incorrect-rule-test-copy/test.ts.lint | 10 ++++++++++ test/files/incorrect-rule-test-copy/tslint.json | 5 +++++ 3 files changed, 30 insertions(+) create mode 100644 test/files/incorrect-rule-test-copy/test.ts.lint create mode 100644 test/files/incorrect-rule-test-copy/tslint.json diff --git a/test/executable/executableTests.ts b/test/executable/executableTests.ts index a28cffe3832..346ea297db6 100644 --- a/test/executable/executableTests.ts +++ b/test/executable/executableTests.ts @@ -159,6 +159,13 @@ describe("Executable", function(this: Mocha.ISuiteCallbackContext) { }); }); + it("exits with code 0 if `--test` flag is used with a wildcard", (done) => { + execCli(["--test", "test/rules/no-e*"], (err) => { + assert.isNull(err, "process should exit without an error"); + done(); + }); + }); + it("exits with code 1 if `--test` flag is used with incorrect rule", (done) => { execCli(["--test", "test/files/incorrect-rule-test"], (err) => { assert.isNotNull(err, "process should exit with error"); @@ -167,6 +174,14 @@ describe("Executable", function(this: Mocha.ISuiteCallbackContext) { }); }); + it("exits with code 1 if `--test` flag is used with incorrect rule in a wildcard", (done) => { + execCli(["--test", "test/files/incorrect-rule-*"], (err) => { + assert.isNotNull(err, "process should exit with error"); + assert.strictEqual(err.code, 1, "error code should be 1"); + done(); + }); + }); + it("exits with code 0 if `--test` flag is used with custom rule", (done) => { execCli(["--test", "test/files/custom-rule-rule-test"], (err) => { assert.isNull(err, "process should exit without an error"); diff --git a/test/files/incorrect-rule-test-copy/test.ts.lint b/test/files/incorrect-rule-test-copy/test.ts.lint new file mode 100644 index 00000000000..b11b3ed8e9e --- /dev/null +++ b/test/files/incorrect-rule-test-copy/test.ts.lint @@ -0,0 +1,10 @@ +var testVariable = "eval"; + +function a() { + function b() { + function c() { + eval("console.log('hi');"); + ~~~~~~~ [wrong error location and message] + } + } +} diff --git a/test/files/incorrect-rule-test-copy/tslint.json b/test/files/incorrect-rule-test-copy/tslint.json new file mode 100644 index 00000000000..1f0ba96f559 --- /dev/null +++ b/test/files/incorrect-rule-test-copy/tslint.json @@ -0,0 +1,5 @@ +{ + "rules": { + "no-eval": true + } +} From 29f8687ccdd8709c1c7f8c73a6b37ae424c414d9 Mon Sep 17 00:00:00 2001 From: Kei Son Date: Sun, 22 Jan 2017 14:56:07 +0900 Subject: [PATCH 058/131] Add support to fix `align` rule (#2097) --- src/rules/alignRule.ts | 10 +- test/rules/align/arguments/test.ts.fix | 149 +++++++++++++++++++++++ test/rules/align/parameters/test.js.lint | 1 + test/rules/align/parameters/test.ts.fix | 32 +++++ test/rules/align/parameters/test.ts.lint | 1 + test/rules/align/statements/test.ts.fix | 149 +++++++++++++++++++++++ 6 files changed, 340 insertions(+), 2 deletions(-) create mode 100644 test/rules/align/arguments/test.ts.fix create mode 100644 test/rules/align/parameters/test.ts.fix create mode 100644 test/rules/align/statements/test.ts.fix diff --git a/src/rules/alignRule.ts b/src/rules/alignRule.ts index 0f86c9830ba..4248fafaccd 100644 --- a/src/rules/alignRule.ts +++ b/src/rules/alignRule.ts @@ -106,8 +106,14 @@ class AlignWalker extends Lint.RuleWalker { for (const node of nodes.slice(1)) { const curPos = this.getLineAndCharacterOfPosition(node.getStart()); if (curPos.line !== prevPos.line && curPos.character !== alignToColumn) { - this.addFailureAtNode(node, kind + Rule.FAILURE_STRING_SUFFIX); - break; + const diff = alignToColumn - curPos.character; + let fix; + if (0 < diff) { + fix = this.createFix(this.appendText(node.getStart(), " ".repeat(diff))); + } else { + fix = this.createFix(this.deleteText(node.getStart() + diff, -diff)); + } + this.addFailureAtNode(node, kind + Rule.FAILURE_STRING_SUFFIX, fix); } prevPos = curPos; } diff --git a/test/rules/align/arguments/test.ts.fix b/test/rules/align/arguments/test.ts.fix new file mode 100644 index 00000000000..7f30959b034 --- /dev/null +++ b/test/rules/align/arguments/test.ts.fix @@ -0,0 +1,149 @@ +function invalidParametersAlignment1(a: number, +b: number) { + var i = 0; +} + +function invalidParametersAlignment2(a: number, b: number, +c: number) { + var i = 0; +} + +function invalidParametersAlignment3(a: number, + b: number, + c: number) { + var i = 0; +} + +class C1 { + invalidParametersAlignment(a: number, + b: number) + { + } +} + +class InvalidAlignmentInConstructor { + constructor(a: number, + str: string) + { + } +} + +var invalidParametersAlignment4 = function(xxx: foo, + yyy: bar) { return true; } + +function validParametersAlignment1(a: number, b: number) { + var i = 0; +} + +function validParametersAlignment2(a: number, b: number, + c: number) { + var i = 0; +} + +function validParametersAlignment3(a: number, + b: number, + c: number) { + var i = 0; +} + +function validParametersAlignment4( + a: number, + b: number, + c: number) { + var i = 0; +} + +var validParametersAlignment6 = function(xxx: foo, + yyy: bar) { return true; } + +/////// + +function invalidArgumentsAlignment1() +{ + f(10, + 'abcd', 0); +} + +function invalidArgumentsAlignment2() +{ + f(10, + 'abcd', + 0); + +} + +class Foo { + constructor(a: number, + str: string) + { + } +} + +var invalidConstructorArgsAlignment = new foo(10, + "abcd"); + +function validArgumentsAlignment1() +{ + f(101, 'xyz', 'abc'); +} + +function validArgumentsAlignment2() +{ + f(1, + 2, + 3, + 4); +} + +function validArgumentsAlignment3() +{ + f( + 1, + 2, + 3, + 4); +} + +function validArgumentsAlignment3() +{ + f(1, 2, + 3, 4); +} + +//////// + +function invalidStatementsAlignment1() +{ + var i = 0; + var j = 0; + var k = 1; +} + +function invalidStatementsAlignment1() +{ + var i = 0; + { + var j = 0; + var k = 1; + } +} + +function validStatementsAlignment1() +{ + var i = 0; + var j = 0; + var k = 1; +} + +function validStatementsAlignment2() +{ + var i = 0; + { + var j = 0; + var k = 1; + } +} + +function shouldntCrash() { + let f = new Foo; +} diff --git a/test/rules/align/parameters/test.js.lint b/test/rules/align/parameters/test.js.lint index 2822562e3b3..32b4a6bcc06 100644 --- a/test/rules/align/parameters/test.js.lint +++ b/test/rules/align/parameters/test.js.lint @@ -14,6 +14,7 @@ function invalidParametersAlignment3(a, b, ~ [parameters are not aligned] c) { + ~ [parameters are not aligned] var i = 0; } diff --git a/test/rules/align/parameters/test.ts.fix b/test/rules/align/parameters/test.ts.fix new file mode 100644 index 00000000000..476991849a0 --- /dev/null +++ b/test/rules/align/parameters/test.ts.fix @@ -0,0 +1,32 @@ +function invalidParametersAlignment1(a: number, + b: number) { + var i = 0; +} + +function invalidParametersAlignment2(a: number, b: number, + c: number) { + var i = 0; +} + +function invalidParametersAlignment3(a: number, + b: number, + c: number) { + var i = 0; +} + +class C1 { + invalidParametersAlignment(a: number, + b: number) + { + } +} + +class InvalidAlignmentInConstructor { + constructor(a: number, + str: string) + { + } +} + +var invalidParametersAlignment4 = function(xxx: foo, + yyy: bar) { return true; } diff --git a/test/rules/align/parameters/test.ts.lint b/test/rules/align/parameters/test.ts.lint index 6eeeae1e99c..a38a5152672 100644 --- a/test/rules/align/parameters/test.ts.lint +++ b/test/rules/align/parameters/test.ts.lint @@ -14,6 +14,7 @@ function invalidParametersAlignment3(a: number, b: number, ~~~~~~~~~ [parameters are not aligned] c: number) { + ~~~~~~~~~ [parameters are not aligned] var i = 0; } diff --git a/test/rules/align/statements/test.ts.fix b/test/rules/align/statements/test.ts.fix new file mode 100644 index 00000000000..5892c10395b --- /dev/null +++ b/test/rules/align/statements/test.ts.fix @@ -0,0 +1,149 @@ +function invalidParametersAlignment1(a: number, +b: number) { + var i = 0; +} + +function invalidParametersAlignment2(a: number, b: number, +c: number) { + var i = 0; +} + +function invalidParametersAlignment3(a: number, + b: number, + c: number) { + var i = 0; +} + +class C1 { + invalidParametersAlignment(a: number, + b: number) + { + } +} + +class InvalidAlignmentInConstructor { + constructor(a: number, + str: string) + { + } +} + +var invalidParametersAlignment4 = function(xxx: foo, + yyy: bar) { return true; } + +function validParametersAlignment1(a: number, b: number) { + var i = 0; +} + +function validParametersAlignment2(a: number, b: number, + c: number) { + var i = 0; +} + +function validParametersAlignment3(a: number, + b: number, + c: number) { + var i = 0; +} + +function validParametersAlignment4( + a: number, + b: number, + c: number) { + var i = 0; +} + +var validParametersAlignment6 = function(xxx: foo, + yyy: bar) { return true; } + + +/////// + +function invalidArgumentsAlignment1() +{ + f(10, + 'abcd', 0); +} + +function invalidArgumentsAlignment2() +{ + f(10, + 'abcd', + 0); +} + +class Foo { + constructor(a: number, + str: string) + { + } +} + +var invalidConstructorArgsAlignment = new foo(10, + "abcd"); + +function validArgumentsAlignment1() +{ + f(101, 'xyz', 'abc'); +} + +function validArgumentsAlignment2() +{ + f(1, + 2, + 3, + 4); +} + +function validArgumentsAlignment3() +{ + f( + 1, + 2, + 3, + 4); +} + +function validArgumentsAlignment3() +{ + f(1, 2, + 3, 4); +} + +//////// + +function invalidStatementsAlignment1() +{ + var i = 0; + var j = 0; + var k = 1; +} + +function invalidStatementsAlignment1() +{ + var i = 0; + { + var j = 0; + var k = 1; + } +} + +function validStatementsAlignment1() +{ + var i = 0; + var j = 0; + var k = 1; +} + +function validStatementsAlignment2() +{ + var i = 0; + { + var j = 0; + var k = 1; + } +} + +function shouldntCrash() { + let f = new Foo; +} From e82b81ebd8d5c3b41ff1feabce32ce9362d85aef Mon Sep 17 00:00:00 2001 From: Noah Chen Date: Sun, 22 Jan 2017 01:01:43 -0500 Subject: [PATCH 059/131] Add hasFix: true to alignRule.ts (#2098) --- src/rules/alignRule.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/rules/alignRule.ts b/src/rules/alignRule.ts index 4248fafaccd..ea050e306fb 100644 --- a/src/rules/alignRule.ts +++ b/src/rules/alignRule.ts @@ -24,6 +24,7 @@ export class Rule extends Lint.Rules.AbstractRule { public static metadata: Lint.IRuleMetadata = { ruleName: "align", description: "Enforces vertical alignment.", + hasFix: true, rationale: "Helps maintain a readable, consistent style in your codebase.", optionsDescription: Lint.Utils.dedent` Three arguments may be optionally provided: From 59c312eceab04ec21960954ea634ffb26e205c06 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Sun, 22 Jan 2017 11:03:25 -0800 Subject: [PATCH 060/131] Add `no-let-undefined` rule (#2100) --- src/rules/noLetUndefinedRule.ts | 57 ++++++++++++++++++++++++ test/rules/no-let-undefined/test.js.lint | 2 + test/rules/no-let-undefined/test.ts.fix | 10 +++++ test/rules/no-let-undefined/test.ts.lint | 13 ++++++ test/rules/no-let-undefined/tslint.json | 8 ++++ 5 files changed, 90 insertions(+) create mode 100644 src/rules/noLetUndefinedRule.ts create mode 100644 test/rules/no-let-undefined/test.js.lint create mode 100644 test/rules/no-let-undefined/test.ts.fix create mode 100644 test/rules/no-let-undefined/test.ts.lint create mode 100644 test/rules/no-let-undefined/tslint.json diff --git a/src/rules/noLetUndefinedRule.ts b/src/rules/noLetUndefinedRule.ts new file mode 100644 index 00000000000..beb2d1f3fb6 --- /dev/null +++ b/src/rules/noLetUndefinedRule.ts @@ -0,0 +1,57 @@ +/** + * @license + * Copyright 2017 Palantir Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as ts from "typescript"; + +import * as Lint from "../index"; + +export class Rule extends Lint.Rules.AbstractRule { + /* tslint:disable:object-literal-sort-keys */ + public static metadata: Lint.IRuleMetadata = { + ruleName: "no-let-undefined", + description: "Forbids a 'let' statement to be initialized to 'undefined'.", + hasFix: true, + optionsDescription: "Not configurable.", + options: null, + optionExamples: ["true"], + type: "style", + typescriptOnly: false, + }; + /* tslint:enable:object-literal-sort-keys */ + + public static FAILURE_STRING = "Unnecessary initialization to 'undefined'."; + + public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { + return this.applyWithWalker(new Walker(sourceFile, this.getOptions())); + } +} + +class Walker extends Lint.RuleWalker { + public visitVariableDeclaration(node: ts.VariableDeclaration) { + if (Lint.isNodeFlagSet(node.parent!, ts.NodeFlags.Let) && isUndefined(node.initializer)) { + const fix = this.createFix(this.deleteFromTo( + Lint.childOfKind(node, ts.SyntaxKind.EqualsToken)!.pos, + node.end)); + this.addFailureAtNode(node, Rule.FAILURE_STRING, fix); + } + super.visitVariableDeclaration(node); + } +} + +function isUndefined(node: ts.Node | undefined): boolean { + return node !== undefined && node.kind === ts.SyntaxKind.Identifier && (node as ts.Identifier).text === "undefined"; +} diff --git a/test/rules/no-let-undefined/test.js.lint b/test/rules/no-let-undefined/test.js.lint new file mode 100644 index 00000000000..4e29a2a5880 --- /dev/null +++ b/test/rules/no-let-undefined/test.js.lint @@ -0,0 +1,2 @@ +let x = undefined; + ~~~~~~~~~~~~~ [Unnecessary initialization to 'undefined'.] \ No newline at end of file diff --git a/test/rules/no-let-undefined/test.ts.fix b/test/rules/no-let-undefined/test.ts.fix new file mode 100644 index 00000000000..6a9080a8d3b --- /dev/null +++ b/test/rules/no-let-undefined/test.ts.fix @@ -0,0 +1,10 @@ +let x: string | undefined; + +for (let y: number | undefined; y < 2; y++) {} + +let z; + +const x = undefined; + +function f(x: string | undefined = undefined) {} + diff --git a/test/rules/no-let-undefined/test.ts.lint b/test/rules/no-let-undefined/test.ts.lint new file mode 100644 index 00000000000..676e899f224 --- /dev/null +++ b/test/rules/no-let-undefined/test.ts.lint @@ -0,0 +1,13 @@ +let x: string | undefined = undefined; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + +for (let y: number | undefined = undefined; y < 2; y++) {} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + +let z; + +const x = undefined; + +function f(x: string | undefined = undefined) {} + +[0]: Unnecessary initialization to 'undefined'. diff --git a/test/rules/no-let-undefined/tslint.json b/test/rules/no-let-undefined/tslint.json new file mode 100644 index 00000000000..5cbf82dce32 --- /dev/null +++ b/test/rules/no-let-undefined/tslint.json @@ -0,0 +1,8 @@ +{ + "rules": { + "no-let-undefined": [true] + }, + "jsRules": { + "no-let-undefined": [true] + } +} From 7751f997fb80f2c5efd6c49a9bfc023dbe6ecee0 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Mon, 23 Jan 2017 08:47:05 -0800 Subject: [PATCH 061/131] Add `await-promise` rule. (#2102) --- src/rules/awaitPromiseRule.ts | 74 ++++++++++++++++++++++++++ test/rules/await-promise/test.ts.lint | 26 +++++++++ test/rules/await-promise/tsconfig.json | 5 ++ test/rules/await-promise/tslint.json | 8 +++ 4 files changed, 113 insertions(+) create mode 100644 src/rules/awaitPromiseRule.ts create mode 100644 test/rules/await-promise/test.ts.lint create mode 100644 test/rules/await-promise/tsconfig.json create mode 100644 test/rules/await-promise/tslint.json diff --git a/src/rules/awaitPromiseRule.ts b/src/rules/awaitPromiseRule.ts new file mode 100644 index 00000000000..7387100a9e7 --- /dev/null +++ b/src/rules/awaitPromiseRule.ts @@ -0,0 +1,74 @@ +/** + * @license + * Copyright 2017 Palantir Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as ts from "typescript"; +import * as Lint from "../index"; + +export class Rule extends Lint.Rules.TypedRule { + /* tslint:disable:object-literal-sort-keys */ + public static metadata: Lint.IRuleMetadata = { + ruleName: "strict-boolean-expressions", + description: "Warns for an awaited value that is not a Promise.", + optionsDescription: "Not configurable.", + options: null, + optionExamples: ["true"], + type: "functionality", + typescriptOnly: true, + requiresTypeInfo: true, + }; + /* tslint:enable:object-literal-sort-keys */ + + public static FAILURE_STRING = "'await' of non-Promise."; + + public applyWithProgram(srcFile: ts.SourceFile, langSvc: ts.LanguageService): Lint.RuleFailure[] { + return this.applyWithWalker(new Walker(srcFile, this.getOptions(), langSvc.getProgram())); + } +} + +class Walker extends Lint.ProgramAwareRuleWalker { + public visitNode(node: ts.Node) { + if (node.kind === ts.SyntaxKind.AwaitExpression && + !couldBePromise(this.getTypeChecker().getTypeAtLocation((node as ts.AwaitExpression).expression))) { + this.addFailureAtNode(node, Rule.FAILURE_STRING); + } + + super.visitNode(node); + } +} + +function couldBePromise(type: ts.Type): boolean { + if (Lint.isTypeFlagSet(type, ts.TypeFlags.Any) || isPromiseType(type)) { + return true; + } + + if (isUnionType(type)) { + return type.types.some(isPromiseType); + } + + const bases = type.getBaseTypes(); + return bases !== undefined && bases.some(couldBePromise); +} + +function isPromiseType(type: ts.Type): boolean { + const { target } = type as ts.TypeReference; + const symbol = target && target.symbol; + return !!symbol && symbol.name === "Promise"; +} + +function isUnionType(type: ts.Type): type is ts.UnionType { + return Lint.isTypeFlagSet(type, ts.TypeFlags.Union); +} diff --git a/test/rules/await-promise/test.ts.lint b/test/rules/await-promise/test.ts.lint new file mode 100644 index 00000000000..dd9bfcb9d83 --- /dev/null +++ b/test/rules/await-promise/test.ts.lint @@ -0,0 +1,26 @@ +declare const num: Promise; +declare const isAny: any; + +async function f() { + await isAny; + await num; + await 0; + ~~~~~~~ [0] + await (Math.random() > 0.5 ? num : 0); + await (Math.random() > 0.5 ? "" : 0); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + + class MyPromise extends Promise {} + const myPromise: MyPromise = MyPromise.resolve(2); + await myPromise; + + class Foo extends MyPromise {} + const foo: Foo = Foo.resolve(2); + await foo; + + class Bar extends Array {} + await new Bar(); + ~~~~~~~~~~~~~~~ [0] +} + +[0]: 'await' of non-Promise. diff --git a/test/rules/await-promise/tsconfig.json b/test/rules/await-promise/tsconfig.json new file mode 100644 index 00000000000..ea71f9415cf --- /dev/null +++ b/test/rules/await-promise/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "target": "es6" + } +} \ No newline at end of file diff --git a/test/rules/await-promise/tslint.json b/test/rules/await-promise/tslint.json new file mode 100644 index 00000000000..310c3d77712 --- /dev/null +++ b/test/rules/await-promise/tslint.json @@ -0,0 +1,8 @@ +{ + "linterOptions": { + "typeCheck": true + }, + "rules": { + "await-promise": true + } +} From 1e3b02411251da32da3a5e2936548343c3dfff55 Mon Sep 17 00:00:00 2001 From: Andrii Dieiev Date: Tue, 24 Jan 2017 00:46:54 +0200 Subject: [PATCH 062/131] Make keys consistent with other options (#2110) --- src/rules/commentFormatRule.ts | 30 ++++++++++--------- .../exceptions-pattern/tslint.json | 4 +-- .../exceptions-words/tslint.json | 4 +-- 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/src/rules/commentFormatRule.ts b/src/rules/commentFormatRule.ts index 688728b03ec..b2dfa406764 100644 --- a/src/rules/commentFormatRule.ts +++ b/src/rules/commentFormatRule.ts @@ -21,8 +21,8 @@ import * as Lint from "../index"; import { escapeRegExp } from "../utils"; interface IExceptionsObject { - ignoreWords?: string[]; - ignorePattern?: string; + "ignore-words"?: string[]; + "ignore-pattern"?: string; } type ExceptionsRegExp = RegExp | null; @@ -49,8 +49,8 @@ export class Rule extends Lint.Rules.AbstractRule { One of two options can be provided in this object: - * \`"ignoreWords"\` - array of strings - words that will be ignored at the beginning of the comment. - * \`"ignorePattern"\` - string - RegExp pattern that will be ignored at the beginning of the comment. + * \`"ignore-words"\` - array of strings - words that will be ignored at the beginning of the comment. + * \`"ignore-pattern"\` - string - RegExp pattern that will be ignored at the beginning of the comment. `, options: { type: "array", @@ -67,13 +67,13 @@ export class Rule extends Lint.Rules.AbstractRule { { type: "object", properties: { - ignoreWords: { + "ignore-words": { type: "array", items: { type: "string", }, }, - ignorePattern: { + "ignore-pattern": { type: "string", }, }, @@ -87,8 +87,8 @@ export class Rule extends Lint.Rules.AbstractRule { }, optionExamples: [ '[true, "check-space", "check-uppercase"]', - '[true, "check-lowercase", {"ignoreWords": ["TODO", "HACK"]}]', - '[true, "check-lowercase", {"ignorePattern": "STD\\w{2,3}\\b"}]', + '[true, "check-lowercase", {"ignore-words": ["TODO", "HACK"]}]', + '[true, "check-lowercase", {"ignore-pattern": "STD\\w{2,3}\\b"}]', ], type: "style", typescriptOnly: false, @@ -158,16 +158,18 @@ class CommentWalker extends Lint.RuleWalker { return null; } - if (exceptionsObject.ignorePattern) { - this.failureIgnorePart = Rule.IGNORE_PATTERN_FAILURE_FACTORY(exceptionsObject.ignorePattern); + if (exceptionsObject["ignore-pattern"]) { + const ignorePattern = exceptionsObject["ignore-pattern"] as string; + this.failureIgnorePart = Rule.IGNORE_PATTERN_FAILURE_FACTORY(ignorePattern); // regex is "start of string"//"any amount of whitespace" followed by user provided ignore pattern - return new RegExp(`^//\\s*(${exceptionsObject.ignorePattern})`); + return new RegExp(`^//\\s*(${ignorePattern})`); } - if (exceptionsObject.ignoreWords) { - this.failureIgnorePart = Rule.IGNORE_WORDS_FAILURE_FACTORY(exceptionsObject.ignoreWords); + if (exceptionsObject["ignore-words"]) { + const ignoreWords = exceptionsObject["ignore-words"] as string[]; + this.failureIgnorePart = Rule.IGNORE_WORDS_FAILURE_FACTORY(ignoreWords); // Converts all exceptions values to strings, trim whitespace, escapes RegExp special characters and combines into alternation - const wordsPattern = exceptionsObject.ignoreWords + const wordsPattern = ignoreWords .map(String) .map((str) => str.trim()) .map(escapeRegExp) diff --git a/test/rules/comment-format/exceptions-pattern/tslint.json b/test/rules/comment-format/exceptions-pattern/tslint.json index 012c339dec3..2c269123c4d 100644 --- a/test/rules/comment-format/exceptions-pattern/tslint.json +++ b/test/rules/comment-format/exceptions-pattern/tslint.json @@ -1,8 +1,8 @@ { "rules": { - "comment-format": [true, "check-space", "check-lowercase", {"ignorePattern": "STD\\w{2,3}"}] + "comment-format": [true, "check-space", "check-lowercase", {"ignore-pattern": "STD\\w{2,3}"}] }, "jsRules": { - "comment-format": [true, "check-uppercase", {"ignorePattern": "std(in|out|err)\\b"}] + "comment-format": [true, "check-uppercase", {"ignore-pattern": "std(in|out|err)\\b"}] } } diff --git a/test/rules/comment-format/exceptions-words/tslint.json b/test/rules/comment-format/exceptions-words/tslint.json index 91aa3c20aac..d242b9f4d7d 100644 --- a/test/rules/comment-format/exceptions-words/tslint.json +++ b/test/rules/comment-format/exceptions-words/tslint.json @@ -1,8 +1,8 @@ { "rules": { - "comment-format": [true, "check-space", "check-lowercase", {"ignoreWords": ["TODO", "HACK"]}] + "comment-format": [true, "check-space", "check-lowercase", {"ignore-words": ["TODO", "HACK"]}] }, "jsRules": { - "comment-format": [true, "check-uppercase", {"ignoreWords": ["todo"]}] + "comment-format": [true, "check-uppercase", {"ignore-words": ["todo"]}] } } From af1ec7c0117a7e581311e0388d552e61a7747fad Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Mon, 23 Jan 2017 18:41:20 -0800 Subject: [PATCH 063/131] Fix typo in await-promise rule metadata (#2113) --- src/rules/awaitPromiseRule.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rules/awaitPromiseRule.ts b/src/rules/awaitPromiseRule.ts index 7387100a9e7..179ab17bccb 100644 --- a/src/rules/awaitPromiseRule.ts +++ b/src/rules/awaitPromiseRule.ts @@ -21,7 +21,7 @@ import * as Lint from "../index"; export class Rule extends Lint.Rules.TypedRule { /* tslint:disable:object-literal-sort-keys */ public static metadata: Lint.IRuleMetadata = { - ruleName: "strict-boolean-expressions", + ruleName: "await-promise", description: "Warns for an awaited value that is not a Promise.", optionsDescription: "Not configurable.", options: null, From f53ec359b7d95795f1da58463b73fc4987bbf554 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Tue, 24 Jan 2017 21:02:44 -0800 Subject: [PATCH 064/131] Rename `no-let-undefined` to `no-unnecessary-initializer` (#2106) * Rename `no-let-undefined` to `no-unnecessary-initializer` * And handle 'var', destructuring, and parameter initializers * fix for parameter that can't be made optional * Fail for *any* parameter with an unnecessary 'undefined' initializer * Fix typo --- src/rules/noLetUndefinedRule.ts | 57 --------- src/rules/noUnnecessaryInitializerRule.ts | 121 ++++++++++++++++++ test/rules/no-let-undefined/test.ts.fix | 10 -- test/rules/no-let-undefined/test.ts.lint | 13 -- test/rules/no-let-undefined/tslint.json | 8 -- .../test.js.lint | 0 .../no-unnecessary-initializer/test.ts.fix | 16 +++ .../no-unnecessary-initializer/test.ts.lint | 25 ++++ .../no-unnecessary-initializer/tslint.json | 8 ++ 9 files changed, 170 insertions(+), 88 deletions(-) delete mode 100644 src/rules/noLetUndefinedRule.ts create mode 100644 src/rules/noUnnecessaryInitializerRule.ts delete mode 100644 test/rules/no-let-undefined/test.ts.fix delete mode 100644 test/rules/no-let-undefined/test.ts.lint delete mode 100644 test/rules/no-let-undefined/tslint.json rename test/rules/{no-let-undefined => no-unnecessary-initializer}/test.js.lint (100%) create mode 100644 test/rules/no-unnecessary-initializer/test.ts.fix create mode 100644 test/rules/no-unnecessary-initializer/test.ts.lint create mode 100644 test/rules/no-unnecessary-initializer/tslint.json diff --git a/src/rules/noLetUndefinedRule.ts b/src/rules/noLetUndefinedRule.ts deleted file mode 100644 index beb2d1f3fb6..00000000000 --- a/src/rules/noLetUndefinedRule.ts +++ /dev/null @@ -1,57 +0,0 @@ -/** - * @license - * Copyright 2017 Palantir Technologies, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as ts from "typescript"; - -import * as Lint from "../index"; - -export class Rule extends Lint.Rules.AbstractRule { - /* tslint:disable:object-literal-sort-keys */ - public static metadata: Lint.IRuleMetadata = { - ruleName: "no-let-undefined", - description: "Forbids a 'let' statement to be initialized to 'undefined'.", - hasFix: true, - optionsDescription: "Not configurable.", - options: null, - optionExamples: ["true"], - type: "style", - typescriptOnly: false, - }; - /* tslint:enable:object-literal-sort-keys */ - - public static FAILURE_STRING = "Unnecessary initialization to 'undefined'."; - - public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { - return this.applyWithWalker(new Walker(sourceFile, this.getOptions())); - } -} - -class Walker extends Lint.RuleWalker { - public visitVariableDeclaration(node: ts.VariableDeclaration) { - if (Lint.isNodeFlagSet(node.parent!, ts.NodeFlags.Let) && isUndefined(node.initializer)) { - const fix = this.createFix(this.deleteFromTo( - Lint.childOfKind(node, ts.SyntaxKind.EqualsToken)!.pos, - node.end)); - this.addFailureAtNode(node, Rule.FAILURE_STRING, fix); - } - super.visitVariableDeclaration(node); - } -} - -function isUndefined(node: ts.Node | undefined): boolean { - return node !== undefined && node.kind === ts.SyntaxKind.Identifier && (node as ts.Identifier).text === "undefined"; -} diff --git a/src/rules/noUnnecessaryInitializerRule.ts b/src/rules/noUnnecessaryInitializerRule.ts new file mode 100644 index 00000000000..f722b3f1ed3 --- /dev/null +++ b/src/rules/noUnnecessaryInitializerRule.ts @@ -0,0 +1,121 @@ +/** + * @license + * Copyright 2017 Palantir Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as ts from "typescript"; + +import * as Lint from "../index"; + +export class Rule extends Lint.Rules.AbstractRule { + /* tslint:disable:object-literal-sort-keys */ + public static metadata: Lint.IRuleMetadata = { + ruleName: "no-unnecessary-initializer", + description: "Forbids a 'var'/'let' statement or destructuring initializer to be initialized to 'undefined'.", + hasFix: true, + optionsDescription: "Not configurable.", + options: null, + optionExamples: ["true"], + type: "style", + typescriptOnly: false, + }; + /* tslint:enable:object-literal-sort-keys */ + + public static FAILURE_STRING = "Unnecessary initialization to 'undefined'."; + public static FAILURE_STRING_PARAMETER = + "Use an optional parameter instead of initializing to 'undefined'. " + + "Also, the type declaration does not need to include '| undefined'."; + + public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { + return this.applyWithWalker(new Walker(sourceFile, this.getOptions())); + } +} + +class Walker extends Lint.RuleWalker { + public visitVariableDeclaration(node: ts.VariableDeclaration) { + if (isBindingPattern(node.name)) { + for (const elem of node.name.elements) { + if (elem.kind === ts.SyntaxKind.BindingElement) { + this.checkInitializer(elem); + } + } + } else if (!Lint.isNodeFlagSet(node.parent!, ts.NodeFlags.Const)) { + this.checkInitializer(node); + } + + super.visitVariableDeclaration(node); + } + + public visitMethodDeclaration(node: ts.MethodDeclaration) { + this.checkSignature(node); + super.visitMethodDeclaration(node); + } + public visitFunctionDeclaration(node: ts.FunctionDeclaration) { + this.checkSignature(node); + super.visitFunctionDeclaration(node); + } + public visitConstructorDeclaration(node: ts.ConstructorDeclaration) { + this.checkSignature(node); + super.visitConstructorDeclaration(node); + } + + private checkSignature({ parameters }: ts.MethodDeclaration | ts.FunctionDeclaration | ts.ConstructorDeclaration) { + parameters.forEach((parameter, i) => { + if (isUndefined(parameter.initializer)) { + if (parametersAllOptionalAfter(parameters, i)) { + // No fix since they may want to remove '| undefined' from the type. + this.addFailureAtNode(parameter, Rule.FAILURE_STRING_PARAMETER); + } else { + this.failWithFix(parameter); + } + } + }); + } + + private checkInitializer(node: ts.VariableDeclaration | ts.BindingElement) { + if (isUndefined(node.initializer)) { + this.failWithFix(node); + } + } + + private failWithFix(node: ts.VariableDeclaration | ts.BindingElement | ts.ParameterDeclaration) { + const fix = this.createFix(this.deleteFromTo( + Lint.childOfKind(node, ts.SyntaxKind.EqualsToken)!.pos, + node.end)); + this.addFailureAtNode(node, Rule.FAILURE_STRING, fix); + } +} + +function parametersAllOptionalAfter(parameters: ts.ParameterDeclaration[], idx: number): boolean { + for (let i = idx + 1; i < parameters.length; i++) { + if (parameters[i].questionToken) { + return true; + } + if (!parameters[i].initializer) { + return false; + } + } + return true; +} + +function isUndefined(node: ts.Node | undefined): boolean { + return node !== undefined && + node.kind === ts.SyntaxKind.Identifier && + (node as ts.Identifier).originalKeywordKind === ts.SyntaxKind.UndefinedKeyword; +} + +function isBindingPattern(node: ts.Node): node is ts.ArrayBindingPattern | ts.ObjectBindingPattern { + return node.kind === ts.SyntaxKind.ArrayBindingPattern || node.kind === ts.SyntaxKind.ObjectBindingPattern; +} diff --git a/test/rules/no-let-undefined/test.ts.fix b/test/rules/no-let-undefined/test.ts.fix deleted file mode 100644 index 6a9080a8d3b..00000000000 --- a/test/rules/no-let-undefined/test.ts.fix +++ /dev/null @@ -1,10 +0,0 @@ -let x: string | undefined; - -for (let y: number | undefined; y < 2; y++) {} - -let z; - -const x = undefined; - -function f(x: string | undefined = undefined) {} - diff --git a/test/rules/no-let-undefined/test.ts.lint b/test/rules/no-let-undefined/test.ts.lint deleted file mode 100644 index 676e899f224..00000000000 --- a/test/rules/no-let-undefined/test.ts.lint +++ /dev/null @@ -1,13 +0,0 @@ -let x: string | undefined = undefined; - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] - -for (let y: number | undefined = undefined; y < 2; y++) {} - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] - -let z; - -const x = undefined; - -function f(x: string | undefined = undefined) {} - -[0]: Unnecessary initialization to 'undefined'. diff --git a/test/rules/no-let-undefined/tslint.json b/test/rules/no-let-undefined/tslint.json deleted file mode 100644 index 5cbf82dce32..00000000000 --- a/test/rules/no-let-undefined/tslint.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "rules": { - "no-let-undefined": [true] - }, - "jsRules": { - "no-let-undefined": [true] - } -} diff --git a/test/rules/no-let-undefined/test.js.lint b/test/rules/no-unnecessary-initializer/test.js.lint similarity index 100% rename from test/rules/no-let-undefined/test.js.lint rename to test/rules/no-unnecessary-initializer/test.js.lint diff --git a/test/rules/no-unnecessary-initializer/test.ts.fix b/test/rules/no-unnecessary-initializer/test.ts.fix new file mode 100644 index 00000000000..a8143aba8d9 --- /dev/null +++ b/test/rules/no-unnecessary-initializer/test.ts.fix @@ -0,0 +1,16 @@ +let a: string | undefined; +var b: string | undefined; + +for (let c: number | undefined; c < 2; c++) {} + +let d; + +const e = undefined; + +function f(x: string | undefined, bar: number) {} +function f2(x: string | undefined = undefined, bar: number = 1) {} + +declare function get(): T +const { g } = get<{ g?: number }>(); +const [h] = get(); + diff --git a/test/rules/no-unnecessary-initializer/test.ts.lint b/test/rules/no-unnecessary-initializer/test.ts.lint new file mode 100644 index 00000000000..66d9dfc0ba4 --- /dev/null +++ b/test/rules/no-unnecessary-initializer/test.ts.lint @@ -0,0 +1,25 @@ +let a: string | undefined = undefined; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] +var b: string | undefined = undefined; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + +for (let c: number | undefined = undefined; c < 2; c++) {} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + +let d; + +const e = undefined; + +function f(x: string | undefined = undefined, bar: number) {} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] +function f2(x: string | undefined = undefined, bar: number = 1) {} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [1] + +declare function get(): T +const { g = undefined } = get<{ g?: number }>(); + ~~~~~~~~~~~~~ [0] +const [h = undefined] = get(); + ~~~~~~~~~~~~~ [0] + +[0]: Unnecessary initialization to 'undefined'. +[1]: Use an optional parameter instead of initializing to 'undefined'. Also, the type declaration does not need to include '| undefined'. diff --git a/test/rules/no-unnecessary-initializer/tslint.json b/test/rules/no-unnecessary-initializer/tslint.json new file mode 100644 index 00000000000..263c0cbe0c3 --- /dev/null +++ b/test/rules/no-unnecessary-initializer/tslint.json @@ -0,0 +1,8 @@ +{ + "rules": { + "no-unnecessary-initializer": [true] + }, + "jsRules": { + "no-unnecessary-initializer": [true] + } +} From aaa231918ca2a4c386b9a307b4abe1fe9d317918 Mon Sep 17 00:00:00 2001 From: Noah Chen Date: Wed, 25 Jan 2017 11:57:34 -0500 Subject: [PATCH 065/131] Revert recent `member-ordering` changes (#2118) * Revert "member-ordering: Support categories (#2041)" This reverts commit 18e760481438404ccc6dc6edef0a6b4bc8e9a9e9. * Revert "Rewrite member-ordering rule (#1957)" This reverts commit e6ca41432521a3e5df2780c50478d10f1964a96d. --- src/rules/memberOrderingRule.ts | 575 ++++++++---------- .../custom-categories/test.ts.lint | 10 - .../custom-categories/tslint.json | 18 - .../test.ts.lint | 17 +- .../tslint.json | 0 .../omit-access-modifier/test.ts.lint | 12 - .../omit-access-modifier/tslint.json | 9 - .../{ => order}/custom/test.ts.lint | 0 .../{ => order}/custom/tslint.json | 0 .../{ => order}/fields-first/test.ts.lint | 0 .../{ => order}/fields-first/tslint.json | 0 .../instance-sandwich/test.ts.lint | 0 .../{ => order}/instance-sandwich/tslint.json | 0 .../{ => order}/statics-first/test.ts.lint | 0 .../{ => order}/statics-first/tslint.json | 0 .../member-ordering/private/test.ts.lint | 10 + .../tslint.json | 0 .../public-before-private/test.ts.lint | 10 - .../static-before-instance/test.ts.lint | 10 - .../rules/member-ordering/static/test.ts.lint | 10 + .../tslint.json | 0 21 files changed, 274 insertions(+), 407 deletions(-) delete mode 100644 test/rules/member-ordering/custom-categories/test.ts.lint delete mode 100644 test/rules/member-ordering/custom-categories/tslint.json rename test/rules/member-ordering/{variables-before-functions => method}/test.ts.lint (66%) rename test/rules/member-ordering/{variables-before-functions => method}/tslint.json (100%) delete mode 100644 test/rules/member-ordering/omit-access-modifier/test.ts.lint delete mode 100644 test/rules/member-ordering/omit-access-modifier/tslint.json rename test/rules/member-ordering/{ => order}/custom/test.ts.lint (100%) rename test/rules/member-ordering/{ => order}/custom/tslint.json (100%) rename test/rules/member-ordering/{ => order}/fields-first/test.ts.lint (100%) rename test/rules/member-ordering/{ => order}/fields-first/tslint.json (100%) rename test/rules/member-ordering/{ => order}/instance-sandwich/test.ts.lint (100%) rename test/rules/member-ordering/{ => order}/instance-sandwich/tslint.json (100%) rename test/rules/member-ordering/{ => order}/statics-first/test.ts.lint (100%) rename test/rules/member-ordering/{ => order}/statics-first/tslint.json (100%) create mode 100644 test/rules/member-ordering/private/test.ts.lint rename test/rules/member-ordering/{public-before-private => private}/tslint.json (100%) delete mode 100644 test/rules/member-ordering/public-before-private/test.ts.lint delete mode 100644 test/rules/member-ordering/static-before-instance/test.ts.lint create mode 100644 test/rules/member-ordering/static/test.ts.lint rename test/rules/member-ordering/{static-before-instance => static}/tslint.json (100%) diff --git a/src/rules/memberOrderingRule.ts b/src/rules/memberOrderingRule.ts index eb26ce6e170..8d558c9b2d8 100644 --- a/src/rules/memberOrderingRule.ts +++ b/src/rules/memberOrderingRule.ts @@ -18,105 +18,16 @@ import * as ts from "typescript"; import * as Lint from "../index"; -const OPTION_ORDER = "order"; - -enum MemberKind { - publicStaticField, - publicStaticMethod, - protectedStaticField, - protectedStaticMethod, - privateStaticField, - privateStaticMethod, - publicInstanceField, - protectedInstanceField, - privateInstanceField, - publicConstructor, - protectedConstructor, - privateConstructor, - publicInstanceMethod, - protectedInstanceMethod, - privateInstanceMethod, -} +/* start old options */ +const OPTION_VARIABLES_BEFORE_FUNCTIONS = "variables-before-functions"; +const OPTION_STATIC_BEFORE_INSTANCE = "static-before-instance"; +const OPTION_PUBLIC_BEFORE_PRIVATE = "public-before-private"; +/* end old options */ -const PRESETS = new Map([ - ["variables-before-functions", [ - { - kinds: [ - "public-static-field", - "protected-static-field", - "private-static-field", - "public-instance-field", - "protected-instance-field", - "private-instance-field", - ], - name: "field", - }, - { - kinds: [ - "constructor", - "public-static-method", - "protected-static-method", - "private-static-method", - "public-instance-method", - "protected-instance-method", - "private-instance-method", - ], - name: "method", - }, - ]], - ["static-before-instance", [ - { - kinds: [ - "public-static-field", - "public-static-method", - "protected-static-field", - "protected-static-method", - "private-static-field", - "private-static-method", - ], - name: "static member", - }, - { - kinds: [ - "public-instance-field", - "protected-instance-field", - "private-instance-field", - "constructor", - "public-instance-method", - "protected-instance-method", - "private-instance-method", - ], - name: "instance member", - }, - ]], - ["public-before-private", [ - { - kinds: [ - "public-static-field", - "protected-static-field", - "public-instance-field", - "protected-instance-field", - "public-instance-method", - "protected-instance-method", - "public-static-method", - "protected-static-method", - "public-constructor", - "protected-constructor", - ], - name: "public member", - }, - { - kinds: [ - "private-static-field", - "private-instance-field", - "private-instance-method", - "private-static-method", - "private-constructor", - ], - name: "private member", - }, - ]], - ["fields-first", [ +/* start new options */ +const OPTION_ORDER = "order"; +const PRESET_ORDERS: { [preset: string]: string[] } = { + "fields-first": [ "public-static-field", "protected-static-field", "private-static-field", @@ -130,8 +41,8 @@ const PRESETS = new Map([ "public-instance-method", "protected-instance-method", "private-instance-method", - ]], - ["instance-sandwich", [ + ], + "instance-sandwich": [ "public-static-field", "protected-static-field", "private-static-field", @@ -145,8 +56,8 @@ const PRESETS = new Map([ "public-static-method", "protected-static-method", "private-static-method", - ]], - ["statics-first", [ + ], + "statics-first": [ "public-static-field", "public-static-method", "protected-static-field", @@ -160,42 +71,9 @@ const PRESETS = new Map([ "public-instance-method", "protected-instance-method", "private-instance-method", - ]], -]); -const PRESET_NAMES = Array.from(PRESETS.keys()); - -const allMemberKindNames = mapDefined(Object.keys(MemberKind), (key) => { - const mk = (MemberKind as any)[key]; - return typeof mk === "number" ? MemberKind[mk].replace(/[A-Z]/g, (cap) => "-" + cap.toLowerCase()) : undefined; -}); - -function namesMarkdown(names: string[]): string { - return names.map((name) => "* `" + name + "`").join("\n "); -} - -const optionsDescription = Lint.Utils.dedent` - One argument, which is an object, must be provided. It should contain an \`order\` property. - The \`order\` property should have a value of one of the following strings: - - ${namesMarkdown(PRESET_NAMES)} - - Alternatively, the value for \`order\` maybe be an array consisting of the following strings: - - ${namesMarkdown(allMemberKindNames)} - - You can also omit the access modifier to refer to "public-", "protected-", and "private-" all at once; for example, "static-field". - - You can also make your own categories by using an object instead of a string: - - { - "name": "static non-private", - "kinds": [ - "public-static-field", - "protected-static-field", - "public-static-method", - "protected-static-method" - ] - }`; + ], +}; +/* end new options */ export class Rule extends Lint.Rules.AbstractRule { /* tslint:disable:object-literal-sort-keys */ @@ -203,19 +81,43 @@ export class Rule extends Lint.Rules.AbstractRule { ruleName: "member-ordering", description: "Enforces member ordering.", rationale: "A consistent ordering for class members can make classes easier to read, navigate, and edit.", - optionsDescription, + optionsDescription: Lint.Utils.dedent` + One argument, which is an object, must be provided. It should contain an \`order\` property. + The \`order\` property should have a value of one of the following strings: + + * \`fields-first\` + * \`statics-first\` + * \`instance-sandwich\` + + Alternatively, the value for \`order\` maybe be an array consisting of the following strings: + + * \`public-static-field\` + * \`protected-static-field\` + * \`private-static-field\` + * \`public-instance-field\` + * \`protected-instance-field\` + * \`private-instance-field\` + * \`constructor\` + * \`public-static-method\` + * \`protected-static-method\` + * \`private-static-method\` + * \`public-instance-method\` + * \`protected-instance-method\` + * \`private-instance-method\` + + This is useful if one of the preset orders does not meet your needs.`, options: { type: "object", properties: { order: { oneOf: [{ type: "string", - enum: PRESET_NAMES, + enum: ["fields-first", "statics-first", "instance-sandwich"], }, { type: "array", items: { type: "string", - enum: allMemberKindNames, + enum: PRESET_ORDERS["statics-first"], }, maxLength: 13, }], @@ -223,35 +125,7 @@ export class Rule extends Lint.Rules.AbstractRule { }, additionalProperties: false, }, - optionExamples: [ - '[true, { "order": "fields-first" }]', - Lint.Utils.dedent` - [true, { - "order": [ - "static-field", - "instance-field", - "constructor", - "public-instance-method", - "protected-instance-method", - "private-instance-method" - ] - }]`, - Lint.Utils.dedent` - [true, { - "order": [ - { - "name": "static non-private", - "kinds": [ - "public-static-field", - "protected-static-field", - "public-static-method", - "protected-static-method" - ] - }, - "constructor" - ] - }]`, - ], + optionExamples: ['[true, { "order": "fields-first" }]'], type: "typescript", typescriptOnly: true, }; @@ -261,205 +135,262 @@ export class Rule extends Lint.Rules.AbstractRule { } } -export class MemberOrderingWalker extends Lint.RuleWalker { - private readonly order: MemberCategory[]; +/* start code supporting old options (i.e. "public-before-private") */ +interface IModifiers { + isMethod: boolean; + isPrivate: boolean; + isInstance: boolean; +} - constructor(sourceFile: ts.SourceFile, options: Lint.IOptions) { - super(sourceFile, options); - this.order = getOrder(this.getOptions()); +function getModifiers(isMethod: boolean, modifiers?: ts.ModifiersArray): IModifiers { + return { + isInstance: !Lint.hasModifier(modifiers, ts.SyntaxKind.StaticKeyword), + isMethod, + isPrivate: Lint.hasModifier(modifiers, ts.SyntaxKind.PrivateKeyword), + }; +} + +function toString(modifiers: IModifiers): string { + return [ + modifiers.isPrivate ? "private" : "public", + modifiers.isInstance ? "instance" : "static", + "member", + modifiers.isMethod ? "function" : "variable", + ].join(" "); +} +/* end old code */ + +/* start new code */ +enum AccessLevel { + PRIVATE, + PROTECTED, + PUBLIC, +} + +enum Membership { + INSTANCE, + STATIC, +} + +enum Kind { + FIELD, + METHOD, +} + +interface INodeAndModifiers { + accessLevel: AccessLevel; + isConstructor: boolean; + kind: Kind; + membership: Membership; + node: ts.Node; +} + +function getNodeAndModifiers(node: ts.Node, isMethod: boolean, isConstructor = false): INodeAndModifiers { + const { modifiers } = node; + const accessLevel = Lint.hasModifier(modifiers, ts.SyntaxKind.PrivateKeyword) ? AccessLevel.PRIVATE + : Lint.hasModifier(modifiers, ts.SyntaxKind.ProtectedKeyword) ? AccessLevel.PROTECTED + : AccessLevel.PUBLIC; + const kind = isMethod ? Kind.METHOD : Kind.FIELD; + const membership = Lint.hasModifier(modifiers, ts.SyntaxKind.StaticKeyword) ? Membership.STATIC : Membership.INSTANCE; + return { + accessLevel, + isConstructor, + kind, + membership, + node, + }; +} + +function getNodeOption({accessLevel, isConstructor, kind, membership}: INodeAndModifiers) { + if (isConstructor) { + return "constructor"; } + return [ + AccessLevel[accessLevel].toLowerCase(), + Membership[membership].toLowerCase(), + Kind[kind].toLowerCase(), + ].join("-"); +} +/* end new code */ + +export class MemberOrderingWalker extends Lint.RuleWalker { + private previousMember: IModifiers; + private memberStack: INodeAndModifiers[][] = []; + private hasOrderOption = this.getHasOrderOption(); + public visitClassDeclaration(node: ts.ClassDeclaration) { - this.visitMembers(node.members); + this.resetPreviousModifiers(); + + this.newMemberList(); super.visitClassDeclaration(node); + this.checkMemberOrder(); } public visitClassExpression(node: ts.ClassExpression) { - this.visitMembers(node.members); + this.resetPreviousModifiers(); + + this.newMemberList(); super.visitClassExpression(node); + this.checkMemberOrder(); } public visitInterfaceDeclaration(node: ts.InterfaceDeclaration) { - this.visitMembers(node.members); + this.resetPreviousModifiers(); + + this.newMemberList(); super.visitInterfaceDeclaration(node); + this.checkMemberOrder(); } - public visitTypeLiteral(node: ts.TypeLiteralNode) { - this.visitMembers(node.members); - super.visitTypeLiteral(node); + public visitMethodDeclaration(node: ts.MethodDeclaration) { + this.checkModifiersAndSetPrevious(node, getModifiers(true, node.modifiers)); + this.pushMember(getNodeAndModifiers(node, true)); + super.visitMethodDeclaration(node); } - private visitMembers(members: Member[]) { - let prevRank = -1; - for (const member of members) { - const rank = this.memberRank(member); - if (rank === -1) { - // no explicit ordering for this kind of node specified, so continue - continue; - } - - if (rank < prevRank) { - const nodeType = this.rankName(rank); - const prevNodeType = this.rankName(prevRank); - const lowerRank = this.findLowerRank(members, rank); - const locationHint = lowerRank !== -1 - ? `after ${this.rankName(lowerRank)}s` - : "at the beginning of the class/interface"; - const errorLine1 = `Declaration of ${nodeType} not allowed after declaration of ${prevNodeType}. ` + - `Instead, this should come ${locationHint}.`; - this.addFailureAtNode(member, errorLine1); - } else { - // keep track of last good node - prevRank = rank; - } - } + public visitMethodSignature(node: ts.SignatureDeclaration) { + this.checkModifiersAndSetPrevious(node, getModifiers(true, node.modifiers)); + this.pushMember(getNodeAndModifiers(node, true)); + super.visitMethodSignature(node); } - /** Finds the highest existing rank lower than `targetRank`. */ - private findLowerRank(members: Member[], targetRank: Rank): Rank | -1 { - let max: Rank | -1 = -1; - for (const member of members) { - const rank = this.memberRank(member); - if (rank !== -1 && rank < targetRank) { - max = Math.max(max, rank); - } - } - return max; + public visitConstructorDeclaration(node: ts.ConstructorDeclaration) { + this.checkModifiersAndSetPrevious(node, getModifiers(true, node.modifiers)); + this.pushMember(getNodeAndModifiers(node, true, true)); + super.visitConstructorDeclaration(node); } - private memberRank(member: Member): Rank | -1 { - const optionName = getMemberKind(member); - if (optionName === undefined) { - return -1; - } - return this.order.findIndex((category) => category.has(optionName)); + public visitPropertyDeclaration(node: ts.PropertyDeclaration) { + const { initializer } = node; + const isFunction = initializer != null + && (initializer.kind === ts.SyntaxKind.ArrowFunction || initializer.kind === ts.SyntaxKind.FunctionExpression); + this.checkModifiersAndSetPrevious(node, getModifiers(isFunction, node.modifiers)); + this.pushMember(getNodeAndModifiers(node, isFunction)); + super.visitPropertyDeclaration(node); } - private rankName(rank: Rank): string { - return this.order[rank].name; + public visitPropertySignature(node: ts.PropertyDeclaration) { + this.checkModifiersAndSetPrevious(node, getModifiers(false, node.modifiers)); + this.pushMember(getNodeAndModifiers(node, false)); + super.visitPropertySignature(node); } -} - -function memberKindForConstructor(access: Access): MemberKind { - return (MemberKind as any)[access + "Constructor"]; -} -function memberKindForMethodOrField(access: Access, membership: "Static" | "Instance", kind: "Method" | "Field"): MemberKind { - return (MemberKind as any)[access + membership + kind]; -} - -const allAccess: Access[] = ["public", "protected", "private"]; - -function memberKindFromName(name: string): MemberKind[] { - const kind = (MemberKind as any)[Lint.Utils.camelize(name)]; - return typeof kind === "number" ? [kind as MemberKind] : allAccess.map(addModifier); - - function addModifier(modifier: string) { - const modifiedKind = (MemberKind as any)[Lint.Utils.camelize(modifier + "-" + name)]; - if (typeof modifiedKind !== "number") { - throw new Error(`Bad member kind: ${name}`); - } - return modifiedKind; + public visitTypeLiteral(_node: ts.TypeLiteralNode) { + // don't call super from here -- we want to skip the property declarations in type literals } -} -function getMemberKind(member: Member): MemberKind | undefined { - const accessLevel = hasModifier(ts.SyntaxKind.PrivateKeyword) ? "private" - : hasModifier(ts.SyntaxKind.ProtectedKeyword) ? "protected" - : "public"; - - switch (member.kind) { - case ts.SyntaxKind.Constructor: - case ts.SyntaxKind.ConstructSignature: - return memberKindForConstructor(accessLevel); - - case ts.SyntaxKind.PropertyDeclaration: - case ts.SyntaxKind.PropertySignature: - return methodOrField(isFunctionLiteral((member as ts.PropertyDeclaration).initializer)); - - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.MethodSignature: - return methodOrField(true); - - default: - return undefined; + public visitObjectLiteralExpression(_node: ts.ObjectLiteralExpression) { + // again, don't call super here - object literals can have methods, + // and we don't wan't to check these } - function methodOrField(isMethod: boolean) { - const membership = hasModifier(ts.SyntaxKind.StaticKeyword) ? "Static" : "Instance"; - return memberKindForMethodOrField(accessLevel, membership, isMethod ? "Method" : "Field"); + /* start old code */ + private resetPreviousModifiers() { + this.previousMember = { + isInstance: false, + isMethod: false, + isPrivate: false, + }; } - function hasModifier(kind: ts.SyntaxKind) { - return Lint.hasModifier(member.modifiers, kind); + private checkModifiersAndSetPrevious(node: ts.Node, currentMember: IModifiers) { + if (!this.canAppearAfter(this.previousMember, currentMember)) { + this.addFailureAtNode(node, + `Declaration of ${toString(currentMember)} not allowed to appear after declaration of ${toString(this.previousMember)}`); + } + this.previousMember = currentMember; } -} -type MemberCategoryJson = { name: string, kinds: string[] } | string; -class MemberCategory { - constructor(readonly name: string, private readonly kinds: Set) {} - public has(kind: MemberKind) { return this.kinds.has(kind); } -} + private canAppearAfter(previousMember: IModifiers, currentMember: IModifiers) { + if (previousMember == null || currentMember == null) { + return true; + } -type Member = ts.TypeElement | ts.ClassElement; -type Rank = number; + if (this.hasOption(OPTION_VARIABLES_BEFORE_FUNCTIONS) && previousMember.isMethod !== currentMember.isMethod) { + return Number(previousMember.isMethod) < Number(currentMember.isMethod); + } -type Access = "public" | "protected" | "private"; + if (this.hasOption(OPTION_STATIC_BEFORE_INSTANCE) && previousMember.isInstance !== currentMember.isInstance) { + return Number(previousMember.isInstance) < Number(currentMember.isInstance); + } -function getOrder(options: any[]): MemberCategory[] { - return getOrderJson(options).map((cat) => typeof cat === "string" - ? new MemberCategory(cat.replace(/-/g, " "), new Set(memberKindFromName(cat))) - : new MemberCategory(cat.name, new Set(flatMap(cat.kinds, memberKindFromName)))); -} -function getOrderJson(allOptions: any[]): MemberCategoryJson[] { - if (allOptions == null || allOptions.length === 0 || allOptions[0] == null) { - throw new Error("Got empty options"); - } + if (this.hasOption(OPTION_PUBLIC_BEFORE_PRIVATE) && previousMember.isPrivate !== currentMember.isPrivate) { + return Number(previousMember.isPrivate) < Number(currentMember.isPrivate); + } - const firstOption = allOptions[0]; - if (typeof firstOption !== "object") { - return categoryFromOption(firstOption); + return true; } + /* end old code */ - return categoryFromOption(firstOption[OPTION_ORDER]); -} -function categoryFromOption(orderOption: {}): MemberCategoryJson[] { - if (Array.isArray(orderOption)) { - return orderOption; + /* start new code */ + private newMemberList() { + if (this.hasOrderOption) { + this.memberStack.push([]); + } } - const preset = PRESETS.get(orderOption as string); - if (!preset) { - throw new Error(`Bad order: ${JSON.stringify(orderOption)}`); + private pushMember(node: INodeAndModifiers) { + if (this.hasOrderOption) { + this.memberStack[this.memberStack.length - 1].push(node); + } } - return preset; -} -function isFunctionLiteral(node: ts.Node | undefined) { - switch (node && node.kind) { - case ts.SyntaxKind.ArrowFunction: - case ts.SyntaxKind.FunctionExpression: - return true; - default: - return false; + private checkMemberOrder() { + if (this.hasOrderOption) { + const memberList = this.memberStack.pop(); + const order = this.getOrder(); + if (memberList !== undefined && order !== null) { + const memberRank = memberList.map((n) => order.indexOf(getNodeOption(n))); + + let prevRank = -1; + memberRank.forEach((rank, i) => { + // no explicit ordering for this kind of node specified, so continue + if (rank === -1) { return; } + + // node should have come before last node, so add a failure + if (rank < prevRank) { + // generate a nice and clear error message + const node = memberList[i].node; + const nodeType = order[rank].split("-").join(" "); + const prevNodeType = order[prevRank].split("-").join(" "); + + const lowerRanks = memberRank.filter((r) => r < rank && r !== -1).sort(); + const locationHint = lowerRanks.length > 0 + ? `after ${order[lowerRanks[lowerRanks.length - 1]].split("-").join(" ")}s` + : "at the beginning of the class/interface"; + + const errorLine1 = `Declaration of ${nodeType} not allowed after declaration of ${prevNodeType}. ` + + `Instead, this should come ${locationHint}.`; + this.addFailureAtNode(node, errorLine1); + } else { + // keep track of last good node + prevRank = rank; + } + }); + } + } } -} -function mapDefined(inputs: T[], getOutput: (input: T) => U | undefined): U[] { - const out = []; - for (const input of inputs) { - const output = getOutput(input); - if (output !== undefined) { - out.push(output); + private getHasOrderOption() { + const allOptions = this.getOptions(); + if (allOptions == null || allOptions.length === 0) { + return false; } + + const firstOption = allOptions[0]; + return firstOption != null && typeof firstOption === "object" && firstOption[OPTION_ORDER] != null; } - return out; -} -function flatMap(inputs: T[], getOutputs: (input: T) => U[]): U[] { - const out = []; - for (const input of inputs) { - out.push(...getOutputs(input)); + // assumes this.hasOrderOption() === true + private getOrder(): string[] | null { + const orderOption = this.getOptions()[0][OPTION_ORDER]; + if (Array.isArray(orderOption)) { + return orderOption; + } else if (typeof orderOption === "string") { + return PRESET_ORDERS[orderOption] || PRESET_ORDERS["default"]; + } + return null; } - return out; + /* end new code */ } diff --git a/test/rules/member-ordering/custom-categories/test.ts.lint b/test/rules/member-ordering/custom-categories/test.ts.lint deleted file mode 100644 index 6df80c3273e..00000000000 --- a/test/rules/member-ordering/custom-categories/test.ts.lint +++ /dev/null @@ -1,10 +0,0 @@ -class C { - static foo() {} - protected static bar = 0; - static baz(); - - constructor(); - - static bang(); - ~~~~~~~~~~~~~~ [Declaration of static non-private not allowed after declaration of constructor. Instead, this should come at the beginning of the class/interface.] -} diff --git a/test/rules/member-ordering/custom-categories/tslint.json b/test/rules/member-ordering/custom-categories/tslint.json deleted file mode 100644 index ddacc962238..00000000000 --- a/test/rules/member-ordering/custom-categories/tslint.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "rules": { - "member-ordering": [true, { - "order": [ - { - "name": "static non-private", - "kinds": [ - "public-static-field", - "protected-static-field", - "public-static-method", - "protected-static-method" - ] - }, - "constructor" - ] - }] - } -} diff --git a/test/rules/member-ordering/variables-before-functions/test.ts.lint b/test/rules/member-ordering/method/test.ts.lint similarity index 66% rename from test/rules/member-ordering/variables-before-functions/test.ts.lint rename to test/rules/member-ordering/method/test.ts.lint index e657b567d72..1e552a3cbef 100644 --- a/test/rules/member-ordering/variables-before-functions/test.ts.lint +++ b/test/rules/member-ordering/method/test.ts.lint @@ -52,20 +52,5 @@ class Constructor2 { ~~~~~~~~~~~~~~~~~ [0] } -// Works for type literal, just like interface -type T = { - x(): void; - y: number; - ~~~~~~~~~~ [0] -} - -// Works for class inside object literal -const o = { - foo: class C { - x(): void; - y: number; - ~~~~~~~~~~ [0] - } -} +[0]: Declaration of public instance member variable not allowed to appear after declaration of public instance member function -[0]: Declaration of field not allowed after declaration of method. Instead, this should come at the beginning of the class/interface. diff --git a/test/rules/member-ordering/variables-before-functions/tslint.json b/test/rules/member-ordering/method/tslint.json similarity index 100% rename from test/rules/member-ordering/variables-before-functions/tslint.json rename to test/rules/member-ordering/method/tslint.json diff --git a/test/rules/member-ordering/omit-access-modifier/test.ts.lint b/test/rules/member-ordering/omit-access-modifier/test.ts.lint deleted file mode 100644 index fdfbf683047..00000000000 --- a/test/rules/member-ordering/omit-access-modifier/test.ts.lint +++ /dev/null @@ -1,12 +0,0 @@ -class C { - private static x = 0; - static y = 1; - - x = 0; - private y = 1; - - constructor() {} - - static z = 2; - ~~~~~~~~~~~~~ [Declaration of static field not allowed after declaration of constructor. Instead, this should come at the beginning of the class/interface.] -} diff --git a/test/rules/member-ordering/omit-access-modifier/tslint.json b/test/rules/member-ordering/omit-access-modifier/tslint.json deleted file mode 100644 index c88314eb612..00000000000 --- a/test/rules/member-ordering/omit-access-modifier/tslint.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "rules": { - "member-ordering": [true, { "order": [ - "static-field", - "instance-field", - "constructor" - ]}] - } -} diff --git a/test/rules/member-ordering/custom/test.ts.lint b/test/rules/member-ordering/order/custom/test.ts.lint similarity index 100% rename from test/rules/member-ordering/custom/test.ts.lint rename to test/rules/member-ordering/order/custom/test.ts.lint diff --git a/test/rules/member-ordering/custom/tslint.json b/test/rules/member-ordering/order/custom/tslint.json similarity index 100% rename from test/rules/member-ordering/custom/tslint.json rename to test/rules/member-ordering/order/custom/tslint.json diff --git a/test/rules/member-ordering/fields-first/test.ts.lint b/test/rules/member-ordering/order/fields-first/test.ts.lint similarity index 100% rename from test/rules/member-ordering/fields-first/test.ts.lint rename to test/rules/member-ordering/order/fields-first/test.ts.lint diff --git a/test/rules/member-ordering/fields-first/tslint.json b/test/rules/member-ordering/order/fields-first/tslint.json similarity index 100% rename from test/rules/member-ordering/fields-first/tslint.json rename to test/rules/member-ordering/order/fields-first/tslint.json diff --git a/test/rules/member-ordering/instance-sandwich/test.ts.lint b/test/rules/member-ordering/order/instance-sandwich/test.ts.lint similarity index 100% rename from test/rules/member-ordering/instance-sandwich/test.ts.lint rename to test/rules/member-ordering/order/instance-sandwich/test.ts.lint diff --git a/test/rules/member-ordering/instance-sandwich/tslint.json b/test/rules/member-ordering/order/instance-sandwich/tslint.json similarity index 100% rename from test/rules/member-ordering/instance-sandwich/tslint.json rename to test/rules/member-ordering/order/instance-sandwich/tslint.json diff --git a/test/rules/member-ordering/statics-first/test.ts.lint b/test/rules/member-ordering/order/statics-first/test.ts.lint similarity index 100% rename from test/rules/member-ordering/statics-first/test.ts.lint rename to test/rules/member-ordering/order/statics-first/test.ts.lint diff --git a/test/rules/member-ordering/statics-first/tslint.json b/test/rules/member-ordering/order/statics-first/tslint.json similarity index 100% rename from test/rules/member-ordering/statics-first/tslint.json rename to test/rules/member-ordering/order/statics-first/tslint.json diff --git a/test/rules/member-ordering/private/test.ts.lint b/test/rules/member-ordering/private/test.ts.lint new file mode 100644 index 00000000000..24f637796f7 --- /dev/null +++ b/test/rules/member-ordering/private/test.ts.lint @@ -0,0 +1,10 @@ +class Foo { + private x: number; + private bar(): any { + var bla: { a: string } = {a: '1'}; + } + y: number; + ~~~~~~~~~~ [0] +} + +[0]: Declaration of public instance member variable not allowed to appear after declaration of private instance member function diff --git a/test/rules/member-ordering/public-before-private/tslint.json b/test/rules/member-ordering/private/tslint.json similarity index 100% rename from test/rules/member-ordering/public-before-private/tslint.json rename to test/rules/member-ordering/private/tslint.json diff --git a/test/rules/member-ordering/public-before-private/test.ts.lint b/test/rules/member-ordering/public-before-private/test.ts.lint deleted file mode 100644 index c26be55d8ac..00000000000 --- a/test/rules/member-ordering/public-before-private/test.ts.lint +++ /dev/null @@ -1,10 +0,0 @@ -class Foo { - private x: number; - private bar(): any { - var bla: { a: string } = {a: '1'}; - } - y: number; - ~~~~~~~~~~ [0] -} - -[0]: Declaration of public member not allowed after declaration of private member. Instead, this should come at the beginning of the class/interface. diff --git a/test/rules/member-ordering/static-before-instance/test.ts.lint b/test/rules/member-ordering/static-before-instance/test.ts.lint deleted file mode 100644 index 532440d9837..00000000000 --- a/test/rules/member-ordering/static-before-instance/test.ts.lint +++ /dev/null @@ -1,10 +0,0 @@ -class Foo { - x: number; - static y: number; - ~~~~~~~~~~~~~~~~~ [0] - constructor() { - // nothing to do - } -} - -[0]: Declaration of static member not allowed after declaration of instance member. Instead, this should come at the beginning of the class/interface. diff --git a/test/rules/member-ordering/static/test.ts.lint b/test/rules/member-ordering/static/test.ts.lint new file mode 100644 index 00000000000..b5396b81a18 --- /dev/null +++ b/test/rules/member-ordering/static/test.ts.lint @@ -0,0 +1,10 @@ +class Foo { + x: number; + static y: number; + ~~~~~~~~~~~~~~~~~ [0] + constructor() { + // nothing to do + } +} + +[0]: Declaration of public static member variable not allowed to appear after declaration of public instance member variable diff --git a/test/rules/member-ordering/static-before-instance/tslint.json b/test/rules/member-ordering/static/tslint.json similarity index 100% rename from test/rules/member-ordering/static-before-instance/tslint.json rename to test/rules/member-ordering/static/tslint.json From c52c8281e24b6487526ec1a64cde3e301f1a3039 Mon Sep 17 00:00:00 2001 From: Noah Chen Date: Wed, 25 Jan 2017 15:34:38 -0500 Subject: [PATCH 066/131] Prepare for v4.4.0 (#2109) --- CHANGELOG.md | 48 +++++++++++++++++++++++++++++++++++++++++++ package.json | 2 +- src/configs/latest.ts | 5 +++++ src/linter.ts | 2 +- 4 files changed, 55 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 461b11e5964..6ce09e3f0fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,54 @@ Change Log === +v4.4.0 +--- + +* [new-rule] `arrow-return-shorthand` (#1972) +* [new-rule] `no-unbound-method` (#2089) +* [new-rule] `no-boolean-literal-compare` (#2013) +* [new-rule] `no-unsafe-any` (#2047) +* [new-rule] `no-unnecessary-qualifier` (#2008) +* [new-rule] `no-unnecessary-initializer` (#2106) +* [new-rule] `await-promise` (#2102) +* [new-rule] `no-floating-promises` (#1632) +* [new-rule] `strict-type-predicates` (#2046) +* [new-rule] `no-misused-new` (#1963) +* [new-rule] `prefer-method-signature` (#2028) +* [new-rule] `prefer-function-over-method` (#2037) +* [new-rule-option] `allow-fast-null-checks` added to `no-unused-expression` (#1638) +* [new-rule-option] `comment-format-rule` adds `ignore-words` and `ignore-pattern` options (#1757) +* [new-rule-option] `whitespace` adds `check-preblock` option (#2002) +* [new-rule-option] `strict-boolean-expressions` adds `allow-null-union`, `allow-undefined-union`, `allow-string`, and `allow-number` and (#2033) +* [new-fixer] `align` (#2097) +* [new-fixer] `no-trailing-whitespace` (#2060) +* [bugfix] `no-magic-numbers` false positive on default parameter values (#2004) +* [bugfix] `no-empty-interface` allow empty interface with 2 or more parents (#2070) +* [bugfix] `no-trailing-whitespace` fixed for comments and EOF (#2060) +* [bugfix] `no-empty` no longer fails for private or protected constructor (#1976) +* [bugfix] `tslint:disable`/`tslint-enable` now handles multiple rules and fixes what code is enabled/disabled (#2061) +* [bugfix] `no-inferrable-types` now validates property declarations (#2081) +* [bugfix] `unified-signatures` false positive (#2016) +* [bugfix] `whitespace` finds all whitespace errors in JsxExpressions and TemplateExpressions (#2036) +* [bugfix] `comment-format` no more false positives in JsxText (#2036) +* [enhancement] `--test` option now accepts glob (#2079) + +Thanks to our contributors! + +* Alexander Rusakov +* Andrii Dieiev +* @andy-ms +* Andy Hanson +* Josh Goldberg +* Kei Son +* Klaus Meinhardt +* Krati Ahuja +* Martin Probst +* Mohsen Azimi +* Noah Chen +* Romke van der Meulen +* cameron-mcateer + v4.3.1 --- diff --git a/package.json b/package.json index c02636f7e09..7d01bd664ac 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tslint", - "version": "4.3.1", + "version": "4.4.0", "description": "An extensible static analysis linter for the TypeScript language", "bin": { "tslint": "./bin/tslint" diff --git a/src/configs/latest.ts b/src/configs/latest.ts index 94149b8a8cd..2bc6fca18e1 100644 --- a/src/configs/latest.ts +++ b/src/configs/latest.ts @@ -47,6 +47,11 @@ export const rules = { }], "typeof-compare": true, "unified-signatures": true, + + // added in v4.4 + "arrow-return-shorthand": true, + "no-unnecessary-initializer": true, + "no-misused-new ": true, }; // tslint:enable object-literal-sort-keys diff --git a/src/linter.ts b/src/linter.ts index e1a7c007176..994e5305a08 100644 --- a/src/linter.ts +++ b/src/linter.ts @@ -43,7 +43,7 @@ import { arrayify, dedent } from "./utils"; * Linter that can lint multiple files in consecutive runs. */ class Linter { - public static VERSION = "4.3.1"; + public static VERSION = "4.4.0"; public static findConfiguration = findConfiguration; public static findConfigurationPath = findConfigurationPath; From 3d775a2642b0dbd30b552f13c44a24e98ed32c7e Mon Sep 17 00:00:00 2001 From: Noah Chen Date: Wed, 25 Jan 2017 16:24:34 -0500 Subject: [PATCH 067/131] Prepare for v4.4.1 (#2120) * Prepare for v4.4.1 * update yarn --- CHANGELOG.md | 6 ++++- package.json | 2 +- src/configs/latest.ts | 2 +- src/linter.ts | 2 +- src/rules/adjacentOverloadSignaturesRule.ts | 2 +- src/rules/commentFormatRule.ts | 2 +- src/rules/memberAccessRule.ts | 2 +- src/rules/noUnusedVariableRule.ts | 2 +- src/rules/oneLineRule.ts | 2 +- src/rules/preferForOfRule.ts | 6 ++--- src/rules/whitespaceRule.ts | 2 +- src/test.ts | 2 +- yarn.lock | 28 ++++++--------------- 13 files changed, 25 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ce09e3f0fd..3ad5e459a6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ Change Log === +v4.4.1 +--- + +* [bugfix] errant space in recommended ruleset (couldn't find `no-misused-new`) + v4.4.0 --- @@ -45,7 +50,6 @@ Thanks to our contributors! * Krati Ahuja * Martin Probst * Mohsen Azimi -* Noah Chen * Romke van der Meulen * cameron-mcateer diff --git a/package.json b/package.json index 7d01bd664ac..bc8dd853844 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tslint", - "version": "4.4.0", + "version": "4.4.1", "description": "An extensible static analysis linter for the TypeScript language", "bin": { "tslint": "./bin/tslint" diff --git a/src/configs/latest.ts b/src/configs/latest.ts index 2bc6fca18e1..0a10c824dec 100644 --- a/src/configs/latest.ts +++ b/src/configs/latest.ts @@ -51,7 +51,7 @@ export const rules = { // added in v4.4 "arrow-return-shorthand": true, "no-unnecessary-initializer": true, - "no-misused-new ": true, + "no-misused-new": true, }; // tslint:enable object-literal-sort-keys diff --git a/src/linter.ts b/src/linter.ts index 994e5305a08..b66d02e2a0f 100644 --- a/src/linter.ts +++ b/src/linter.ts @@ -43,7 +43,7 @@ import { arrayify, dedent } from "./utils"; * Linter that can lint multiple files in consecutive runs. */ class Linter { - public static VERSION = "4.4.0"; + public static VERSION = "4.4.1"; public static findConfiguration = findConfiguration; public static findConfigurationPath = findConfigurationPath; diff --git a/src/rules/adjacentOverloadSignaturesRule.ts b/src/rules/adjacentOverloadSignaturesRule.ts index eb896aefee6..c44db5ee8ba 100644 --- a/src/rules/adjacentOverloadSignaturesRule.ts +++ b/src/rules/adjacentOverloadSignaturesRule.ts @@ -89,7 +89,7 @@ class AdjacentOverloadSignaturesWalker extends Lint.RuleWalker { /** 'getOverloadName' may return undefined for nodes that cannot be overloads, e.g. a `const` declaration. */ private checkOverloadsAdjacent(overloads: T[], getOverload: (node: T) => Overload | undefined) { - let lastKey: string | undefined = undefined; + let lastKey: string | undefined; const seen = new Set(); for (const node of overloads) { const overload = getOverload(node); diff --git a/src/rules/commentFormatRule.ts b/src/rules/commentFormatRule.ts index b2dfa406764..995ec565203 100644 --- a/src/rules/commentFormatRule.ts +++ b/src/rules/commentFormatRule.ts @@ -168,7 +168,7 @@ class CommentWalker extends Lint.RuleWalker { if (exceptionsObject["ignore-words"]) { const ignoreWords = exceptionsObject["ignore-words"] as string[]; this.failureIgnorePart = Rule.IGNORE_WORDS_FAILURE_FACTORY(ignoreWords); - // Converts all exceptions values to strings, trim whitespace, escapes RegExp special characters and combines into alternation + // Converts all exceptions values to strings, trim whitespace, escapes RegExp special characters and combines into alternation const wordsPattern = ignoreWords .map(String) .map((str) => str.trim()) diff --git a/src/rules/memberAccessRule.ts b/src/rules/memberAccessRule.ts index 391a92e8e2a..84a6f33a1d4 100644 --- a/src/rules/memberAccessRule.ts +++ b/src/rules/memberAccessRule.ts @@ -129,7 +129,7 @@ export class MemberAccessWalker extends Lint.RuleWalker { throw new Error("unhandled node type"); } - let memberName: string|undefined = undefined; + let memberName: string|undefined; // look for the identifier and get its text if (node.name !== undefined && node.name.kind === ts.SyntaxKind.Identifier) { memberName = (node.name as ts.Identifier).text; diff --git a/src/rules/noUnusedVariableRule.ts b/src/rules/noUnusedVariableRule.ts index dfa3c74c260..437cfd1e53a 100644 --- a/src/rules/noUnusedVariableRule.ts +++ b/src/rules/noUnusedVariableRule.ts @@ -452,7 +452,7 @@ class NoUnusedVariablesWalker extends Lint.RuleWalker { } private fail(type: string, name: string, position: number, replacements?: Lint.Replacement[]) { - let fix: Lint.Fix | undefined = undefined; + let fix: Lint.Fix | undefined; if (replacements && replacements.length) { fix = new Lint.Fix(Rule.metadata.ruleName, replacements); } diff --git a/src/rules/oneLineRule.ts b/src/rules/oneLineRule.ts index 89ae394d58d..89304f96cc0 100644 --- a/src/rules/oneLineRule.ts +++ b/src/rules/oneLineRule.ts @@ -291,7 +291,7 @@ class OneLineWalker extends Lint.RuleWalker { const previousNodeLine = this.getLineAndCharacterOfPosition(previousNode.getEnd()).line; const openBraceLine = this.getLineAndCharacterOfPosition(openBraceToken.getStart()).line; - let failure: string | undefined = undefined; + let failure: string | undefined; if (this.hasOption(OPTION_BRACE) && previousNodeLine !== openBraceLine) { failure = Rule.BRACE_FAILURE_STRING; diff --git a/src/rules/preferForOfRule.ts b/src/rules/preferForOfRule.ts index d4e546df2c9..9749e333d64 100644 --- a/src/rules/preferForOfRule.ts +++ b/src/rules/preferForOfRule.ts @@ -59,7 +59,7 @@ class PreferForOfWalker extends Lint.BlockScopeAwareRuleWalker true, diff --git a/yarn.lock b/yarn.lock index 1b29d28be62..fee9777b8f0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -57,15 +57,9 @@ dependencies: "@types/node" "*" -"@types/underscore.string@0.0.30": - version "0.0.30" - resolved "https://registry.yarnpkg.com/@types/underscore.string/-/underscore.string-0.0.30.tgz#b027548c7397bd0de290be70c9f60eb34efe391a" - dependencies: - "@types/underscore" "*" - -"@types/underscore@*", "@types/underscore@^1.7.36": - version "1.7.36" - resolved "https://registry.yarnpkg.com/@types/underscore/-/underscore-1.7.36.tgz#e2900fd4ff7a71b742d4a7ab98ffe17b04b068c0" +"@types/update-notifier@^1.0.0": + version "1.0.0" + resolved "https://https://registry.yarnpkg.com/@types/update-notifier/-/update-notifier-1.0.0.tgz#3ae6206a6d67c01ffddb9a1eac4cd9b518d534ee" ansi-align@^1.1.0: version "1.1.0" @@ -919,7 +913,7 @@ split@0.3: dependencies: through "2" -sprintf-js@^1.0.3, sprintf-js@~1.0.2: +sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" @@ -987,8 +981,8 @@ timed-out@^3.0.0: version "0.0.1" tslint@latest: - version "4.2.0" - resolved "https://registry.yarnpkg.com/tslint/-/tslint-4.2.0.tgz#b9f5c5b871b784ab2f4809e704ade42d62f523ad" + version "4.4.0" + resolved "https://https://registry.yarnpkg.com/tslint/-/tslint-4.4.0.tgz#748d17bf8598dd044cca5771a03353ea422da29e" dependencies: babel-code-frame "^6.20.0" colors "^1.1.2" @@ -997,7 +991,6 @@ tslint@latest: glob "^7.1.1" optimist "~0.6.0" resolve "^1.1.7" - underscore.string "^3.3.4" update-notifier "^1.0.2" type-detect@0.1.1: @@ -1012,13 +1005,6 @@ typescript@2.1.4: version "2.1.4" resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.1.4.tgz#b53b69fb841126acb1dd4b397d21daba87572251" -underscore.string@^3.3.4: - version "3.3.4" - resolved "https://registry.yarnpkg.com/underscore.string/-/underscore.string-3.3.4.tgz#2c2a3f9f83e64762fdc45e6ceac65142864213db" - dependencies: - sprintf-js "^1.0.3" - util-deprecate "^1.0.2" - unzip-response@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-1.0.2.tgz#b984f0877fc0a89c2c773cc1ef7b5b232b5b06fe" @@ -1042,7 +1028,7 @@ url-parse-lax@^1.0.0: dependencies: prepend-http "^1.0.1" -util-deprecate@^1.0.2, util-deprecate@~1.0.1: +util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" From f82ac799662a691572bc532c3f6e1b255717e5e8 Mon Sep 17 00:00:00 2001 From: Noah Chen Date: Wed, 25 Jan 2017 19:42:30 -0500 Subject: [PATCH 068/131] Fix linting errors (#2121) - fix minor lint failure - build was also failing because it was linting a test file, "b.ts", which should have been named "b.test.ts" to avoid being linted --- test/formatters/proseFormatterTests.ts | 2 +- test/rules/no-unnecessary-qualifier/{b.ts => b.test.ts} | 0 test/rules/no-unnecessary-qualifier/test.ts.fix | 4 ++-- test/rules/no-unnecessary-qualifier/test.ts.lint | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) rename test/rules/no-unnecessary-qualifier/{b.ts => b.test.ts} (100%) diff --git a/test/formatters/proseFormatterTests.ts b/test/formatters/proseFormatterTests.ts index 0643e58bfdf..b83e550a6f1 100644 --- a/test/formatters/proseFormatterTests.ts +++ b/test/formatters/proseFormatterTests.ts @@ -52,7 +52,7 @@ describe("Prose Formatter", () => { new RuleFailure(sourceFile, 0, 1, "first failure", "first-name"), ]; - const mockFix = { getFileName: () => { return "file2"; } } as any; + const mockFix = { getFileName: () => "file2" } as any; const fixes = [ new RuleFailure(sourceFile, 0, 1, "first failure", "first-name"), diff --git a/test/rules/no-unnecessary-qualifier/b.ts b/test/rules/no-unnecessary-qualifier/b.test.ts similarity index 100% rename from test/rules/no-unnecessary-qualifier/b.ts rename to test/rules/no-unnecessary-qualifier/b.test.ts diff --git a/test/rules/no-unnecessary-qualifier/test.ts.fix b/test/rules/no-unnecessary-qualifier/test.ts.fix index cd80a83a502..0775562d407 100644 --- a/test/rules/no-unnecessary-qualifier/test.ts.fix +++ b/test/rules/no-unnecessary-qualifier/test.ts.fix @@ -20,8 +20,8 @@ namespace A.B.C2 { const copy = C1.D; } -import * as B from "./b"; -declare module "./b" { +import * as B from "./b.test"; +declare module "./b.test" { const x: T; } diff --git a/test/rules/no-unnecessary-qualifier/test.ts.lint b/test/rules/no-unnecessary-qualifier/test.ts.lint index 6e7b2b4cca0..ee09132ffd4 100644 --- a/test/rules/no-unnecessary-qualifier/test.ts.lint +++ b/test/rules/no-unnecessary-qualifier/test.ts.lint @@ -28,8 +28,8 @@ namespace A.B.C2 { ~~~ [Qualifier is unnecessary since 'A.B' is in scope.] } -import * as B from "./b"; -declare module "./b" { +import * as B from "./b.test"; +declare module "./b.test" { const x: B.T; ~ [Qualifier is unnecessary since 'B' is in scope.] } From c5f52a11a487c22177bef566701b4adbb9ec36f5 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Wed, 25 Jan 2017 19:41:45 -0800 Subject: [PATCH 069/131] Support multiple old-style options. (#2123) * Revert "Revert recent `member-ordering` changes (#2118)" This reverts commit aaa231918ca2a4c386b9a307b4abe1fe9d317918. * Support multiple old-style options. * Do not use presets; use an algorithm to generate a preset from multiple options. --- src/rules/memberOrderingRule.ts | 540 ++++++++++-------- .../custom-categories/test.ts.lint | 10 + .../custom-categories/tslint.json | 18 + .../{order => }/custom/test.ts.lint | 0 .../{order => }/custom/tslint.json | 0 .../{order => }/fields-first/test.ts.lint | 0 .../{order => }/fields-first/tslint.json | 0 .../instance-sandwich/test.ts.lint | 0 .../{order => }/instance-sandwich/tslint.json | 0 .../mix-old-options/test.ts.lint | 16 + .../mix-old-options/tslint.json | 9 + .../omit-access-modifier/test.ts.lint | 12 + .../omit-access-modifier/tslint.json | 9 + .../member-ordering/private/test.ts.lint | 10 - .../public-before-private/test.ts.lint | 10 + .../tslint.json | 0 .../static-before-instance/test.ts.lint | 10 + .../tslint.json | 0 .../rules/member-ordering/static/test.ts.lint | 10 - .../{order => }/statics-first/test.ts.lint | 0 .../{order => }/statics-first/tslint.json | 0 .../test.ts.lint | 17 +- .../tslint.json | 0 23 files changed, 400 insertions(+), 271 deletions(-) create mode 100644 test/rules/member-ordering/custom-categories/test.ts.lint create mode 100644 test/rules/member-ordering/custom-categories/tslint.json rename test/rules/member-ordering/{order => }/custom/test.ts.lint (100%) rename test/rules/member-ordering/{order => }/custom/tslint.json (100%) rename test/rules/member-ordering/{order => }/fields-first/test.ts.lint (100%) rename test/rules/member-ordering/{order => }/fields-first/tslint.json (100%) rename test/rules/member-ordering/{order => }/instance-sandwich/test.ts.lint (100%) rename test/rules/member-ordering/{order => }/instance-sandwich/tslint.json (100%) create mode 100644 test/rules/member-ordering/mix-old-options/test.ts.lint create mode 100644 test/rules/member-ordering/mix-old-options/tslint.json create mode 100644 test/rules/member-ordering/omit-access-modifier/test.ts.lint create mode 100644 test/rules/member-ordering/omit-access-modifier/tslint.json delete mode 100644 test/rules/member-ordering/private/test.ts.lint create mode 100644 test/rules/member-ordering/public-before-private/test.ts.lint rename test/rules/member-ordering/{private => public-before-private}/tslint.json (100%) create mode 100644 test/rules/member-ordering/static-before-instance/test.ts.lint rename test/rules/member-ordering/{static => static-before-instance}/tslint.json (100%) delete mode 100644 test/rules/member-ordering/static/test.ts.lint rename test/rules/member-ordering/{order => }/statics-first/test.ts.lint (100%) rename test/rules/member-ordering/{order => }/statics-first/tslint.json (100%) rename test/rules/member-ordering/{method => variables-before-functions}/test.ts.lint (66%) rename test/rules/member-ordering/{method => variables-before-functions}/tslint.json (100%) diff --git a/src/rules/memberOrderingRule.ts b/src/rules/memberOrderingRule.ts index 8d558c9b2d8..834dac52d70 100644 --- a/src/rules/memberOrderingRule.ts +++ b/src/rules/memberOrderingRule.ts @@ -18,16 +18,28 @@ import * as ts from "typescript"; import * as Lint from "../index"; -/* start old options */ -const OPTION_VARIABLES_BEFORE_FUNCTIONS = "variables-before-functions"; -const OPTION_STATIC_BEFORE_INSTANCE = "static-before-instance"; -const OPTION_PUBLIC_BEFORE_PRIVATE = "public-before-private"; -/* end old options */ - -/* start new options */ const OPTION_ORDER = "order"; -const PRESET_ORDERS: { [preset: string]: string[] } = { - "fields-first": [ + +enum MemberKind { + publicStaticField, + publicStaticMethod, + protectedStaticField, + protectedStaticMethod, + privateStaticField, + privateStaticMethod, + publicInstanceField, + protectedInstanceField, + privateInstanceField, + publicConstructor, + protectedConstructor, + privateConstructor, + publicInstanceMethod, + protectedInstanceMethod, + privateInstanceMethod, +} + +const PRESETS = new Map([ + ["fields-first", [ "public-static-field", "protected-static-field", "private-static-field", @@ -41,8 +53,8 @@ const PRESET_ORDERS: { [preset: string]: string[] } = { "public-instance-method", "protected-instance-method", "private-instance-method", - ], - "instance-sandwich": [ + ]], + ["instance-sandwich", [ "public-static-field", "protected-static-field", "private-static-field", @@ -56,8 +68,8 @@ const PRESET_ORDERS: { [preset: string]: string[] } = { "public-static-method", "protected-static-method", "private-static-method", - ], - "statics-first": [ + ]], + ["statics-first", [ "public-static-field", "public-static-method", "protected-static-field", @@ -71,9 +83,42 @@ const PRESET_ORDERS: { [preset: string]: string[] } = { "public-instance-method", "protected-instance-method", "private-instance-method", - ], -}; -/* end new options */ + ]], +]); +const PRESET_NAMES = Array.from(PRESETS.keys()); + +const allMemberKindNames = mapDefined(Object.keys(MemberKind), (key) => { + const mk = (MemberKind as any)[key]; + return typeof mk === "number" ? MemberKind[mk].replace(/[A-Z]/g, (cap) => "-" + cap.toLowerCase()) : undefined; +}); + +function namesMarkdown(names: string[]): string { + return names.map((name) => "* `" + name + "`").join("\n "); +} + +const optionsDescription = Lint.Utils.dedent` + One argument, which is an object, must be provided. It should contain an \`order\` property. + The \`order\` property should have a value of one of the following strings: + + ${namesMarkdown(PRESET_NAMES)} + + Alternatively, the value for \`order\` maybe be an array consisting of the following strings: + + ${namesMarkdown(allMemberKindNames)} + + You can also omit the access modifier to refer to "public-", "protected-", and "private-" all at once; for example, "static-field". + + You can also make your own categories by using an object instead of a string: + + { + "name": "static non-private", + "kinds": [ + "public-static-field", + "protected-static-field", + "public-static-method", + "protected-static-method" + ] + }`; export class Rule extends Lint.Rules.AbstractRule { /* tslint:disable:object-literal-sort-keys */ @@ -81,43 +126,19 @@ export class Rule extends Lint.Rules.AbstractRule { ruleName: "member-ordering", description: "Enforces member ordering.", rationale: "A consistent ordering for class members can make classes easier to read, navigate, and edit.", - optionsDescription: Lint.Utils.dedent` - One argument, which is an object, must be provided. It should contain an \`order\` property. - The \`order\` property should have a value of one of the following strings: - - * \`fields-first\` - * \`statics-first\` - * \`instance-sandwich\` - - Alternatively, the value for \`order\` maybe be an array consisting of the following strings: - - * \`public-static-field\` - * \`protected-static-field\` - * \`private-static-field\` - * \`public-instance-field\` - * \`protected-instance-field\` - * \`private-instance-field\` - * \`constructor\` - * \`public-static-method\` - * \`protected-static-method\` - * \`private-static-method\` - * \`public-instance-method\` - * \`protected-instance-method\` - * \`private-instance-method\` - - This is useful if one of the preset orders does not meet your needs.`, + optionsDescription, options: { type: "object", properties: { order: { oneOf: [{ type: "string", - enum: ["fields-first", "statics-first", "instance-sandwich"], + enum: PRESET_NAMES, }, { type: "array", items: { type: "string", - enum: PRESET_ORDERS["statics-first"], + enum: allMemberKindNames, }, maxLength: 13, }], @@ -125,7 +146,35 @@ export class Rule extends Lint.Rules.AbstractRule { }, additionalProperties: false, }, - optionExamples: ['[true, { "order": "fields-first" }]'], + optionExamples: [ + '[true, { "order": "fields-first" }]', + Lint.Utils.dedent` + [true, { + "order": [ + "static-field", + "instance-field", + "constructor", + "public-instance-method", + "protected-instance-method", + "private-instance-method" + ] + }]`, + Lint.Utils.dedent` + [true, { + "order": [ + { + "name": "static non-private", + "kinds": [ + "public-static-field", + "protected-static-field", + "public-static-method", + "protected-static-method" + ] + }, + "constructor" + ] + }]`, + ], type: "typescript", typescriptOnly: true, }; @@ -135,262 +184,253 @@ export class Rule extends Lint.Rules.AbstractRule { } } -/* start code supporting old options (i.e. "public-before-private") */ -interface IModifiers { - isMethod: boolean; - isPrivate: boolean; - isInstance: boolean; -} - -function getModifiers(isMethod: boolean, modifiers?: ts.ModifiersArray): IModifiers { - return { - isInstance: !Lint.hasModifier(modifiers, ts.SyntaxKind.StaticKeyword), - isMethod, - isPrivate: Lint.hasModifier(modifiers, ts.SyntaxKind.PrivateKeyword), - }; -} - -function toString(modifiers: IModifiers): string { - return [ - modifiers.isPrivate ? "private" : "public", - modifiers.isInstance ? "instance" : "static", - "member", - modifiers.isMethod ? "function" : "variable", - ].join(" "); -} -/* end old code */ - -/* start new code */ -enum AccessLevel { - PRIVATE, - PROTECTED, - PUBLIC, -} - -enum Membership { - INSTANCE, - STATIC, -} - -enum Kind { - FIELD, - METHOD, -} - -interface INodeAndModifiers { - accessLevel: AccessLevel; - isConstructor: boolean; - kind: Kind; - membership: Membership; - node: ts.Node; -} - -function getNodeAndModifiers(node: ts.Node, isMethod: boolean, isConstructor = false): INodeAndModifiers { - const { modifiers } = node; - const accessLevel = Lint.hasModifier(modifiers, ts.SyntaxKind.PrivateKeyword) ? AccessLevel.PRIVATE - : Lint.hasModifier(modifiers, ts.SyntaxKind.ProtectedKeyword) ? AccessLevel.PROTECTED - : AccessLevel.PUBLIC; - const kind = isMethod ? Kind.METHOD : Kind.FIELD; - const membership = Lint.hasModifier(modifiers, ts.SyntaxKind.StaticKeyword) ? Membership.STATIC : Membership.INSTANCE; - return { - accessLevel, - isConstructor, - kind, - membership, - node, - }; -} +export class MemberOrderingWalker extends Lint.RuleWalker { + private readonly order: MemberCategory[]; -function getNodeOption({accessLevel, isConstructor, kind, membership}: INodeAndModifiers) { - if (isConstructor) { - return "constructor"; + constructor(sourceFile: ts.SourceFile, options: Lint.IOptions) { + super(sourceFile, options); + this.order = getOrder(this.getOptions()); } - return [ - AccessLevel[accessLevel].toLowerCase(), - Membership[membership].toLowerCase(), - Kind[kind].toLowerCase(), - ].join("-"); -} -/* end new code */ - -export class MemberOrderingWalker extends Lint.RuleWalker { - private previousMember: IModifiers; - private memberStack: INodeAndModifiers[][] = []; - private hasOrderOption = this.getHasOrderOption(); - public visitClassDeclaration(node: ts.ClassDeclaration) { - this.resetPreviousModifiers(); - - this.newMemberList(); + this.visitMembers(node.members); super.visitClassDeclaration(node); - this.checkMemberOrder(); } public visitClassExpression(node: ts.ClassExpression) { - this.resetPreviousModifiers(); - - this.newMemberList(); + this.visitMembers(node.members); super.visitClassExpression(node); - this.checkMemberOrder(); } public visitInterfaceDeclaration(node: ts.InterfaceDeclaration) { - this.resetPreviousModifiers(); - - this.newMemberList(); + this.visitMembers(node.members); super.visitInterfaceDeclaration(node); - this.checkMemberOrder(); } - public visitMethodDeclaration(node: ts.MethodDeclaration) { - this.checkModifiersAndSetPrevious(node, getModifiers(true, node.modifiers)); - this.pushMember(getNodeAndModifiers(node, true)); - super.visitMethodDeclaration(node); + public visitTypeLiteral(node: ts.TypeLiteralNode) { + this.visitMembers(node.members); + super.visitTypeLiteral(node); } - public visitMethodSignature(node: ts.SignatureDeclaration) { - this.checkModifiersAndSetPrevious(node, getModifiers(true, node.modifiers)); - this.pushMember(getNodeAndModifiers(node, true)); - super.visitMethodSignature(node); + private visitMembers(members: Member[]) { + let prevRank = -1; + for (const member of members) { + const rank = this.memberRank(member); + if (rank === -1) { + // no explicit ordering for this kind of node specified, so continue + continue; + } + + if (rank < prevRank) { + const nodeType = this.rankName(rank); + const prevNodeType = this.rankName(prevRank); + const lowerRank = this.findLowerRank(members, rank); + const locationHint = lowerRank !== -1 + ? `after ${this.rankName(lowerRank)}s` + : "at the beginning of the class/interface"; + const errorLine1 = `Declaration of ${nodeType} not allowed after declaration of ${prevNodeType}. ` + + `Instead, this should come ${locationHint}.`; + this.addFailureAtNode(member, errorLine1); + } else { + // keep track of last good node + prevRank = rank; + } + } } - public visitConstructorDeclaration(node: ts.ConstructorDeclaration) { - this.checkModifiersAndSetPrevious(node, getModifiers(true, node.modifiers)); - this.pushMember(getNodeAndModifiers(node, true, true)); - super.visitConstructorDeclaration(node); + /** Finds the highest existing rank lower than `targetRank`. */ + private findLowerRank(members: Member[], targetRank: Rank): Rank | -1 { + let max: Rank | -1 = -1; + for (const member of members) { + const rank = this.memberRank(member); + if (rank !== -1 && rank < targetRank) { + max = Math.max(max, rank); + } + } + return max; } - public visitPropertyDeclaration(node: ts.PropertyDeclaration) { - const { initializer } = node; - const isFunction = initializer != null - && (initializer.kind === ts.SyntaxKind.ArrowFunction || initializer.kind === ts.SyntaxKind.FunctionExpression); - this.checkModifiersAndSetPrevious(node, getModifiers(isFunction, node.modifiers)); - this.pushMember(getNodeAndModifiers(node, isFunction)); - super.visitPropertyDeclaration(node); + private memberRank(member: Member): Rank | -1 { + const optionName = getMemberKind(member); + if (optionName === undefined) { + return -1; + } + return this.order.findIndex((category) => category.has(optionName)); } - public visitPropertySignature(node: ts.PropertyDeclaration) { - this.checkModifiersAndSetPrevious(node, getModifiers(false, node.modifiers)); - this.pushMember(getNodeAndModifiers(node, false)); - super.visitPropertySignature(node); + private rankName(rank: Rank): string { + return this.order[rank].name; } +} + +function memberKindForConstructor(access: Access): MemberKind { + return (MemberKind as any)[access + "Constructor"]; +} + +function memberKindForMethodOrField(access: Access, membership: "Static" | "Instance", kind: "Method" | "Field"): MemberKind { + return (MemberKind as any)[access + membership + kind]; +} + +const allAccess: Access[] = ["public", "protected", "private"]; - public visitTypeLiteral(_node: ts.TypeLiteralNode) { - // don't call super from here -- we want to skip the property declarations in type literals +function memberKindFromName(name: string): MemberKind[] { + const kind = (MemberKind as any)[Lint.Utils.camelize(name)]; + return typeof kind === "number" ? [kind as MemberKind] : allAccess.map(addModifier); + + function addModifier(modifier: string) { + const modifiedKind = (MemberKind as any)[Lint.Utils.camelize(modifier + "-" + name)]; + if (typeof modifiedKind !== "number") { + throw new Error(`Bad member kind: ${name}`); + } + return modifiedKind; } +} + +function getMemberKind(member: Member): MemberKind | undefined { + const accessLevel = hasModifier(ts.SyntaxKind.PrivateKeyword) ? "private" + : hasModifier(ts.SyntaxKind.ProtectedKeyword) ? "protected" + : "public"; + + switch (member.kind) { + case ts.SyntaxKind.Constructor: + case ts.SyntaxKind.ConstructSignature: + return memberKindForConstructor(accessLevel); + + case ts.SyntaxKind.PropertyDeclaration: + case ts.SyntaxKind.PropertySignature: + return methodOrField(isFunctionLiteral((member as ts.PropertyDeclaration).initializer)); - public visitObjectLiteralExpression(_node: ts.ObjectLiteralExpression) { - // again, don't call super here - object literals can have methods, - // and we don't wan't to check these + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.MethodSignature: + return methodOrField(true); + + default: + return undefined; } - /* start old code */ - private resetPreviousModifiers() { - this.previousMember = { - isInstance: false, - isMethod: false, - isPrivate: false, - }; + function methodOrField(isMethod: boolean) { + const membership = hasModifier(ts.SyntaxKind.StaticKeyword) ? "Static" : "Instance"; + return memberKindForMethodOrField(accessLevel, membership, isMethod ? "Method" : "Field"); } - private checkModifiersAndSetPrevious(node: ts.Node, currentMember: IModifiers) { - if (!this.canAppearAfter(this.previousMember, currentMember)) { - this.addFailureAtNode(node, - `Declaration of ${toString(currentMember)} not allowed to appear after declaration of ${toString(this.previousMember)}`); - } - this.previousMember = currentMember; + function hasModifier(kind: ts.SyntaxKind) { + return Lint.hasModifier(member.modifiers, kind); } +} - private canAppearAfter(previousMember: IModifiers, currentMember: IModifiers) { - if (previousMember == null || currentMember == null) { - return true; - } +type MemberCategoryJson = { name: string, kinds: string[] } | string; +class MemberCategory { + constructor(readonly name: string, private readonly kinds: Set) {} + public has(kind: MemberKind) { return this.kinds.has(kind); } +} - if (this.hasOption(OPTION_VARIABLES_BEFORE_FUNCTIONS) && previousMember.isMethod !== currentMember.isMethod) { - return Number(previousMember.isMethod) < Number(currentMember.isMethod); - } +type Member = ts.TypeElement | ts.ClassElement; +type Rank = number; - if (this.hasOption(OPTION_STATIC_BEFORE_INSTANCE) && previousMember.isInstance !== currentMember.isInstance) { - return Number(previousMember.isInstance) < Number(currentMember.isInstance); - } +type Access = "public" | "protected" | "private"; - if (this.hasOption(OPTION_PUBLIC_BEFORE_PRIVATE) && previousMember.isPrivate !== currentMember.isPrivate) { - return Number(previousMember.isPrivate) < Number(currentMember.isPrivate); - } +function getOrder(options: any[]): MemberCategory[] { + return getOrderJson(options).map((cat) => typeof cat === "string" + ? new MemberCategory(cat.replace(/-/g, " "), new Set(memberKindFromName(cat))) + : new MemberCategory(cat.name, new Set(flatMap(cat.kinds, memberKindFromName)))); +} +function getOrderJson(allOptions: any[]): MemberCategoryJson[] { + if (allOptions == null || allOptions.length === 0 || allOptions[0] == null) { + throw new Error("Got empty options"); + } - return true; + const firstOption = allOptions[0]; + if (typeof firstOption !== "object") { + // Undocumented direct string option. Deprecate eventually. + return convertFromOldStyleOptions(allOptions); // presume it to be string[] } - /* end old code */ - /* start new code */ - private newMemberList() { - if (this.hasOrderOption) { - this.memberStack.push([]); - } + return categoryFromOption(firstOption[OPTION_ORDER]); +} +function categoryFromOption(orderOption: {}): MemberCategoryJson[] { + if (Array.isArray(orderOption)) { + return orderOption; } - private pushMember(node: INodeAndModifiers) { - if (this.hasOrderOption) { - this.memberStack[this.memberStack.length - 1].push(node); - } + const preset = PRESETS.get(orderOption as string); + if (!preset) { + throw new Error(`Bad order: ${JSON.stringify(orderOption)}`); + } + return preset; +} + +/** + * Convert from undocumented old-style options. + * This is designed to mimic the old behavior and should be removed eventually. + */ +function convertFromOldStyleOptions(options: string[]): MemberCategoryJson[] { + let categories: NameAndKinds[] = [{ name: "member", kinds: allMemberKindNames }]; + if (hasOption("variables-before-functions")) { + categories = splitOldStyleOptions(categories, (kind) => kind.includes("field"), "field", "method"); } + if (hasOption("static-before-instance")) { + categories = splitOldStyleOptions(categories, (kind) => kind.includes("static"), "static", "instance"); + } + if (hasOption("public-before-private")) { + // 'protected' is considered public + categories = splitOldStyleOptions(categories, (kind) => !kind.includes("private"), "public", "private"); + } + return categories; - private checkMemberOrder() { - if (this.hasOrderOption) { - const memberList = this.memberStack.pop(); - const order = this.getOrder(); - if (memberList !== undefined && order !== null) { - const memberRank = memberList.map((n) => order.indexOf(getNodeOption(n))); - - let prevRank = -1; - memberRank.forEach((rank, i) => { - // no explicit ordering for this kind of node specified, so continue - if (rank === -1) { return; } - - // node should have come before last node, so add a failure - if (rank < prevRank) { - // generate a nice and clear error message - const node = memberList[i].node; - const nodeType = order[rank].split("-").join(" "); - const prevNodeType = order[prevRank].split("-").join(" "); - - const lowerRanks = memberRank.filter((r) => r < rank && r !== -1).sort(); - const locationHint = lowerRanks.length > 0 - ? `after ${order[lowerRanks[lowerRanks.length - 1]].split("-").join(" ")}s` - : "at the beginning of the class/interface"; - - const errorLine1 = `Declaration of ${nodeType} not allowed after declaration of ${prevNodeType}. ` + - `Instead, this should come ${locationHint}.`; - this.addFailureAtNode(node, errorLine1); - } else { - // keep track of last good node - prevRank = rank; - } - }); + function hasOption(x: string): boolean { + return options.indexOf(x) !== -1; + } +} +interface NameAndKinds { name: string; kinds: string[]; } +function splitOldStyleOptions(categories: NameAndKinds[], filter: (name: string) => boolean, a: string, b: string): NameAndKinds[] { + const newCategories: NameAndKinds[] = []; + for (const cat of categories) { + const yes = []; const no = []; + for (const kind of cat.kinds) { + if (filter(kind)) { + yes.push(kind); + } else { + no.push(kind); } } + const augmentName = (s: string) => { + if (a === "field") { + // Replace "member" with "field"/"method" instead of augmenting. + return s; + } + return `${s} ${cat.name}`; + }; + newCategories.push({ name: augmentName(a), kinds: yes }); + newCategories.push({ name: augmentName(b), kinds: no }); } + return newCategories; +} - private getHasOrderOption() { - const allOptions = this.getOptions(); - if (allOptions == null || allOptions.length === 0) { +function isFunctionLiteral(node: ts.Node | undefined) { + switch (node && node.kind) { + case ts.SyntaxKind.ArrowFunction: + case ts.SyntaxKind.FunctionExpression: + return true; + default: return false; - } - - const firstOption = allOptions[0]; - return firstOption != null && typeof firstOption === "object" && firstOption[OPTION_ORDER] != null; } +} - // assumes this.hasOrderOption() === true - private getOrder(): string[] | null { - const orderOption = this.getOptions()[0][OPTION_ORDER]; - if (Array.isArray(orderOption)) { - return orderOption; - } else if (typeof orderOption === "string") { - return PRESET_ORDERS[orderOption] || PRESET_ORDERS["default"]; +function mapDefined(inputs: T[], getOutput: (input: T) => U | undefined): U[] { + const out = []; + for (const input of inputs) { + const output = getOutput(input); + if (output !== undefined) { + out.push(output); } - return null; } - /* end new code */ + return out; +} + +function flatMap(inputs: T[], getOutputs: (input: T) => U[]): U[] { + const out = []; + for (const input of inputs) { + out.push(...getOutputs(input)); + } + return out; } diff --git a/test/rules/member-ordering/custom-categories/test.ts.lint b/test/rules/member-ordering/custom-categories/test.ts.lint new file mode 100644 index 00000000000..6df80c3273e --- /dev/null +++ b/test/rules/member-ordering/custom-categories/test.ts.lint @@ -0,0 +1,10 @@ +class C { + static foo() {} + protected static bar = 0; + static baz(); + + constructor(); + + static bang(); + ~~~~~~~~~~~~~~ [Declaration of static non-private not allowed after declaration of constructor. Instead, this should come at the beginning of the class/interface.] +} diff --git a/test/rules/member-ordering/custom-categories/tslint.json b/test/rules/member-ordering/custom-categories/tslint.json new file mode 100644 index 00000000000..ddacc962238 --- /dev/null +++ b/test/rules/member-ordering/custom-categories/tslint.json @@ -0,0 +1,18 @@ +{ + "rules": { + "member-ordering": [true, { + "order": [ + { + "name": "static non-private", + "kinds": [ + "public-static-field", + "protected-static-field", + "public-static-method", + "protected-static-method" + ] + }, + "constructor" + ] + }] + } +} diff --git a/test/rules/member-ordering/order/custom/test.ts.lint b/test/rules/member-ordering/custom/test.ts.lint similarity index 100% rename from test/rules/member-ordering/order/custom/test.ts.lint rename to test/rules/member-ordering/custom/test.ts.lint diff --git a/test/rules/member-ordering/order/custom/tslint.json b/test/rules/member-ordering/custom/tslint.json similarity index 100% rename from test/rules/member-ordering/order/custom/tslint.json rename to test/rules/member-ordering/custom/tslint.json diff --git a/test/rules/member-ordering/order/fields-first/test.ts.lint b/test/rules/member-ordering/fields-first/test.ts.lint similarity index 100% rename from test/rules/member-ordering/order/fields-first/test.ts.lint rename to test/rules/member-ordering/fields-first/test.ts.lint diff --git a/test/rules/member-ordering/order/fields-first/tslint.json b/test/rules/member-ordering/fields-first/tslint.json similarity index 100% rename from test/rules/member-ordering/order/fields-first/tslint.json rename to test/rules/member-ordering/fields-first/tslint.json diff --git a/test/rules/member-ordering/order/instance-sandwich/test.ts.lint b/test/rules/member-ordering/instance-sandwich/test.ts.lint similarity index 100% rename from test/rules/member-ordering/order/instance-sandwich/test.ts.lint rename to test/rules/member-ordering/instance-sandwich/test.ts.lint diff --git a/test/rules/member-ordering/order/instance-sandwich/tslint.json b/test/rules/member-ordering/instance-sandwich/tslint.json similarity index 100% rename from test/rules/member-ordering/order/instance-sandwich/tslint.json rename to test/rules/member-ordering/instance-sandwich/tslint.json diff --git a/test/rules/member-ordering/mix-old-options/test.ts.lint b/test/rules/member-ordering/mix-old-options/test.ts.lint new file mode 100644 index 00000000000..109ef9699e3 --- /dev/null +++ b/test/rules/member-ordering/mix-old-options/test.ts.lint @@ -0,0 +1,16 @@ +class Foo { + public static x: number; + private static y: number; + public x: number; + private y: number; + + public static m() {} + private static n() {} + + constructor() {} + public m() {} + private n() {} + + z: number; + ~~~~~~~~~~ [Declaration of public instance field not allowed after declaration of private instance method. Instead, this should come after private static fields.] +} diff --git a/test/rules/member-ordering/mix-old-options/tslint.json b/test/rules/member-ordering/mix-old-options/tslint.json new file mode 100644 index 00000000000..659333c1fbe --- /dev/null +++ b/test/rules/member-ordering/mix-old-options/tslint.json @@ -0,0 +1,9 @@ +{ + "rules": { + "member-ordering": [true, + "public-before-private", + "static-before-instance", + "variables-before-functions" + ] + } +} diff --git a/test/rules/member-ordering/omit-access-modifier/test.ts.lint b/test/rules/member-ordering/omit-access-modifier/test.ts.lint new file mode 100644 index 00000000000..fdfbf683047 --- /dev/null +++ b/test/rules/member-ordering/omit-access-modifier/test.ts.lint @@ -0,0 +1,12 @@ +class C { + private static x = 0; + static y = 1; + + x = 0; + private y = 1; + + constructor() {} + + static z = 2; + ~~~~~~~~~~~~~ [Declaration of static field not allowed after declaration of constructor. Instead, this should come at the beginning of the class/interface.] +} diff --git a/test/rules/member-ordering/omit-access-modifier/tslint.json b/test/rules/member-ordering/omit-access-modifier/tslint.json new file mode 100644 index 00000000000..c88314eb612 --- /dev/null +++ b/test/rules/member-ordering/omit-access-modifier/tslint.json @@ -0,0 +1,9 @@ +{ + "rules": { + "member-ordering": [true, { "order": [ + "static-field", + "instance-field", + "constructor" + ]}] + } +} diff --git a/test/rules/member-ordering/private/test.ts.lint b/test/rules/member-ordering/private/test.ts.lint deleted file mode 100644 index 24f637796f7..00000000000 --- a/test/rules/member-ordering/private/test.ts.lint +++ /dev/null @@ -1,10 +0,0 @@ -class Foo { - private x: number; - private bar(): any { - var bla: { a: string } = {a: '1'}; - } - y: number; - ~~~~~~~~~~ [0] -} - -[0]: Declaration of public instance member variable not allowed to appear after declaration of private instance member function diff --git a/test/rules/member-ordering/public-before-private/test.ts.lint b/test/rules/member-ordering/public-before-private/test.ts.lint new file mode 100644 index 00000000000..c26be55d8ac --- /dev/null +++ b/test/rules/member-ordering/public-before-private/test.ts.lint @@ -0,0 +1,10 @@ +class Foo { + private x: number; + private bar(): any { + var bla: { a: string } = {a: '1'}; + } + y: number; + ~~~~~~~~~~ [0] +} + +[0]: Declaration of public member not allowed after declaration of private member. Instead, this should come at the beginning of the class/interface. diff --git a/test/rules/member-ordering/private/tslint.json b/test/rules/member-ordering/public-before-private/tslint.json similarity index 100% rename from test/rules/member-ordering/private/tslint.json rename to test/rules/member-ordering/public-before-private/tslint.json diff --git a/test/rules/member-ordering/static-before-instance/test.ts.lint b/test/rules/member-ordering/static-before-instance/test.ts.lint new file mode 100644 index 00000000000..532440d9837 --- /dev/null +++ b/test/rules/member-ordering/static-before-instance/test.ts.lint @@ -0,0 +1,10 @@ +class Foo { + x: number; + static y: number; + ~~~~~~~~~~~~~~~~~ [0] + constructor() { + // nothing to do + } +} + +[0]: Declaration of static member not allowed after declaration of instance member. Instead, this should come at the beginning of the class/interface. diff --git a/test/rules/member-ordering/static/tslint.json b/test/rules/member-ordering/static-before-instance/tslint.json similarity index 100% rename from test/rules/member-ordering/static/tslint.json rename to test/rules/member-ordering/static-before-instance/tslint.json diff --git a/test/rules/member-ordering/static/test.ts.lint b/test/rules/member-ordering/static/test.ts.lint deleted file mode 100644 index b5396b81a18..00000000000 --- a/test/rules/member-ordering/static/test.ts.lint +++ /dev/null @@ -1,10 +0,0 @@ -class Foo { - x: number; - static y: number; - ~~~~~~~~~~~~~~~~~ [0] - constructor() { - // nothing to do - } -} - -[0]: Declaration of public static member variable not allowed to appear after declaration of public instance member variable diff --git a/test/rules/member-ordering/order/statics-first/test.ts.lint b/test/rules/member-ordering/statics-first/test.ts.lint similarity index 100% rename from test/rules/member-ordering/order/statics-first/test.ts.lint rename to test/rules/member-ordering/statics-first/test.ts.lint diff --git a/test/rules/member-ordering/order/statics-first/tslint.json b/test/rules/member-ordering/statics-first/tslint.json similarity index 100% rename from test/rules/member-ordering/order/statics-first/tslint.json rename to test/rules/member-ordering/statics-first/tslint.json diff --git a/test/rules/member-ordering/method/test.ts.lint b/test/rules/member-ordering/variables-before-functions/test.ts.lint similarity index 66% rename from test/rules/member-ordering/method/test.ts.lint rename to test/rules/member-ordering/variables-before-functions/test.ts.lint index 1e552a3cbef..e657b567d72 100644 --- a/test/rules/member-ordering/method/test.ts.lint +++ b/test/rules/member-ordering/variables-before-functions/test.ts.lint @@ -52,5 +52,20 @@ class Constructor2 { ~~~~~~~~~~~~~~~~~ [0] } -[0]: Declaration of public instance member variable not allowed to appear after declaration of public instance member function +// Works for type literal, just like interface +type T = { + x(): void; + y: number; + ~~~~~~~~~~ [0] +} + +// Works for class inside object literal +const o = { + foo: class C { + x(): void; + y: number; + ~~~~~~~~~~ [0] + } +} +[0]: Declaration of field not allowed after declaration of method. Instead, this should come at the beginning of the class/interface. diff --git a/test/rules/member-ordering/method/tslint.json b/test/rules/member-ordering/variables-before-functions/tslint.json similarity index 100% rename from test/rules/member-ordering/method/tslint.json rename to test/rules/member-ordering/variables-before-functions/tslint.json From 7eed9cec9d603e56696bafdf4c3f7a5a96ebd349 Mon Sep 17 00:00:00 2001 From: Klaus Meinhardt Date: Thu, 26 Jan 2017 13:03:58 +0100 Subject: [PATCH 070/131] Fix whitespaceRule at EOF (#2131) Fixed: #2129 --- src/rules/whitespaceRule.ts | 5 ++++- test/rules/whitespace/check-separator/test.ts.lint | 1 + test/rules/whitespace/check-separator/tslint.json | 5 +++++ 3 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 test/rules/whitespace/check-separator/test.ts.lint create mode 100644 test/rules/whitespace/check-separator/tslint.json diff --git a/src/rules/whitespaceRule.ts b/src/rules/whitespaceRule.ts index 18aa338f5af..711693cf97f 100644 --- a/src/rules/whitespaceRule.ts +++ b/src/rules/whitespaceRule.ts @@ -81,7 +81,10 @@ class WhitespaceWalker extends Lint.RuleWalker { let prevTokenShouldBeFollowedByWhitespace = false; Lint.forEachToken(node, false, (_text, tokenKind, pos, parent) => { - if (tokenKind === ts.SyntaxKind.WhitespaceTrivia || tokenKind === ts.SyntaxKind.NewLineTrivia) { + if (tokenKind === ts.SyntaxKind.WhitespaceTrivia || + tokenKind === ts.SyntaxKind.NewLineTrivia || + tokenKind === ts.SyntaxKind.EndOfFileToken) { + prevTokenShouldBeFollowedByWhitespace = false; return; } else if (prevTokenShouldBeFollowedByWhitespace) { diff --git a/test/rules/whitespace/check-separator/test.ts.lint b/test/rules/whitespace/check-separator/test.ts.lint new file mode 100644 index 00000000000..57cb542c501 --- /dev/null +++ b/test/rules/whitespace/check-separator/test.ts.lint @@ -0,0 +1 @@ +let foo = "bar"; \ No newline at end of file diff --git a/test/rules/whitespace/check-separator/tslint.json b/test/rules/whitespace/check-separator/tslint.json new file mode 100644 index 00000000000..82d10d10b1c --- /dev/null +++ b/test/rules/whitespace/check-separator/tslint.json @@ -0,0 +1,5 @@ +{ + "rules": { + "whitespace": [true, "check-separator"] + } +} From 5f628efd4bde4880ba611e7c43b7da065e54526e Mon Sep 17 00:00:00 2001 From: Noah Chen Date: Thu, 26 Jan 2017 07:25:15 -0500 Subject: [PATCH 071/131] Trim formatter name (#2132) --- src/formatterLoader.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/formatterLoader.ts b/src/formatterLoader.ts index 32f7b09cdbc..942f40f8db9 100644 --- a/src/formatterLoader.ts +++ b/src/formatterLoader.ts @@ -34,6 +34,7 @@ export function findFormatter(name: string | Function, formattersDirectory?: str if (isFunction(name)) { return name; } else if (isString(name)) { + name = name.trim(); const camelizedName = camelize(`${name}Formatter`); // first check for core formatters From c2362e8050259d8171445662e6eb840b3da9909e Mon Sep 17 00:00:00 2001 From: Noah Chen Date: Thu, 26 Jan 2017 09:03:35 -0500 Subject: [PATCH 072/131] Use ./bin/tslint to lint self (#2124) --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index bc8dd853844..c83a9dc9447 100644 --- a/package.json +++ b/package.json @@ -25,9 +25,10 @@ "compile:core": "tsc -p src", "compile:scripts": "tsc -p scripts", "compile:test": "tsc -p test", - "lint": "npm-run-all -p lint:core lint:test", + "lint": "npm-run-all -p lint:core lint:test lint:from-bin", "lint:core": "tslint \"src/**/*.ts\"", "lint:test": "tslint \"test/**/*.ts\" -e \"test/**/*.test.ts\"", + "lint:from-bin": "bin/tslint \"{src,test}/**/*.ts\" -e \"test/**/*.test.ts\"", "test": "npm-run-all test:pre -p test:mocha test:rules", "test:pre": "cd ./test/config && npm install", "test:mocha": "mocha --reporter spec --colors \"build/test/**/*Tests.js\" build/test/assert.js", From 6405fbfa90b25f95de1703866f071ed396b9ced4 Mon Sep 17 00:00:00 2001 From: Adi Dahiya Date: Thu, 26 Jan 2017 09:06:09 -0500 Subject: [PATCH 073/131] Clean up preferFunctionOverMethodRule (#2119) More descriptive failure message, minor code style fixes --- src/rules/preferFunctionOverMethodRule.ts | 42 +++++++++++-------- .../allow-protected/test.ts.lint | 2 +- .../allow-public-and-protected/test.ts.lint | 2 +- .../allow-public/test.ts.lint | 2 +- .../default/test.js.lint | 2 +- .../default/test.ts.lint | 2 +- 6 files changed, 30 insertions(+), 22 deletions(-) diff --git a/src/rules/preferFunctionOverMethodRule.ts b/src/rules/preferFunctionOverMethodRule.ts index a806a7b1c9f..a3eefc34492 100644 --- a/src/rules/preferFunctionOverMethodRule.ts +++ b/src/rules/preferFunctionOverMethodRule.ts @@ -26,10 +26,10 @@ export class Rule extends Lint.Rules.AbstractRule { /* tslint:disable:object-literal-sort-keys */ public static metadata: Lint.IRuleMetadata = { ruleName: "prefer-function-over-method", - description: "Warns for methods that do not use 'this'.", + description: "Warns for class methods that do not use 'this'.", optionsDescription: Lint.Utils.dedent` - "${OPTION_ALLOW_PUBLIC}" excludes public methods. - "${OPTION_ALLOW_PROTECTED}" excludes protected methods.`, + "${OPTION_ALLOW_PUBLIC}" excludes checking of public methods. + "${OPTION_ALLOW_PROTECTED}" excludes checking of protected methods.`, options: { type: "string", enum: [OPTION_ALLOW_PUBLIC, OPTION_ALLOW_PROTECTED], @@ -43,14 +43,14 @@ export class Rule extends Lint.Rules.AbstractRule { }; /* tslint:enable:object-literal-sort-keys */ - public static FAILURE_STRING = "Method does not use 'this'. Use a function instead."; + public static FAILURE_STRING = "Class method does not use 'this'. Use a function instead."; public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { - return this.applyWithWalker(new Walker(sourceFile, this.getOptions())); + return this.applyWithWalker(new PreferFunctionOverMethodWalker(sourceFile, this.getOptions())); } } -class Walker extends Lint.RuleWalker { +class PreferFunctionOverMethodWalker extends Lint.RuleWalker { private allowPublic = this.hasOption(OPTION_ALLOW_PUBLIC); private allowProtected = this.hasOption(OPTION_ALLOW_PROTECTED); private stack: ThisUsed[] = []; @@ -66,10 +66,11 @@ class Walker extends Lint.RuleWalker { const { name } = node as ts.MethodDeclaration; const usesThis = this.withThisScope( name.kind === ts.SyntaxKind.Identifier ? name.text : undefined, - () => super.visitNode(node)); - if (!usesThis && - node.parent!.kind !== ts.SyntaxKind.ObjectLiteralExpression && - this.shouldWarnForModifiers(node as ts.MethodDeclaration)) { + () => super.visitNode(node), + ); + if (!usesThis + && node.parent!.kind !== ts.SyntaxKind.ObjectLiteralExpression + && this.shouldWarnForModifiers(node as ts.MethodDeclaration)) { this.addFailureAtNode((node as ts.MethodDeclaration).name, Rule.FAILURE_STRING); } break; @@ -114,19 +115,26 @@ class Walker extends Lint.RuleWalker { } } -interface ThisUsed { readonly name: string | undefined; isThisUsed: boolean; } +interface ThisUsed { + readonly name: string | undefined; + isThisUsed: boolean; +} function isRecursiveCall(thisOrSuper: ts.Node, cur: ThisUsed) { const parent = thisOrSuper.parent!; - return thisOrSuper.kind === ts.SyntaxKind.ThisKeyword && - parent.kind === ts.SyntaxKind.PropertyAccessExpression && - (parent as ts.PropertyAccessExpression).name.text === cur.name; + return thisOrSuper.kind === ts.SyntaxKind.ThisKeyword + && parent.kind === ts.SyntaxKind.PropertyAccessExpression + && (parent as ts.PropertyAccessExpression).name.text === cur.name; } const enum Visibility { Public, Protected, Private } function methodVisibility(node: ts.MethodDeclaration): Visibility { - return Lint.hasModifier(node.modifiers, ts.SyntaxKind.PrivateKeyword) ? Visibility.Private : - Lint.hasModifier(node.modifiers, ts.SyntaxKind.ProtectedKeyword) ? Visibility.Protected : - Visibility.Public; + if (Lint.hasModifier(node.modifiers, ts.SyntaxKind.PrivateKeyword)) { + return Visibility.Private; + } else if (Lint.hasModifier(node.modifiers, ts.SyntaxKind.ProtectedKeyword)) { + return Visibility.Protected; + } else { + return Visibility.Public; + } } diff --git a/test/rules/prefer-function-over-method/allow-protected/test.ts.lint b/test/rules/prefer-function-over-method/allow-protected/test.ts.lint index e4df4b33c93..880413a27ec 100644 --- a/test/rules/prefer-function-over-method/allow-protected/test.ts.lint +++ b/test/rules/prefer-function-over-method/allow-protected/test.ts.lint @@ -6,4 +6,4 @@ class C { ~ [0] } -[0]: Method does not use 'this'. Use a function instead. +[0]: Class method does not use 'this'. Use a function instead. diff --git a/test/rules/prefer-function-over-method/allow-public-and-protected/test.ts.lint b/test/rules/prefer-function-over-method/allow-public-and-protected/test.ts.lint index 0642288c026..bef8ec11b35 100644 --- a/test/rules/prefer-function-over-method/allow-public-and-protected/test.ts.lint +++ b/test/rules/prefer-function-over-method/allow-public-and-protected/test.ts.lint @@ -5,4 +5,4 @@ class C { ~ [0] } -[0]: Method does not use 'this'. Use a function instead. +[0]: Class method does not use 'this'. Use a function instead. diff --git a/test/rules/prefer-function-over-method/allow-public/test.ts.lint b/test/rules/prefer-function-over-method/allow-public/test.ts.lint index f7f7f363d80..9781d94bf63 100644 --- a/test/rules/prefer-function-over-method/allow-public/test.ts.lint +++ b/test/rules/prefer-function-over-method/allow-public/test.ts.lint @@ -6,4 +6,4 @@ class C { ~ [0] } -[0]: Method does not use 'this'. Use a function instead. +[0]: Class method does not use 'this'. Use a function instead. diff --git a/test/rules/prefer-function-over-method/default/test.js.lint b/test/rules/prefer-function-over-method/default/test.js.lint index 5fd64ecbc48..69cfe162d07 100644 --- a/test/rules/prefer-function-over-method/default/test.js.lint +++ b/test/rules/prefer-function-over-method/default/test.js.lint @@ -1,4 +1,4 @@ class C { a() {} - ~ [Method does not use 'this'. Use a function instead.] + ~ [Class method does not use 'this'. Use a function instead.] } diff --git a/test/rules/prefer-function-over-method/default/test.ts.lint b/test/rules/prefer-function-over-method/default/test.ts.lint index 48ba5c89222..7cc2e39d5ac 100644 --- a/test/rules/prefer-function-over-method/default/test.ts.lint +++ b/test/rules/prefer-function-over-method/default/test.ts.lint @@ -48,4 +48,4 @@ const o = { x() {} } -[0]: Method does not use 'this'. Use a function instead. +[0]: Class method does not use 'this'. Use a function instead. From 7cb4a9851f608aa60f8b4ff96d57adc3307f9ae5 Mon Sep 17 00:00:00 2001 From: Noah Chen Date: Thu, 26 Jan 2017 09:20:12 -0500 Subject: [PATCH 074/131] Prepare for v4.4.2 (#2135) --- CHANGELOG.md | 6 ++++++ package.json | 2 +- src/linter.ts | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ad5e459a6d..ddccb9fbabb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ Change Log === +v4.4.2 +--- + +* [bugfix] `whitespace` rule caused false positive on EOF (#2131) +* [bugfix] WebStorm fails because `json` formatter parameter has extra space (#2132) + v4.4.1 --- diff --git a/package.json b/package.json index c83a9dc9447..a1aaccae2ac 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tslint", - "version": "4.4.1", + "version": "4.4.2", "description": "An extensible static analysis linter for the TypeScript language", "bin": { "tslint": "./bin/tslint" diff --git a/src/linter.ts b/src/linter.ts index b66d02e2a0f..43984bb9916 100644 --- a/src/linter.ts +++ b/src/linter.ts @@ -43,7 +43,7 @@ import { arrayify, dedent } from "./utils"; * Linter that can lint multiple files in consecutive runs. */ class Linter { - public static VERSION = "4.4.1"; + public static VERSION = "4.4.2"; public static findConfiguration = findConfiguration; public static findConfigurationPath = findConfigurationPath; From 140488074c84152a2112ea0809f94b46163cae2a Mon Sep 17 00:00:00 2001 From: Noah Chen Date: Thu, 26 Jan 2017 16:20:48 -0500 Subject: [PATCH 075/131] Write test to verify that each rule corresponds to a rule test (#2136) --- test/ruleLoaderTests.ts | 34 ++++++++++++++++++- .../test.ts.lint | 0 .../tslint.json | 0 3 files changed, 33 insertions(+), 1 deletion(-) rename test/rules/{mergeable-namespace => no-mergeable-namespace}/test.ts.lint (100%) rename test/rules/{mergeable-namespace => no-mergeable-namespace}/tslint.json (100%) diff --git a/test/ruleLoaderTests.ts b/test/ruleLoaderTests.ts index 21f784254f4..b0270118f80 100644 --- a/test/ruleLoaderTests.ts +++ b/test/ruleLoaderTests.ts @@ -15,7 +15,11 @@ * limitations under the License. */ -import {loadRules} from "./lint"; +import * as diff from "diff"; +import * as fs from "fs"; +import * as path from "path"; +import { camelize } from "../src/utils"; +import { loadRules } from "./lint"; describe("Rule Loader", () => { const RULES_DIRECTORY = "build/src/rules"; @@ -65,4 +69,32 @@ describe("Rule Loader", () => { const rules = loadRules(validConfiguration, {}, RULES_DIRECTORY, true); assert.equal(rules.length, 1); }); + + it("tests every rule", () => { + const rulesDir = "src/rules"; + const testsDir = "test/rules"; + const rules = fs.readdirSync(rulesDir) + .filter((file) => /Rule.ts$/.test(file)) + .map((file) => file.substr(0, file.length - "Rule.ts".length)) + .sort() + .join("\n"); + const tests = fs.readdirSync(testsDir) + .filter((file) => !file.startsWith("_") && fs.statSync(path.join(testsDir, file)).isDirectory()) + .map(camelize) + .sort() + .join("\n"); + const diffResults = diff.diffLines(rules, tests); + let testFailed = false; + for (const result of diffResults) { + if (result.added) { + console.warn("Test has no matching rule: " + result.value); + testFailed = true; + } else if (result.removed) { + console.warn("Missing test: " + result.value); + testFailed = true; + } + } + + assert.isFalse(testFailed, "List of rules doesn't match list of tests"); + }); }); diff --git a/test/rules/mergeable-namespace/test.ts.lint b/test/rules/no-mergeable-namespace/test.ts.lint similarity index 100% rename from test/rules/mergeable-namespace/test.ts.lint rename to test/rules/no-mergeable-namespace/test.ts.lint diff --git a/test/rules/mergeable-namespace/tslint.json b/test/rules/no-mergeable-namespace/tslint.json similarity index 100% rename from test/rules/mergeable-namespace/tslint.json rename to test/rules/no-mergeable-namespace/tslint.json From 67732d625069f104a0e9efd755a95e5369313966 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Fri, 27 Jan 2017 13:51:47 -0800 Subject: [PATCH 076/131] no-floating-promises: Fix metadata (#2142) --- src/rules/noFloatingPromisesRule.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/rules/noFloatingPromisesRule.ts b/src/rules/noFloatingPromisesRule.ts index b5612ace3f2..6f029e8d523 100644 --- a/src/rules/noFloatingPromisesRule.ts +++ b/src/rules/noFloatingPromisesRule.ts @@ -37,7 +37,8 @@ export class Rule extends Lint.Rules.TypedRule { optionExamples: ["true", `[true, "JQueryPromise"]`], rationale: "Unhandled Promises can cause unexpected behavior, such as resolving at unexpected times.", type: "functionality", - typescriptOnly: false, + typescriptOnly: true, + requiresTypeInfo: true, }; /* tslint:enable:object-literal-sort-keys */ From ffad65fa716cb753859e17e9ec1f1bbf36c4f03f Mon Sep 17 00:00:00 2001 From: Stefan Reichel Date: Sat, 28 Jan 2017 02:43:40 +0100 Subject: [PATCH 077/131] Set node engine version to 4.1.2 (#2148) --- appveyor.yml | 2 +- circle.yml | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 7cfb2bbfa21..ee9e14a7a5a 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,6 +1,6 @@ environment: matrix: - - nodejs_version: "4.2" + - nodejs_version: "4.1.2" - nodejs_version: "5.7" - nodejs_version: "6.1" diff --git a/circle.yml b/circle.yml index 358b6f4aff7..292df0344f8 100644 --- a/circle.yml +++ b/circle.yml @@ -5,7 +5,7 @@ general: dependencies: pre: - node ./scripts/assertMinCircleNodes.js $CIRCLE_NODE_TOTAL - - case $CIRCLE_NODE_INDEX in 0) nvm use 4.2 ;; 1) nvm use 5.7 ;; [2-3]) nvm use 6.1 ;; esac + - case $CIRCLE_NODE_INDEX in 0) nvm use 4.1.2 ;; 1) nvm use 5.7 ;; [2-3]) nvm use 6.1 ;; esac test: override: - case $CIRCLE_NODE_INDEX in [0-2]) npm run verify ;; 3) npm run clean && npm run compile && npm i typescript@2.0.10 && npm run test ;; esac: diff --git a/package.json b/package.json index a1aaccae2ac..04bd96c6953 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,6 @@ }, "license": "Apache-2.0", "engines": { - "node": ">=4.2.6" + "node": ">=4.1.2" } } From 26700c041c328040cd80a260cf90424621a96294 Mon Sep 17 00:00:00 2001 From: Shlomi Assaf Date: Sat, 28 Jan 2017 22:53:55 +0200 Subject: [PATCH 078/131] Add new object/array-destructuring options to typedef rule (#2146) --- src/rules/typedefRule.ts | 54 ++++++++++++++++--- test/rules/typedef/all/test.ts.lint | 12 +++++ test/rules/typedef/all/tslint.json | 4 +- .../typedef/array-destructuring/test.ts.lint | 4 ++ .../typedef/array-destructuring/tslint.json | 7 +++ test/rules/typedef/none/test.ts.lint | 8 +++ .../typedef/object-destructuring/test.ts.lint | 7 +++ .../typedef/object-destructuring/tslint.json | 7 +++ .../typedef/variable-declaration/test.ts.lint | 6 +++ .../typedef/variable-declaration/tslint.json | 7 +++ 10 files changed, 107 insertions(+), 9 deletions(-) create mode 100644 test/rules/typedef/array-destructuring/test.ts.lint create mode 100644 test/rules/typedef/array-destructuring/tslint.json create mode 100644 test/rules/typedef/object-destructuring/test.ts.lint create mode 100644 test/rules/typedef/object-destructuring/tslint.json create mode 100644 test/rules/typedef/variable-declaration/test.ts.lint create mode 100644 test/rules/typedef/variable-declaration/tslint.json diff --git a/src/rules/typedefRule.ts b/src/rules/typedefRule.ts index b4c97e1d1db..0e5326faf17 100644 --- a/src/rules/typedefRule.ts +++ b/src/rules/typedefRule.ts @@ -32,8 +32,10 @@ export class Rule extends Lint.Rules.AbstractRule { * \`"parameter"\` checks type specifier of function parameters for non-arrow functions. * \`"arrow-parameter"\` checks type specifier of function parameters for arrow functions. * \`"property-declaration"\` checks return types of interface properties. - * \`"variable-declaration"\` checks variable declarations. - * \`"member-variable-declaration"\` checks member variable declarations.`, + * \`"variable-declaration"\` checks non-binding variable declarations. + * \`"member-variable-declaration"\` checks member variable declarations. + * \`"object-destructuring"\` checks object destructuring declarations. + * \`"array-destructuring"\` checks array destructuring declarations.`, options: { type: "array", items: { @@ -46,6 +48,8 @@ export class Rule extends Lint.Rules.AbstractRule { "property-declaration", "variable-declaration", "member-variable-declaration", + "object-destructuring", + "array-destructuring", ], }, minLength: 0, @@ -194,7 +198,21 @@ class TypedefWalker extends Lint.RuleWalker { && (node as ts.Node).parent!.kind !== ts.SyntaxKind.CatchClause && node.parent.parent.kind !== ts.SyntaxKind.ForInStatement && node.parent.parent.kind !== ts.SyntaxKind.ForOfStatement) { - this.checkTypeAnnotation("variable-declaration", node.name.getEnd(), node.type, node.name); + + let rule: string; + switch (node.name.kind) { + case ts.SyntaxKind.ObjectBindingPattern: + rule = "object-destructuring"; + break; + case ts.SyntaxKind.ArrayBindingPattern: + rule = "array-destructuring"; + break; + default: + rule = "variable-declaration"; + break; + } + + this.checkTypeAnnotation(rule, node.name.getEnd(), node.type, node.name); } super.visitVariableDeclaration(node); } @@ -212,13 +230,33 @@ class TypedefWalker extends Lint.RuleWalker { typeAnnotation: ts.TypeNode | undefined, name?: ts.Node) { if (this.hasOption(option) && typeAnnotation == null) { - let ns = ""; - if (name != null && name.kind === ts.SyntaxKind.Identifier) { - ns = `: '${(name as ts.Identifier).text}'`; - } - this.addFailureAt(location, 1, "expected " + option + ns + " to have a typedef"); + this.addFailureAt(location, 1, "expected " + option + getName(name, ": '", "'") + " to have a typedef"); + } + } +} + +function getName(name?: ts.Node, prefix?: string, suffix?: string): string { + let ns = ""; + + if (name != null) { + switch (name.kind) { + case ts.SyntaxKind.Identifier: + ns = (name as ts.Identifier).text; + break; + case ts.SyntaxKind.BindingElement: + ns = getName((name as ts.BindingElement).name); + break; + case ts.SyntaxKind.ArrayBindingPattern: + ns = `[ ${(name as ts.ArrayBindingPattern).elements.map( (n) => getName(n) ).join(", ")} ]`; + break; + case ts.SyntaxKind.ObjectBindingPattern: + ns = `{ ${(name as ts.ObjectBindingPattern).elements.map( (n) => getName(n) ).join(", ")} }`; + break; + default: + break; } } + return ns ? `${prefix || ""}${ns}${suffix || ""}` : ""; } function isPropertyDeclaration(node: ts.Node): node is ts.PropertyDeclaration { diff --git a/test/rules/typedef/all/test.ts.lint b/test/rules/typedef/all/test.ts.lint index b2991703b9c..909f6c4b981 100644 --- a/test/rules/typedef/all/test.ts.lint +++ b/test/rules/typedef/all/test.ts.lint @@ -92,3 +92,15 @@ class A { ~ [expected member-variable-declaration: 'foo' to have a typedef] private bar: number; } + + +const { paramA, paramB } = { paramA: "test", paramB: 15 }; + ~ [expected object-destructuring: '{ paramA, paramB }' to have a typedef] + +const { paramA, paramB }: { paramA: string, paramB: number } = { paramA: "test", paramB: 15 }; + +const [ paramA, paramB ] = [15, 'test']; + ~ [expected array-destructuring: '[ paramA, paramB ]' to have a typedef] + +const [ paramA3, paramB3 ]: [ number, string ] = [15, 'test']; + diff --git a/test/rules/typedef/all/tslint.json b/test/rules/typedef/all/tslint.json index 1a085ce87a4..70ecde71823 100644 --- a/test/rules/typedef/all/tslint.json +++ b/test/rules/typedef/all/tslint.json @@ -7,7 +7,9 @@ "arrow-parameter", "variable-declaration", "property-declaration", - "member-variable-declaration" + "member-variable-declaration", + "object-destructuring", + "array-destructuring" ] } } diff --git a/test/rules/typedef/array-destructuring/test.ts.lint b/test/rules/typedef/array-destructuring/test.ts.lint new file mode 100644 index 00000000000..840f1b72518 --- /dev/null +++ b/test/rules/typedef/array-destructuring/test.ts.lint @@ -0,0 +1,4 @@ +const [ paramA, paramB ] = [15, 'test']; + ~ [expected array-destructuring: '[ paramA, paramB ]' to have a typedef] + +const [ paramA3, paramB3 ]: { number: string, paramB1: string } = [15, 'test']; diff --git a/test/rules/typedef/array-destructuring/tslint.json b/test/rules/typedef/array-destructuring/tslint.json new file mode 100644 index 00000000000..fcc56d55361 --- /dev/null +++ b/test/rules/typedef/array-destructuring/tslint.json @@ -0,0 +1,7 @@ +{ + "rules": { + "typedef": [true, + "array-destructuring" + ] + } +} diff --git a/test/rules/typedef/none/test.ts.lint b/test/rules/typedef/none/test.ts.lint index 641cc56f5fd..3af21a467cb 100644 --- a/test/rules/typedef/none/test.ts.lint +++ b/test/rules/typedef/none/test.ts.lint @@ -77,3 +77,11 @@ try { for (let i of [1, 2, 3]) { } + +const [ paramA, paramB ] = [ 'test', 15 ]; + +const { paramA, paramB } = { paramA: "test", paramB: 15 }; + +const [ paramA3, paramB3 ]: [ number, string ] = [15, 'test']; + +const { paramA, paramB }: { paramA: string, paramB: number } = { paramA: "test", paramB: 15 }; diff --git a/test/rules/typedef/object-destructuring/test.ts.lint b/test/rules/typedef/object-destructuring/test.ts.lint new file mode 100644 index 00000000000..9a89039bcc7 --- /dev/null +++ b/test/rules/typedef/object-destructuring/test.ts.lint @@ -0,0 +1,7 @@ + + +const { paramA, paramB } = { paramA: "test", paramB: 15 }; + ~ [expected object-destructuring: '{ paramA, paramB }' to have a typedef] + + +const { paramA, paramB }: { paramA: string, paramB: number } = { paramA: "test", paramB: 15 }; diff --git a/test/rules/typedef/object-destructuring/tslint.json b/test/rules/typedef/object-destructuring/tslint.json new file mode 100644 index 00000000000..fec26c5acc2 --- /dev/null +++ b/test/rules/typedef/object-destructuring/tslint.json @@ -0,0 +1,7 @@ +{ + "rules": { + "typedef": [true, + "object-destructuring" + ] + } +} diff --git a/test/rules/typedef/variable-declaration/test.ts.lint b/test/rules/typedef/variable-declaration/test.ts.lint new file mode 100644 index 00000000000..c1a30f42398 --- /dev/null +++ b/test/rules/typedef/variable-declaration/test.ts.lint @@ -0,0 +1,6 @@ +var NoTypeObjectLiteralWithPropertyGetter = { } + ~ [expected variable-declaration: 'NoTypeObjectLiteralWithPropertyGetter' to have a typedef] + + +const { paramA, paramB } = { paramA: "test", paramB: 15 }; +const [ paramA, paramB ] = [15, 'test']; diff --git a/test/rules/typedef/variable-declaration/tslint.json b/test/rules/typedef/variable-declaration/tslint.json new file mode 100644 index 00000000000..15593c2829e --- /dev/null +++ b/test/rules/typedef/variable-declaration/tslint.json @@ -0,0 +1,7 @@ +{ + "rules": { + "typedef": [true, + "variable-declaration" + ] + } +} From 2d07e238f063d9a91a2d0a29594009b713670c58 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Sat, 28 Jan 2017 19:19:41 -0800 Subject: [PATCH 079/131] no-unbound-method: Add JSX test (#2115) --- test/rules/no-unbound-method/test.ts.lint | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/rules/no-unbound-method/test.ts.lint b/test/rules/no-unbound-method/test.ts.lint index 8b435583dd4..b23e2bf9213 100644 --- a/test/rules/no-unbound-method/test.ts.lint +++ b/test/rules/no-unbound-method/test.ts.lint @@ -25,4 +25,8 @@ i.foo; ~~~~~ [0] i.bar; + +; + ~~~~~~~~ [0] + [0]: Avoid referencing unbound methods which may cause unintentional scoping of 'this'. From 22e0fd3d13edbdd3c5c76bcee3a24510ae2386bc Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Sun, 29 Jan 2017 14:35:51 -0800 Subject: [PATCH 080/131] Add `match-default-export-name` rule (#2117) --- src/rules/matchDefaultExportNameRule.ts | 68 +++++++++++++++++++ .../match-default-export-name/anonymous.ts | 1 + test/rules/match-default-export-name/named.ts | 1 + .../match-default-export-name/test.ts.lint | 3 + .../match-default-export-name/tslint.json | 8 +++ 5 files changed, 81 insertions(+) create mode 100644 src/rules/matchDefaultExportNameRule.ts create mode 100644 test/rules/match-default-export-name/anonymous.ts create mode 100644 test/rules/match-default-export-name/named.ts create mode 100644 test/rules/match-default-export-name/test.ts.lint create mode 100644 test/rules/match-default-export-name/tslint.json diff --git a/src/rules/matchDefaultExportNameRule.ts b/src/rules/matchDefaultExportNameRule.ts new file mode 100644 index 00000000000..cc64de31359 --- /dev/null +++ b/src/rules/matchDefaultExportNameRule.ts @@ -0,0 +1,68 @@ +/** + * @license + * Copyright 2017 Palantir Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as ts from "typescript"; + +import * as Lint from "../index"; + +export class Rule extends Lint.Rules.TypedRule { + /* tslint:disable:object-literal-sort-keys */ + public static metadata: Lint.IRuleMetadata = { + ruleName: "match-default-export-name", + description: Lint.Utils.dedent` + Requires that a default import have the same name as the declaration it imports. + Does nothing for anonymous default exports.`, + optionsDescription: "Not configurable.", + options: null, + optionExamples: ["true"], + type: "style", + typescriptOnly: true, + }; + /* tslint:enable:object-literal-sort-keys */ + + public static FAILURE_STRING(importName: string, exportName: string): string { + return `Expected import '${importName}' to match the default export '${exportName}'.`; + } + + public applyWithProgram(sourceFile: ts.SourceFile, langSvc: ts.LanguageService): Lint.RuleFailure[] { + return this.applyWithWalker(new Walker(sourceFile, this.getOptions(), langSvc.getProgram())); + } +} + +class Walker extends Lint.ProgramAwareRuleWalker { + public visitSourceFile(node: ts.SourceFile) { + for (const statement of node.statements) { + if (statement.kind !== ts.SyntaxKind.ImportDeclaration) { + continue; + } + + const { importClause } = statement as ts.ImportDeclaration; + if (importClause && importClause.name) { + this.checkDefaultImport(importClause.name); + } + } + } + + private checkDefaultImport(defaultImport: ts.Identifier) { + const { declarations } = this.getTypeChecker().getAliasedSymbol( + this.getTypeChecker().getSymbolAtLocation(defaultImport)); + const name = declarations && declarations[0] && declarations[0].name; + if (name && name.kind === ts.SyntaxKind.Identifier && defaultImport.text !== name.text) { + this.addFailureAtNode(defaultImport, Rule.FAILURE_STRING(defaultImport.text, name.text)); + } + } +} diff --git a/test/rules/match-default-export-name/anonymous.ts b/test/rules/match-default-export-name/anonymous.ts new file mode 100644 index 00000000000..7f810d3f328 --- /dev/null +++ b/test/rules/match-default-export-name/anonymous.ts @@ -0,0 +1 @@ +export default 0; diff --git a/test/rules/match-default-export-name/named.ts b/test/rules/match-default-export-name/named.ts new file mode 100644 index 00000000000..c33f5633d79 --- /dev/null +++ b/test/rules/match-default-export-name/named.ts @@ -0,0 +1 @@ +export default function a() {} diff --git a/test/rules/match-default-export-name/test.ts.lint b/test/rules/match-default-export-name/test.ts.lint new file mode 100644 index 00000000000..e2db8e270bb --- /dev/null +++ b/test/rules/match-default-export-name/test.ts.lint @@ -0,0 +1,3 @@ +import b from "./named"; + ~ [Expected import 'b' to match the default export 'a'.] +import anyName from "./anonymous"; diff --git a/test/rules/match-default-export-name/tslint.json b/test/rules/match-default-export-name/tslint.json new file mode 100644 index 00000000000..4a9afbf9587 --- /dev/null +++ b/test/rules/match-default-export-name/tslint.json @@ -0,0 +1,8 @@ +{ + "linterOptions": { + "typeCheck": true + }, + "rules": { + "match-default-export-name": true + } +} From 3b0ba051be4725c8bad9a5f4dee14d720fda8ad9 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Sun, 29 Jan 2017 14:46:36 -0800 Subject: [PATCH 081/131] Add `no-duplicate-super` (#2038) --- src/language/utils.ts | 14 +- src/rules/noDuplicateSuperRule.ts | 191 +++++++++++++++ test/rules/no-duplicate-super/test.js.lint | 8 + test/rules/no-duplicate-super/test.ts.lint | 263 +++++++++++++++++++++ test/rules/no-duplicate-super/tslint.json | 8 + 5 files changed, 479 insertions(+), 5 deletions(-) create mode 100644 src/rules/noDuplicateSuperRule.ts create mode 100644 test/rules/no-duplicate-super/test.js.lint create mode 100644 test/rules/no-duplicate-super/test.ts.lint create mode 100644 test/rules/no-duplicate-super/tslint.json diff --git a/src/language/utils.ts b/src/language/utils.ts index a31b93159c7..8baf0d1ce24 100644 --- a/src/language/utils.ts +++ b/src/language/utils.ts @@ -230,11 +230,7 @@ export function isScopeBoundary(node: ts.Node): boolean { export function isBlockScopeBoundary(node: ts.Node): boolean { return isScopeBoundary(node) || node.kind === ts.SyntaxKind.Block - || node.kind === ts.SyntaxKind.DoStatement - || node.kind === ts.SyntaxKind.WhileStatement - || node.kind === ts.SyntaxKind.ForStatement - || node.kind === ts.SyntaxKind.ForInStatement - || node.kind === ts.SyntaxKind.ForOfStatement + || isLoop(node) || node.kind === ts.SyntaxKind.WithStatement || node.kind === ts.SyntaxKind.SwitchStatement || node.parent !== undefined @@ -242,6 +238,14 @@ export function isBlockScopeBoundary(node: ts.Node): boolean { || node.parent.kind === ts.SyntaxKind.IfStatement); } +export function isLoop(node: ts.Node): node is ts.IterationStatement { + return node.kind === ts.SyntaxKind.DoStatement + || node.kind === ts.SyntaxKind.WhileStatement + || node.kind === ts.SyntaxKind.ForStatement + || node.kind === ts.SyntaxKind.ForInStatement + || node.kind === ts.SyntaxKind.ForOfStatement; +} + export interface TokenPosition { /** The start of the token including all trivia before it */ fullStart: number; diff --git a/src/rules/noDuplicateSuperRule.ts b/src/rules/noDuplicateSuperRule.ts new file mode 100644 index 00000000000..e57a6f160e2 --- /dev/null +++ b/src/rules/noDuplicateSuperRule.ts @@ -0,0 +1,191 @@ +/** + * @license + * Copyright 2017 Palantir Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as ts from "typescript"; + +import * as Lint from "../index"; + +export class Rule extends Lint.Rules.AbstractRule { + /* tslint:disable:object-literal-sort-keys */ + public static metadata: Lint.IRuleMetadata = { + ruleName: "no-duplicate-super", + description: "Warns if 'super()' appears twice in a constructor.", + rationale: "The second call to 'super()' will fail at runtime.", + optionsDescription: "Not configurable.", + options: null, + optionExamples: ["true"], + type: "functionality", + typescriptOnly: false, + }; + /* tslint:enable:object-literal-sort-keys */ + + public static FAILURE_STRING_DUPLICATE = "Multiple calls to 'super()' found. It must be called only once."; + public static FAILURE_STRING_LOOP = "'super()' called in a loop. It must be called only once."; + + public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { + return this.applyWithWalker(new Walker(sourceFile, this.getOptions())); + } +} + +class Walker extends Lint.RuleWalker { + /** Whether we've seen 'super()' yet in the current constructor. */ + public visitConstructorDeclaration(node: ts.ConstructorDeclaration) { + if (!node.body) { + return; + } + + this.getSuperForNode(node.body); + super.visitConstructorDeclaration(node); + } + + private getSuperForNode(node: ts.Node): Super { + if (Lint.isLoop(node)) { + const bodySuper = this.combineSequentialChildren(node); + if (typeof bodySuper === "number") { + return Kind.NoSuper; + } + if (!bodySuper.break) { + this.addFailureAtNode(bodySuper.node, Rule.FAILURE_STRING_LOOP); + } + return { ...bodySuper, break: false }; + } + + switch (node.kind) { + case ts.SyntaxKind.ReturnStatement: + case ts.SyntaxKind.ThrowStatement: + return Kind.Return; + + case ts.SyntaxKind.BreakStatement: + return Kind.Break; + + case ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.ClassExpression: + // 'super()' is bound differently inside, so ignore. + return Kind.NoSuper; + + case ts.SyntaxKind.SuperKeyword: + return node.parent!.kind === ts.SyntaxKind.CallExpression && (node.parent as ts.CallExpression).expression === node + ? { node: node.parent! as ts.CallExpression, break: false } + : Kind.NoSuper; + + case ts.SyntaxKind.IfStatement: { + const { thenStatement, elseStatement } = node as ts.IfStatement; + return worse(this.getSuperForNode(thenStatement), elseStatement ? this.getSuperForNode(elseStatement) : Kind.NoSuper); + } + + case ts.SyntaxKind.SwitchStatement: + return this.getSuperForSwitch(node as ts.SwitchStatement); + + default: + return this.combineSequentialChildren(node); + } + } + + private getSuperForSwitch(node: ts.SwitchStatement): Super { + // 'super()' from any clause. Used to track whether 'super()' happens in the switch at all. + let foundSingle: ts.CallExpression | undefined; + // 'super()' from the previous clause if it did not 'break;'. + let fallthroughSingle: ts.CallExpression | undefined; + for (const clause of node.caseBlock.clauses) { + const clauseSuper = this.combineSequentialChildren(clause); + switch (clauseSuper) { + case Kind.NoSuper: + break; + + case Kind.Break: + fallthroughSingle = undefined; + break; + + case Kind.Return: + return Kind.NoSuper; + + default: + if (fallthroughSingle) { + this.addDuplicateFailure(fallthroughSingle, clauseSuper.node); + } + if (!clauseSuper.break) { + fallthroughSingle = clauseSuper.node; + } + foundSingle = clauseSuper.node; + break; + } + } + + return foundSingle ? { node: foundSingle, break: false } : Kind.NoSuper; + } + + /** + * Combines children that come one after another. + * (As opposed to if/else, switch, or loops, which need their own handling.) + */ + private combineSequentialChildren(node: ts.Node): Super { + let seenSingle: Single | undefined = undefined; + const res = ts.forEachChild(node, (child) => { + const childSuper = this.getSuperForNode(child); + switch (childSuper) { + case Kind.NoSuper: + return; + + case Kind.Break: + if (seenSingle) { + return { ...seenSingle, break: true }; + } + return childSuper; + + case Kind.Return: + return childSuper; + + default: + if (seenSingle && !seenSingle.break) { + this.addDuplicateFailure(seenSingle.node, childSuper.node); + } + seenSingle = childSuper; + return; + } + }); + return res || seenSingle || Kind.NoSuper; + } + + private addDuplicateFailure(a: ts.Node, b: ts.Node) { + this.addFailureFromStartToEnd(a.getStart(), b.end, Rule.FAILURE_STRING_DUPLICATE); + } +} + +/** Kind of 'super()' use in a node. */ +type Super = Kind | Single; +const enum Kind { + /** 'super()' never called. */ + NoSuper, + /** This node returns. It doesn't matter whether 'super()' was called in it. */ + Return, + /** This node breaks, and doesn't have 'super()'. */ + Break, +}; +/** Represents a single 'super()' call. */ +interface Single { + /** Node of the 'super()' call. */ + node: ts.CallExpression; + /** Whether it is followed by 'break;'. */ + break: boolean; +} + +// If/else run separately, so return the branch more likely to result in eventual errors. +function worse(a: Super, b: Super): Super { + return typeof a === "number" + ? typeof b === "number" ? (a < b ? b : a) : b + : typeof b === "number" ? a : a.break ? b : a; +} diff --git a/test/rules/no-duplicate-super/test.js.lint b/test/rules/no-duplicate-super/test.js.lint new file mode 100644 index 00000000000..f82ff763aaa --- /dev/null +++ b/test/rules/no-duplicate-super/test.js.lint @@ -0,0 +1,8 @@ +class { + constructor() { + super(); + ~~~~~~~~ + super(); +~~~~~~~~~~~~~~~ [Multiple calls to 'super()' found. It must be called only once.] + } +} diff --git a/test/rules/no-duplicate-super/test.ts.lint b/test/rules/no-duplicate-super/test.ts.lint new file mode 100644 index 00000000000..70d9eeeefc6 --- /dev/null +++ b/test/rules/no-duplicate-super/test.ts.lint @@ -0,0 +1,263 @@ +declare const b: boolean; +declare const b2: boolean; +declare const n: number; + +// Simple +{ + class { + constructor() { + super(); + ~~~~~~~~ + super(); +~~~~~~~~~~~~~~~~~~~ [0] + } + } + + class { + constructor() { + super(); + super.foo(); + } + } + + class { + constructor() { + super(); + + class C2 { + constructor() { + super(); + } + } + } + } + + class { + constructor() { + super(super()); + ~~~~~~~~~~~~~ [0] + } + } +} + +// If/else +{ + class { + constructor() { + if (b) { + super(); + return; + } + super(); + } + } + + class { + constructor() { + if (b) { + super(); + } else { + super(); + } + } + } + + class { + constructor() { + if (b) { + super(); + return; + } else if (b2) { + super(); + ~~~~~~~~ + } +~~~~~~~~~~~~~ + super(); +~~~~~~~~~~~~~~~~~~~ [0] + } + } + + class { + constructor() { + if (b) { + super(); + return; + } else if (b) { + super(); + return; + } + super(); + } + } +} + +// Loop +{ + class { + constructor() { + while (b) { + if (b) { + super(); + break; + } + } + } + } + + class { + constructor() { + while (b) { + if (b) { + super(); + break; + } + if (b2) { + super(); + break; + } + super(); + break; + } + } + } + + class { + constructor() { + while (b) { + if (b) { + super(); + return; + } else { + super(); + return; + } + } + super(); + } + } + + class { + constructor() { + while (b) { + if (b) { + super(); + return; + } else { + super(); + ~~~~~~~ + break; +~~~~~~~~~~~~~~~~~~~~~~~~~ + } +~~~~~~~~~~~~~~~~~ + } +~~~~~~~~~~~~~ + super(); +~~~~~~~~~~~~~~~~~~~ [0] + } + } + + class { + constructor() { + while (b) { + if (b) { + super(); + ~~~~~~~ + break; +~~~~~~~~~~~~~~~~~~~~~~~~~~ + } else { +~~~~~~~~~~~~~~~~~~~~~~~~ + super(); +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + return; +~~~~~~~~~~~~~~~~~~~~~~~~~ + } +~~~~~~~~~~~~~~~~~ + } +~~~~~~~~~~~~~ + super(); +~~~~~~~~~~~~~~~~~~~ [0] + } + } +} + +// Switch +{ + class { + constructor() { + switch (n) { + case 0: + super(); + ~~~~~~~~ + case 1: +~~~~~~~~~~~~~~~~~~~~~~~ + super(); +~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + } + } + } + + class { + constructor() { + switch (n) { + case 0: + super(); + break; + case 1: + super(); + break; + } + } + } + + class { + constructor() { + switch (n) { + case 0: + super(); + break; + case 1: + super(); + ~~~~~~~~ + break; +~~~~~~~~~~~~~~~~~~~~~~~~~~ + } +~~~~~~~~~~~~~ + super(); +~~~~~~~~~~~~~~~~~~~ [0] + } + } +} + +// Wierd +{ + class { + constructor() { + if (b) { + super(); + { + return; + } + } + super(); + } + } + + class { + constructor() { + switch (n) { + case 0: + if (b) { + super(); + return; + } else { + super(); + if (b2) return; else return; + } + } + super(); + } + } +} + +[0]: Multiple calls to 'super()' found. It must be called only once. +[1]: 'super()' called in a loop. It must be called only once. diff --git a/test/rules/no-duplicate-super/tslint.json b/test/rules/no-duplicate-super/tslint.json new file mode 100644 index 00000000000..ea3a1e95df6 --- /dev/null +++ b/test/rules/no-duplicate-super/tslint.json @@ -0,0 +1,8 @@ +{ + "rules": { + "no-duplicate-super": true + }, + "jsRules": { + "no-duplicate-super": true + } +} From 9f8fd27d4aa67425891eae2722189c4f280307f1 Mon Sep 17 00:00:00 2001 From: Noah Chen Date: Sun, 29 Jan 2017 18:34:14 -0500 Subject: [PATCH 082/131] Fix build (#2152) Add test to check that `.ts` files in `/test/rules` end with `test.ts` --- src/rules/matchDefaultExportNameRule.ts | 1 + src/rules/noDuplicateSuperRule.ts | 2 +- test/formatters/codeFrameFormatterTests.ts | 1 + test/ruleLoaderTests.ts | 39 ++++++++++++++----- .../{anonymous.ts => anonymous.test.ts} | 0 .../{named.ts => named.test.ts} | 0 .../match-default-export-name/test.ts.lint | 4 +- .../type-checked/{a.ts => a.test.ts} | 0 .../type-checked/test.ts.lint | 2 +- 9 files changed, 35 insertions(+), 14 deletions(-) rename test/rules/match-default-export-name/{anonymous.ts => anonymous.test.ts} (100%) rename test/rules/match-default-export-name/{named.ts => named.test.ts} (100%) rename test/rules/no-unused-variable/type-checked/{a.ts => a.test.ts} (100%) diff --git a/src/rules/matchDefaultExportNameRule.ts b/src/rules/matchDefaultExportNameRule.ts index cc64de31359..1c49fd9b8bb 100644 --- a/src/rules/matchDefaultExportNameRule.ts +++ b/src/rules/matchDefaultExportNameRule.ts @@ -31,6 +31,7 @@ export class Rule extends Lint.Rules.TypedRule { optionExamples: ["true"], type: "style", typescriptOnly: true, + requiresTypeInfo: true, }; /* tslint:enable:object-literal-sort-keys */ diff --git a/src/rules/noDuplicateSuperRule.ts b/src/rules/noDuplicateSuperRule.ts index e57a6f160e2..eea04853f9f 100644 --- a/src/rules/noDuplicateSuperRule.ts +++ b/src/rules/noDuplicateSuperRule.ts @@ -133,7 +133,7 @@ class Walker extends Lint.RuleWalker { * (As opposed to if/else, switch, or loops, which need their own handling.) */ private combineSequentialChildren(node: ts.Node): Super { - let seenSingle: Single | undefined = undefined; + let seenSingle: Single | undefined; const res = ts.forEachChild(node, (child) => { const childSuper = this.getSuperForNode(child); switch (childSuper) { diff --git a/test/formatters/codeFrameFormatterTests.ts b/test/formatters/codeFrameFormatterTests.ts index 2884a3d3cd7..fb2e6820244 100644 --- a/test/formatters/codeFrameFormatterTests.ts +++ b/test/formatters/codeFrameFormatterTests.ts @@ -26,6 +26,7 @@ describe("CodeFrame Formatter", () => { let formatter: IFormatter; before(() => { + (colors as any).enabled = true; const Formatter = TestUtils.getFormatter("codeFrame"); sourceFile = TestUtils.getSourceFile(TEST_FILE); formatter = new Formatter(); diff --git a/test/ruleLoaderTests.ts b/test/ruleLoaderTests.ts index b0270118f80..2c5eac97e96 100644 --- a/test/ruleLoaderTests.ts +++ b/test/ruleLoaderTests.ts @@ -22,7 +22,9 @@ import { camelize } from "../src/utils"; import { loadRules } from "./lint"; describe("Rule Loader", () => { - const RULES_DIRECTORY = "build/src/rules"; + const builtRulesDir = "build/src/rules"; + const srcRulesDir = "src/rules"; + const testRulesDir = "test/rules"; it("loads core rules", () => { const validConfiguration: {[name: string]: any} = { @@ -33,7 +35,7 @@ describe("Rule Loader", () => { "quotemark": "single", }; - const rules = loadRules(validConfiguration, {}, RULES_DIRECTORY); + const rules = loadRules(validConfiguration, {}, builtRulesDir); assert.equal(rules.length, 5); }); @@ -44,7 +46,7 @@ describe("Rule Loader", () => { "invalidConfig2": false, }; - const rules = loadRules(invalidConfiguration, {}, [RULES_DIRECTORY]); + const rules = loadRules(invalidConfiguration, {}, [builtRulesDir]); assert.equal(rules.length, 1); }); @@ -57,7 +59,7 @@ describe("Rule Loader", () => { "quotemark": "single", }; - const rules = loadRules(validConfiguration, {}, [RULES_DIRECTORY]); + const rules = loadRules(validConfiguration, {}, [builtRulesDir]); assert.equal(rules.length, 5); }); @@ -66,20 +68,18 @@ describe("Rule Loader", () => { "class-name": true, }; - const rules = loadRules(validConfiguration, {}, RULES_DIRECTORY, true); + const rules = loadRules(validConfiguration, {}, builtRulesDir, true); assert.equal(rules.length, 1); }); it("tests every rule", () => { - const rulesDir = "src/rules"; - const testsDir = "test/rules"; - const rules = fs.readdirSync(rulesDir) + const rules = fs.readdirSync(srcRulesDir) .filter((file) => /Rule.ts$/.test(file)) .map((file) => file.substr(0, file.length - "Rule.ts".length)) .sort() .join("\n"); - const tests = fs.readdirSync(testsDir) - .filter((file) => !file.startsWith("_") && fs.statSync(path.join(testsDir, file)).isDirectory()) + const tests = fs.readdirSync(testRulesDir) + .filter((file) => !file.startsWith("_") && fs.statSync(path.join(testRulesDir, file)).isDirectory()) .map(camelize) .sort() .join("\n"); @@ -97,4 +97,23 @@ describe("Rule Loader", () => { assert.isFalse(testFailed, "List of rules doesn't match list of tests"); }); + + it("ensures that `.ts` files in `rules/` end in `.test.ts` to avoid being linted", () => { + walkSync(testRulesDir, (filename) => { + if (/\.ts$/.test(filename)) { + assert.match(filename, /\.test\.ts$/); + } + }); + }); + + const walkSync = (dir: string, cb: (filename: string) => void) => { + fs.readdirSync(dir).forEach((file) => { + const fullPath = path.join(dir, file); + if (fs.statSync(path.join(dir, file)).isDirectory()) { + walkSync(fullPath, cb); + } else { + cb(fullPath); + } + }); + }; }); diff --git a/test/rules/match-default-export-name/anonymous.ts b/test/rules/match-default-export-name/anonymous.test.ts similarity index 100% rename from test/rules/match-default-export-name/anonymous.ts rename to test/rules/match-default-export-name/anonymous.test.ts diff --git a/test/rules/match-default-export-name/named.ts b/test/rules/match-default-export-name/named.test.ts similarity index 100% rename from test/rules/match-default-export-name/named.ts rename to test/rules/match-default-export-name/named.test.ts diff --git a/test/rules/match-default-export-name/test.ts.lint b/test/rules/match-default-export-name/test.ts.lint index e2db8e270bb..5fdb33083bd 100644 --- a/test/rules/match-default-export-name/test.ts.lint +++ b/test/rules/match-default-export-name/test.ts.lint @@ -1,3 +1,3 @@ -import b from "./named"; +import b from "./named.test"; ~ [Expected import 'b' to match the default export 'a'.] -import anyName from "./anonymous"; +import anyName from "./anonymous.test"; diff --git a/test/rules/no-unused-variable/type-checked/a.ts b/test/rules/no-unused-variable/type-checked/a.test.ts similarity index 100% rename from test/rules/no-unused-variable/type-checked/a.ts rename to test/rules/no-unused-variable/type-checked/a.test.ts diff --git a/test/rules/no-unused-variable/type-checked/test.ts.lint b/test/rules/no-unused-variable/type-checked/test.ts.lint index de64a5953ac..23392d115a2 100644 --- a/test/rules/no-unused-variable/type-checked/test.ts.lint +++ b/test/rules/no-unused-variable/type-checked/test.ts.lint @@ -1,4 +1,4 @@ -import {a, A} from './a'; +import {a, A} from './a.test'; export class B { static thing = a; From d7c2bde0d25258e47e3e5a957600e08523168a9f Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Mon, 30 Jan 2017 14:37:13 -0800 Subject: [PATCH 083/131] member-ordering: Add 'alphabetize' option (#2101) --- src/rules/memberOrderingRule.ts | 83 ++++++++++++++++--- .../member-ordering/alphabetize/test.ts.lint | 25 ++++++ .../member-ordering/alphabetize/tslint.json | 8 ++ 3 files changed, 106 insertions(+), 10 deletions(-) create mode 100644 test/rules/member-ordering/alphabetize/test.ts.lint create mode 100644 test/rules/member-ordering/alphabetize/tslint.json diff --git a/src/rules/memberOrderingRule.ts b/src/rules/memberOrderingRule.ts index 834dac52d70..b09998fb2f2 100644 --- a/src/rules/memberOrderingRule.ts +++ b/src/rules/memberOrderingRule.ts @@ -19,6 +19,7 @@ import * as ts from "typescript"; import * as Lint from "../index"; const OPTION_ORDER = "order"; +const OPTION_ALPHABETIZE = "alphabetize"; enum MemberKind { publicStaticField, @@ -118,7 +119,9 @@ const optionsDescription = Lint.Utils.dedent` "public-static-method", "protected-static-method" ] - }`; + } + + The '${OPTION_ALPHABETIZE}' option will enforce that members within the same category should be alphabetically sorted by name.`; export class Rule extends Lint.Rules.AbstractRule { /* tslint:disable:object-literal-sort-keys */ @@ -178,6 +181,14 @@ export class Rule extends Lint.Rules.AbstractRule { type: "typescript", typescriptOnly: true, }; + + public static FAILURE_STRING_ALPHABETIZE(prevName: string, curName: string) { + return `${show(curName)} should come alphabetically before ${show(prevName)}`; + function show(s: string) { + return s === "" ? "Computed property" : `'${s}'`; + } + } + /* tslint:enable:object-literal-sort-keys */ public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { return this.applyWithWalker(new MemberOrderingWalker(sourceFile, this.getOptions())); @@ -185,11 +196,11 @@ export class Rule extends Lint.Rules.AbstractRule { } export class MemberOrderingWalker extends Lint.RuleWalker { - private readonly order: MemberCategory[]; + private readonly opts: Options; constructor(sourceFile: ts.SourceFile, options: Lint.IOptions) { super(sourceFile, options); - this.order = getOrder(this.getOptions()); + this.opts = parseOptions(this.getOptions()); } public visitClassDeclaration(node: ts.ClassDeclaration) { @@ -214,6 +225,7 @@ export class MemberOrderingWalker extends Lint.RuleWalker { private visitMembers(members: Member[]) { let prevRank = -1; + let prevName: string | undefined; for (const member of members) { const rank = this.memberRank(member); if (rank === -1) { @@ -232,12 +244,41 @@ export class MemberOrderingWalker extends Lint.RuleWalker { `Instead, this should come ${locationHint}.`; this.addFailureAtNode(member, errorLine1); } else { + if (this.opts.alphabetize && member.name) { + if (rank !== prevRank) { + // No alphabetical ordering between different ranks + prevName = undefined; + } + + const curName = nameString(member.name); + if (prevName !== undefined && caseInsensitiveLess(curName, prevName)) { + this.addFailureAtNode(member.name, + Rule.FAILURE_STRING_ALPHABETIZE(this.findLowerName(members, rank, curName), curName)); + } else { + prevName = curName; + } + } + // keep track of last good node prevRank = rank; } } } + /** Finds the lowest name higher than 'targetName'. */ + private findLowerName(members: Member[], targetRank: Rank, targetName: string): string { + for (const member of members) { + if (!member.name || this.memberRank(member) !== targetRank) { + continue; + } + const name = nameString(member.name); + if (caseInsensitiveLess(targetName, name)) { + return name; + } + } + throw new Error("Expected to find a name"); + } + /** Finds the highest existing rank lower than `targetRank`. */ private findLowerRank(members: Member[], targetRank: Rank): Rank | -1 { let max: Rank | -1 = -1; @@ -255,14 +296,18 @@ export class MemberOrderingWalker extends Lint.RuleWalker { if (optionName === undefined) { return -1; } - return this.order.findIndex((category) => category.has(optionName)); + return this.opts.order.findIndex((category) => category.has(optionName)); } private rankName(rank: Rank): string { - return this.order[rank].name; + return this.opts.order[rank].name; } } +function caseInsensitiveLess(a: string, b: string) { + return a.toLowerCase() < b.toLowerCase(); +} + function memberKindForConstructor(access: Access): MemberKind { return (MemberKind as any)[access + "Constructor"]; } @@ -329,12 +374,19 @@ type Rank = number; type Access = "public" | "protected" | "private"; -function getOrder(options: any[]): MemberCategory[] { - return getOrderJson(options).map((cat) => typeof cat === "string" +interface Options { + order: MemberCategory[]; + alphabetize: boolean; +} + +function parseOptions(options: any[]): Options { + const { order: orderJson, alphabetize } = getOptionsJson(options); + const order = orderJson.map((cat) => typeof cat === "string" ? new MemberCategory(cat.replace(/-/g, " "), new Set(memberKindFromName(cat))) : new MemberCategory(cat.name, new Set(flatMap(cat.kinds, memberKindFromName)))); + return { order, alphabetize }; } -function getOrderJson(allOptions: any[]): MemberCategoryJson[] { +function getOptionsJson(allOptions: any[]): { order: MemberCategoryJson[], alphabetize: boolean } { if (allOptions == null || allOptions.length === 0 || allOptions[0] == null) { throw new Error("Got empty options"); } @@ -342,10 +394,10 @@ function getOrderJson(allOptions: any[]): MemberCategoryJson[] { const firstOption = allOptions[0]; if (typeof firstOption !== "object") { // Undocumented direct string option. Deprecate eventually. - return convertFromOldStyleOptions(allOptions); // presume it to be string[] + return { order: convertFromOldStyleOptions(allOptions), alphabetize: false }; // presume allOptions to be string[] } - return categoryFromOption(firstOption[OPTION_ORDER]); + return { order: categoryFromOption(firstOption[OPTION_ORDER]), alphabetize: !!firstOption[OPTION_ALPHABETIZE] }; } function categoryFromOption(orderOption: {}): MemberCategoryJson[] { if (Array.isArray(orderOption)) { @@ -416,6 +468,17 @@ function isFunctionLiteral(node: ts.Node | undefined) { } } +function nameString(name: ts.PropertyName): string { + switch (name.kind) { + case ts.SyntaxKind.Identifier: + case ts.SyntaxKind.StringLiteral: + case ts.SyntaxKind.NumericLiteral: + return (name as ts.Identifier | ts.LiteralExpression).text; + default: + return ""; + } +} + function mapDefined(inputs: T[], getOutput: (input: T) => U | undefined): U[] { const out = []; for (const input of inputs) { diff --git a/test/rules/member-ordering/alphabetize/test.ts.lint b/test/rules/member-ordering/alphabetize/test.ts.lint new file mode 100644 index 00000000000..126b0fe9cc4 --- /dev/null +++ b/test/rules/member-ordering/alphabetize/test.ts.lint @@ -0,0 +1,25 @@ +class X { + // Different categories have alphabetization together. + bar: number; + foo: string; + + [Symbol.iterator]() {} + // No ordering among computed properties + [Symbol.alpherator]() {} + + 0() {} + 2() {} + 1() {} + ~ ['1' should come alphabetically before '2'] + + foo() {} + "goo"() {} + Hoo() {} + ioo() {} + bar() {} + ~~~ ['bar' should come alphabetically before 'foo'] + + // Computed properties must go at the beginning. + [Symbol.zeterator]() {} + ~~~~~~~~~~~~~~~~~~ [Computed property should come alphabetically before '0'] +} diff --git a/test/rules/member-ordering/alphabetize/tslint.json b/test/rules/member-ordering/alphabetize/tslint.json new file mode 100644 index 00000000000..d63ae1f4057 --- /dev/null +++ b/test/rules/member-ordering/alphabetize/tslint.json @@ -0,0 +1,8 @@ +{ + "rules": { + "member-ordering": [true, { + "order": "fields-first", + "alphabetize": true + }] + } +} From 547f2fe5c10421ac0685a7419a8fd1e44360b0c4 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Mon, 30 Jan 2017 14:52:56 -0800 Subject: [PATCH 084/131] Rewrite 'no-mergeable-namespace' to use AST walking instead of document highlights (#2105) --- src/rules/noMergeableNamespaceRule.ts | 73 +++++++++++-------- .../rules/no-mergeable-namespace/test.ts.lint | 29 ++++++-- 2 files changed, 68 insertions(+), 34 deletions(-) diff --git a/src/rules/noMergeableNamespaceRule.ts b/src/rules/noMergeableNamespaceRule.ts index 3db30c14518..09bdbb362a9 100644 --- a/src/rules/noMergeableNamespaceRule.ts +++ b/src/rules/noMergeableNamespaceRule.ts @@ -32,48 +32,63 @@ export class Rule extends Lint.Rules.AbstractRule { }; /* tslint:enable:object-literal-sort-keys */ - public static failureStringFactory(identifier: string, locationToMerge: ts.LineAndCharacter): string { - return `Mergeable namespace ${identifier} found. Merge its contents with the namespace on line ${locationToMerge.line}.`; + public static failureStringFactory(name: string, seenBeforeLine: number) { + return `Mergeable namespace '${name}' found. Merge its contents with the namespace on line ${seenBeforeLine}.`; } - public apply(sourceFile: ts.SourceFile, languageService: ts.LanguageService): Lint.RuleFailure[] { - const noMergeableNamespaceWalker = new NoMergeableNamespaceWalker(sourceFile, this.getOptions(), languageService); - return this.applyWithWalker(noMergeableNamespaceWalker); + public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { + return this.applyWithWalker(new Walker(sourceFile, this.getOptions())); } } -class NoMergeableNamespaceWalker extends Lint.RuleWalker { - constructor(sourceFile: ts.SourceFile, options: Lint.IOptions, private languageService: ts.LanguageService) { - super(sourceFile, options); +class Walker extends Lint.RuleWalker { + public visitSourceFile(node: ts.SourceFile) { + this.checkStatements(node.statements); + // All tree-walking handled by 'checkStatements' } - public visitModuleDeclaration(node: ts.ModuleDeclaration) { - if (Lint.isNodeFlagSet(node, ts.NodeFlags.Namespace) - && node.name.kind === ts.SyntaxKind.Identifier) { - this.validateReferencesForNamespace((node.name as ts.Identifier).text, node.name.getStart()); - } - super.visitModuleDeclaration(node); - } + private checkStatements(statements: ts.Statement[]): void { + const seen = new Map(); - private validateReferencesForNamespace(name: string, position: number) { - const { fileName } = this.getSourceFile(); - const highlights = this.languageService.getDocumentHighlights(fileName, position, [fileName]); + for (const statement of statements) { + if (statement.kind !== ts.SyntaxKind.ModuleDeclaration) { + continue; + } + + const { name } = statement as ts.ModuleDeclaration; + if (name.kind === ts.SyntaxKind.Identifier) { + const { text } = name; + const prev = seen.get(text); + if (prev) { + this.addFailureAtNode(name, Rule.failureStringFactory(text, this.getLineOfNode(prev))); + } + seen.set(text, statement as ts.NamespaceDeclaration); + } - if (highlights == null || highlights[0].highlightSpans.length > 1) { - const failureString = Rule.failureStringFactory(name, this.findLocationToMerge(position, highlights[0].highlightSpans)); - this.addFailureAt(position, name.length, failureString); + // Recursively check in all module declarations + this.checkModuleDeclaration(statement as ts.ModuleDeclaration); } } - private findLocationToMerge(currentPosition: number, highlightSpans: ts.HighlightSpan[]): ts.LineAndCharacter { - const { line } = this.getLineAndCharacterOfPosition(currentPosition); + private checkModuleDeclaration(decl: ts.ModuleDeclaration): void { + const { body } = decl; + if (!body) { + return; + } - for (const span of highlightSpans) { - const lineAndCharacter = this.getLineAndCharacterOfPosition(span.textSpan.start); - if (lineAndCharacter.line !== line) { - return lineAndCharacter; - } + switch (body.kind) { + case ts.SyntaxKind.ModuleBlock: + this.checkStatements(body.statements); + break; + case ts.SyntaxKind.ModuleDeclaration: + this.checkModuleDeclaration(body as ts.ModuleDeclaration); + break; + default: + break; } - throw new Error("expected more than one highlightSpan"); + } + + private getLineOfNode(node: ts.Node): number { + return this.getLineAndCharacterOfPosition(node.pos).line; } } diff --git a/test/rules/no-mergeable-namespace/test.ts.lint b/test/rules/no-mergeable-namespace/test.ts.lint index 4f880c9ce5c..9b87fa17946 100644 --- a/test/rules/no-mergeable-namespace/test.ts.lint +++ b/test/rules/no-mergeable-namespace/test.ts.lint @@ -8,15 +8,34 @@ module bar {} // invalid case namespace a {} - ~ [Mergeable namespace a found. Merge its contents with the namespace on line 10.] namespace a {} - ~ [Mergeable namespace a found. Merge its contents with the namespace on line 9.] + ~ [Mergeable namespace 'a' found. Merge its contents with the namespace on line 6.] namespace b { namespace b {} - ~ [Mergeable namespace b found. Merge its contents with the namespace on line 14.] namespace b {} - ~ [Mergeable namespace b found. Merge its contents with the namespace on line 13.] + ~ [Mergeable namespace 'b' found. Merge its contents with the namespace on line 12.] namespace b {} - ~ [Mergeable namespace b found. Merge its contents with the namespace on line 13.] + ~ [Mergeable namespace 'b' found. Merge its contents with the namespace on line 13.] } + +namespace x.y {} +namespace x { namespace y{} } + ~ [Mergeable namespace 'x' found. Merge its contents with the namespace on line 16.] + +namespace m.n { + namespace l {} + namespace l {} + ~ [Mergeable namespace 'l' found. Merge its contents with the namespace on line 21.] +} + +module "foo" { + // Different than outer 'a' + namespace a {} + + namespace q {} + namespace q {} + ~ [Mergeable namespace 'q' found. Merge its contents with the namespace on line 28.] +} + +[0]: This namespace has already been declared. Merge the declarations. From 6395aeaee1d8431797193254a724c84b939b99c4 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Mon, 30 Jan 2017 18:03:35 -0800 Subject: [PATCH 085/131] object-literal-key-quotes: No need to quote a float if its .toString() is the same. (#2144) --- src/rules/objectLiteralKeyQuotesRule.ts | 3 +-- test/rules/object-literal-key-quotes/as-needed/test.ts.fix | 1 + test/rules/object-literal-key-quotes/as-needed/test.ts.lint | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/rules/objectLiteralKeyQuotesRule.ts b/src/rules/objectLiteralKeyQuotesRule.ts index c13491834a8..9be27c2a570 100644 --- a/src/rules/objectLiteralKeyQuotesRule.ts +++ b/src/rules/objectLiteralKeyQuotesRule.ts @@ -155,9 +155,8 @@ function quotesAreInconsistent(properties: ts.ObjectLiteralElementLike[]): boole } function propertyNeedsQuotes(property: string): boolean { - return !(IDENTIFIER_NAME_REGEX.test(property) || NUMBER_REGEX.test(property) && Number(property).toString() === property); + return !IDENTIFIER_NAME_REGEX.test(property) && Number(property).toString() !== property; } // This is simplistic. See https://mothereff.in/js-properties for the gorey details. const IDENTIFIER_NAME_REGEX = /^(?:[\$A-Z_a-z])+$/; -const NUMBER_REGEX = /^[0-9]+$/; diff --git a/test/rules/object-literal-key-quotes/as-needed/test.ts.fix b/test/rules/object-literal-key-quotes/as-needed/test.ts.fix index 97192e27cff..9dd05ac5201 100644 --- a/test/rules/object-literal-key-quotes/as-needed/test.ts.fix +++ b/test/rules/object-literal-key-quotes/as-needed/test.ts.fix @@ -9,6 +9,7 @@ const o = { 1e4: "something", .123: "float", 123: 'numbers do not need quotes', // failure + 1.23: null, '010': 'but this one does.', '.123': 'as does this one', fn() { return }, diff --git a/test/rules/object-literal-key-quotes/as-needed/test.ts.lint b/test/rules/object-literal-key-quotes/as-needed/test.ts.lint index f0171420b1b..4a7facdccfe 100644 --- a/test/rules/object-literal-key-quotes/as-needed/test.ts.lint +++ b/test/rules/object-literal-key-quotes/as-needed/test.ts.lint @@ -12,6 +12,7 @@ const o = { .123: "float", '123': 'numbers do not need quotes', // failure ~~~~~ [Unnecessarily quoted property '123' found.] + 1.23: null, '010': 'but this one does.', '.123': 'as does this one', fn() { return }, From eae8fe0faab37697be74b627a64125360c31c208 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Mon, 30 Jan 2017 18:12:24 -0800 Subject: [PATCH 086/131] ignore-comments option for no-trailing-whitespace (#2153) Addresses comments in #2049. --- src/rules/noTrailingWhitespaceRule.ts | 61 +++++++++++-------- .../{ => default}/test.js.fix | 0 .../{ => default}/test.js.lint | 0 .../{ => default}/test.ts.fix | 0 .../{ => default}/test.ts.lint | 0 .../{ => default}/tslint.json | 0 .../ignore-comments/test.js.fix | 10 +++ .../ignore-comments/test.js.lint | 16 +++++ .../ignore-comments/test.ts.fix | 20 ++++++ .../ignore-comments/test.ts.lint | 28 +++++++++ .../ignore-comments/tslint.json | 8 +++ 11 files changed, 119 insertions(+), 24 deletions(-) rename test/rules/no-trailing-whitespace/{ => default}/test.js.fix (100%) rename test/rules/no-trailing-whitespace/{ => default}/test.js.lint (100%) rename test/rules/no-trailing-whitespace/{ => default}/test.ts.fix (100%) rename test/rules/no-trailing-whitespace/{ => default}/test.ts.lint (100%) rename test/rules/no-trailing-whitespace/{ => default}/tslint.json (100%) create mode 100644 test/rules/no-trailing-whitespace/ignore-comments/test.js.fix create mode 100644 test/rules/no-trailing-whitespace/ignore-comments/test.js.lint create mode 100644 test/rules/no-trailing-whitespace/ignore-comments/test.ts.fix create mode 100644 test/rules/no-trailing-whitespace/ignore-comments/test.ts.lint create mode 100644 test/rules/no-trailing-whitespace/ignore-comments/tslint.json diff --git a/src/rules/noTrailingWhitespaceRule.ts b/src/rules/noTrailingWhitespaceRule.ts index 6cc85e2eacd..ef6a2578fb5 100644 --- a/src/rules/noTrailingWhitespaceRule.ts +++ b/src/rules/noTrailingWhitespaceRule.ts @@ -19,6 +19,8 @@ import * as ts from "typescript"; import * as Lint from "../index"; +const OPTION_IGNORE_COMMENTS = "ignore-comments"; + export class Rule extends Lint.Rules.AbstractRule { /* tslint:disable:object-literal-sort-keys */ public static metadata: Lint.IRuleMetadata = { @@ -27,9 +29,18 @@ export class Rule extends Lint.Rules.AbstractRule { rationale: "Keeps version control diffs clean as it prevents accidental whitespace from being committed.", optionsDescription: "Not configurable.", hasFix: true, - options: null, - optionExamples: ["true"], - type: "maintainability", + options: { + type: "array", + items: { + type: "string", + enum: [OPTION_IGNORE_COMMENTS], + }, + }, + optionExamples: [ + "true", + `[true, "${OPTION_IGNORE_COMMENTS}"]`, + ], + type: "style", typescriptOnly: false, }; /* tslint:enable:object-literal-sort-keys */ @@ -55,29 +66,31 @@ class NoTrailingWhitespaceWalker extends Lint.RuleWalker { lastSeenWasWhitespace = true; lastSeenWhitespacePosition = pos.tokenStart; } else { - if (kind === ts.SyntaxKind.SingleLineCommentTrivia) { - const commentText = fullText.substring(pos.tokenStart + 2, pos.end); - const match = /\s+$/.exec(commentText); - if (match !== null) { - this.reportFailure(pos.end - match[0].length, pos.end); - } - } else if (kind === ts.SyntaxKind.MultiLineCommentTrivia) { - let startPos = pos.tokenStart + 2; - const commentText = fullText.substring(startPos, pos.end - 2); - const lines = commentText.split("\n"); - // we don't want to check the content of the last comment line, as it is always followed by */ - const len = lines.length - 1; - for (let i = 0; i < len; ++i) { - let line = lines[i]; - // remove carriage return at the end, it is does not account to trailing whitespace - if (line.endsWith("\r")) { - line = line.substr(0, line.length - 1); + if (!this.hasOption(OPTION_IGNORE_COMMENTS)) { + if (kind === ts.SyntaxKind.SingleLineCommentTrivia) { + const commentText = fullText.substring(pos.tokenStart + 2, pos.end); + const match = /\s+$/.exec(commentText); + if (match !== null) { + this.reportFailure(pos.end - match[0].length, pos.end); } - const start = line.search(/\s+$/); - if (start !== -1) { - this.reportFailure(startPos + start, startPos + line.length); + } else if (kind === ts.SyntaxKind.MultiLineCommentTrivia) { + let startPos = pos.tokenStart + 2; + const commentText = fullText.substring(startPos, pos.end - 2); + const lines = commentText.split("\n"); + // we don't want to check the content of the last comment line, as it is always followed by */ + const len = lines.length - 1; + for (let i = 0; i < len; ++i) { + let line = lines[i]; + // remove carriage return at the end, it is does not account to trailing whitespace + if (line.endsWith("\r")) { + line = line.substr(0, line.length - 1); + } + const start = line.search(/\s+$/); + if (start !== -1) { + this.reportFailure(startPos + start, startPos + line.length); + } + startPos += lines[i].length + 1; } - startPos += lines[i].length + 1; } } lastSeenWasWhitespace = false; diff --git a/test/rules/no-trailing-whitespace/test.js.fix b/test/rules/no-trailing-whitespace/default/test.js.fix similarity index 100% rename from test/rules/no-trailing-whitespace/test.js.fix rename to test/rules/no-trailing-whitespace/default/test.js.fix diff --git a/test/rules/no-trailing-whitespace/test.js.lint b/test/rules/no-trailing-whitespace/default/test.js.lint similarity index 100% rename from test/rules/no-trailing-whitespace/test.js.lint rename to test/rules/no-trailing-whitespace/default/test.js.lint diff --git a/test/rules/no-trailing-whitespace/test.ts.fix b/test/rules/no-trailing-whitespace/default/test.ts.fix similarity index 100% rename from test/rules/no-trailing-whitespace/test.ts.fix rename to test/rules/no-trailing-whitespace/default/test.ts.fix diff --git a/test/rules/no-trailing-whitespace/test.ts.lint b/test/rules/no-trailing-whitespace/default/test.ts.lint similarity index 100% rename from test/rules/no-trailing-whitespace/test.ts.lint rename to test/rules/no-trailing-whitespace/default/test.ts.lint diff --git a/test/rules/no-trailing-whitespace/tslint.json b/test/rules/no-trailing-whitespace/default/tslint.json similarity index 100% rename from test/rules/no-trailing-whitespace/tslint.json rename to test/rules/no-trailing-whitespace/default/tslint.json diff --git a/test/rules/no-trailing-whitespace/ignore-comments/test.js.fix b/test/rules/no-trailing-whitespace/ignore-comments/test.js.fix new file mode 100644 index 00000000000..c2b659fb154 --- /dev/null +++ b/test/rules/no-trailing-whitespace/ignore-comments/test.js.fix @@ -0,0 +1,10 @@ +class Clazz { + public funcxion() { + console.log("test") ; + } + + + private foobar() { + } +} + diff --git a/test/rules/no-trailing-whitespace/ignore-comments/test.js.lint b/test/rules/no-trailing-whitespace/ignore-comments/test.js.lint new file mode 100644 index 00000000000..6ef8b5e18b4 --- /dev/null +++ b/test/rules/no-trailing-whitespace/ignore-comments/test.js.lint @@ -0,0 +1,16 @@ +class Clazz { + public funcxion() { + ~~~~ [0] + console.log("test") ; + ~~~~ [0] + } + +~~~~ [0] + +~~~~ [0] + private foobar() { + } +} + ~~~~ [0] + +[0]: trailing whitespace diff --git a/test/rules/no-trailing-whitespace/ignore-comments/test.ts.fix b/test/rules/no-trailing-whitespace/ignore-comments/test.ts.fix new file mode 100644 index 00000000000..10912132870 --- /dev/null +++ b/test/rules/no-trailing-whitespace/ignore-comments/test.ts.fix @@ -0,0 +1,20 @@ +class Clazz { + public funcxion() { + console.log("test") ; + } + + + private foobar() { + } +} +// single line comment without trailing whitespace +// single line comment with trailing whitespace + /* single line multiline comment */ +/* whitespace after comment */ +/* first line has trailing whitespace + second line is ok + last line is not checked */ +/* + */ + +// following line checks for trailing whitespace before EOF diff --git a/test/rules/no-trailing-whitespace/ignore-comments/test.ts.lint b/test/rules/no-trailing-whitespace/ignore-comments/test.ts.lint new file mode 100644 index 00000000000..be0264853af --- /dev/null +++ b/test/rules/no-trailing-whitespace/ignore-comments/test.ts.lint @@ -0,0 +1,28 @@ +class Clazz { + public funcxion() { + ~~~~ [trailing whitespace] + console.log("test") ; + ~~~~ [trailing whitespace] + } + +~~~~ [trailing whitespace] + +~~~~ [trailing whitespace] + private foobar() { + } +} + ~~~~ [trailing whitespace] +// single line comment without trailing whitespace +// single line comment with trailing whitespace + /* single line multiline comment */ +/* whitespace after comment */ + ~ [trailing whitespace] +/* first line has trailing whitespace + second line is ok + last line is not checked */ +/* + */ + +// following line checks for trailing whitespace before EOF + +~~~ [trailing whitespace] \ No newline at end of file diff --git a/test/rules/no-trailing-whitespace/ignore-comments/tslint.json b/test/rules/no-trailing-whitespace/ignore-comments/tslint.json new file mode 100644 index 00000000000..e5fca3fd774 --- /dev/null +++ b/test/rules/no-trailing-whitespace/ignore-comments/tslint.json @@ -0,0 +1,8 @@ +{ + "rules": { + "no-trailing-whitespace": [true, "ignore-comments"] + }, + "jsRules": { + "no-trailing-whitespace": [true, "ignore-comments"] + } +} From 04c2966e5c4fb0926adc38a750714a0e1ea2a29b Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Mon, 30 Jan 2017 18:26:44 -0800 Subject: [PATCH 087/131] Add more exceptions to 'no-unbound-method' (#2143) * Allow property access. * Allow binary expression (except `||`) --- src/rules/noUnboundMethodRule.ts | 10 ++++++++-- test/rules/no-unbound-method/test.ts.lint | 5 +++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/rules/noUnboundMethodRule.ts b/src/rules/noUnboundMethodRule.ts index a54549a1836..556597491ff 100644 --- a/src/rules/noUnboundMethodRule.ts +++ b/src/rules/noUnboundMethodRule.ts @@ -41,7 +41,7 @@ export class Rule extends Lint.Rules.TypedRule { class Walker extends Lint.ProgramAwareRuleWalker { public visitPropertyAccessExpression(node: ts.PropertyAccessExpression) { - if (!isInCall(node)) { + if (!isSafeUse(node)) { const symbol = this.getTypeChecker().getSymbolAtLocation(node); const declaration = symbol && symbol.valueDeclaration; if (declaration && isMethod(declaration)) { @@ -62,13 +62,19 @@ function isMethod(node: ts.Node): boolean { } } -function isInCall(node: ts.Node): boolean { +function isSafeUse(node: ts.Node): boolean { const parent = node.parent!; switch (parent.kind) { case ts.SyntaxKind.CallExpression: return (parent as ts.CallExpression).expression === node; case ts.SyntaxKind.TaggedTemplateExpression: return (parent as ts.TaggedTemplateExpression).tag === node; + // E.g. `obj.method.bind(obj)`. + case ts.SyntaxKind.PropertyAccessExpression: + return true; + // Allow most binary operators, but don't allow e.g. `myArray.forEach(obj.method || otherObj.otherMethod)`. + case ts.SyntaxKind.BinaryExpression: + return (parent as ts.BinaryExpression).operatorToken.kind !== ts.SyntaxKind.BarBarToken; default: return false; } diff --git a/test/rules/no-unbound-method/test.ts.lint b/test/rules/no-unbound-method/test.ts.lint index b23e2bf9213..b5142787b67 100644 --- a/test/rules/no-unbound-method/test.ts.lint +++ b/test/rules/no-unbound-method/test.ts.lint @@ -25,6 +25,11 @@ i.foo; ~~~~~ [0] i.bar; +c.method === i.foo; +[0].forEach(c.method || i.foo); + ~~~~~~~~ [0] + ~~~~~ [0] +[0].forEach(c.method.bind(c)); ; ~~~~~~~~ [0] From abaa138f43641ae2d9ba39cf07f306625a40d40b Mon Sep 17 00:00:00 2001 From: Minko Gechev Date: Mon, 30 Jan 2017 19:08:57 -0800 Subject: [PATCH 088/131] Remove Angular version number (#2156) Align to the new branding https://angularjs.blogspot.com/2017/01/branding-guidelines-for-angular-and.html --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 64ad94193e1..f6e23d41413 100644 --- a/README.md +++ b/README.md @@ -301,7 +301,7 @@ If we don't have all the rules you're looking for, you can either write your own - [ESLint rules for TSLint](https://github.com/buzinas/tslint-eslint-rules) - Improve your TSLint with the missing ESLint Rules - [tslint-microsoft-contrib](https://github.com/Microsoft/tslint-microsoft-contrib) - A set of TSLint rules used on some Microsoft projects -- [codelyzer](https://github.com/mgechev/codelyzer) - A set of tslint rules for static code analysis of Angular 2 TypeScript projects +- [codelyzer](https://github.com/mgechev/codelyzer) - A set of tslint rules for static code analysis of Angular TypeScript projects - [vrsource-tslint-rules](https://github.com/vrsource/vrsource-tslint-rules) #### Writing custom rules From 3554fb2e70fef828e48b1925f6601118d0153b2e Mon Sep 17 00:00:00 2001 From: Noah Chen Date: Mon, 30 Jan 2017 23:03:47 -0500 Subject: [PATCH 089/131] Perf improvements, cache/reuse to reduce i/o (#2126) --- src/configuration.ts | 1 + src/linter.ts | 8 ++-- src/ruleLoader.ts | 83 ++++++++++++++++++++++++++-------------- src/runner.ts | 11 +++++- test/eofLineRuleTests.ts | 7 ++-- test/ruleLoaderTests.ts | 21 ++++++++-- 6 files changed, 89 insertions(+), 42 deletions(-) diff --git a/src/configuration.ts b/src/configuration.ts index 04abae59438..8037fb2a067 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -266,6 +266,7 @@ function getHomeDir() { } } +// returns the absolute path (contrary to what the name implies) export function getRelativePath(directory?: string | null, relativeTo?: string) { if (directory != null) { const basePath = relativeTo || process.cwd(); diff --git a/src/linter.ts b/src/linter.ts index 43984bb9916..c6d76947c5d 100644 --- a/src/linter.ts +++ b/src/linter.ts @@ -99,8 +99,10 @@ class Linter { } public lint(fileName: string, source: string, configuration: IConfigurationFile = DEFAULT_CONFIG): void { - const enabledRules = this.getEnabledRules(fileName, source, configuration); let sourceFile = this.getSourceFile(fileName, source); + const isJs = /\.jsx?$/i.test(fileName); + + const enabledRules = this.getEnabledRules(sourceFile, configuration, isJs); let hasLinterRun = false; let fileFailures: RuleFailure[] = []; @@ -174,9 +176,7 @@ class Linter { return fileFailures; } - private getEnabledRules(fileName: string, source: string, configuration: IConfigurationFile = DEFAULT_CONFIG): IRule[] { - const sourceFile = this.getSourceFile(fileName, source); - const isJs = /\.jsx?$/i.test(fileName); + private getEnabledRules(sourceFile: ts.SourceFile, configuration: IConfigurationFile = DEFAULT_CONFIG, isJs: boolean): IRule[] { const configurationRules = isJs ? configuration.jsRules : configuration.rules; // walk the code first to find all the intervals where rules are disabled diff --git a/src/ruleLoader.ts b/src/ruleLoader.ts index 9ca7859370a..e7183e5547f 100644 --- a/src/ruleLoader.ts +++ b/src/ruleLoader.ts @@ -18,13 +18,15 @@ import * as fs from "fs"; import * as path from "path"; -import {getRulesDirectories} from "./configuration"; -import {IDisabledInterval, IRule} from "./language/rule/rule"; -import {camelize, dedent} from "./utils"; +import { getRelativePath } from "./configuration"; +import { AbstractRule } from "./language/rule/abstractRule"; +import { IDisabledInterval, IRule } from "./language/rule/rule"; +import { arrayify, camelize, dedent } from "./utils"; const moduleDirectory = path.dirname(module.filename); const CORE_RULES_DIRECTORY = path.resolve(moduleDirectory, ".", "rules"); const shownDeprecations: string[] = []; +const cachedRules = new Map(); // null indicates that the rule was not found export interface IEnableDisablePosition { isEnabled: boolean; @@ -42,20 +44,22 @@ export function loadRules(ruleConfiguration: {[name: string]: any}, for (const ruleName in ruleConfiguration) { if (ruleConfiguration.hasOwnProperty(ruleName)) { const ruleValue = ruleConfiguration[ruleName]; - const Rule = findRule(ruleName, rulesDirectories); - if (Rule == null) { - notFoundRules.push(ruleName); - } else { - if (isJs && Rule.metadata && Rule.metadata.typescriptOnly != null && Rule.metadata.typescriptOnly) { - notAllowedInJsRules.push(ruleName); + if (AbstractRule.isRuleEnabled(ruleValue) || enableDisableRuleMap.hasOwnProperty(ruleName)) { + const Rule: (typeof AbstractRule) | null = findRule(ruleName, rulesDirectories); + if (Rule == null) { + notFoundRules.push(ruleName); } else { - const ruleSpecificList = (ruleName in enableDisableRuleMap ? enableDisableRuleMap[ruleName] : []); - const disabledIntervals = buildDisabledIntervalsFromSwitches(ruleSpecificList); - rules.push(new Rule(ruleName, ruleValue, disabledIntervals)); - - if (Rule.metadata && Rule.metadata.deprecationMessage && shownDeprecations.indexOf(Rule.metadata.ruleName) === -1) { - console.warn(`${Rule.metadata.ruleName} is deprecated. ${Rule.metadata.deprecationMessage}`); - shownDeprecations.push(Rule.metadata.ruleName); + if (isJs && Rule.metadata && Rule.metadata.typescriptOnly != null && Rule.metadata.typescriptOnly) { + notAllowedInJsRules.push(ruleName); + } else { + const ruleSpecificList = (ruleName in enableDisableRuleMap ? enableDisableRuleMap[ruleName] : []); + const disabledIntervals = buildDisabledIntervalsFromSwitches(ruleSpecificList); + rules.push(new (Rule as any)(ruleName, ruleValue, disabledIntervals)); + + if (Rule.metadata && Rule.metadata.deprecationMessage && shownDeprecations.indexOf(Rule.metadata.ruleName) === -1) { + console.warn(`${Rule.metadata.ruleName} is deprecated. ${Rule.metadata.deprecationMessage}`); + shownDeprecations.push(Rule.metadata.ruleName); + } } } } @@ -89,26 +93,22 @@ export function loadRules(ruleConfiguration: {[name: string]: any}, export function findRule(name: string, rulesDirectories?: string | string[]) { const camelizedName = transformName(name); + let Rule: typeof AbstractRule | null; // first check for core rules - let Rule = loadRule(CORE_RULES_DIRECTORY, camelizedName); - if (Rule != null) { - return Rule; - } + Rule = loadCachedRule(CORE_RULES_DIRECTORY, camelizedName); - const directories = getRulesDirectories(rulesDirectories); - - for (const rulesDirectory of directories) { + if (Rule == null) { // then check for rules within the first level of rulesDirectory - if (rulesDirectory != null) { - Rule = loadRule(rulesDirectory, camelizedName); + for (const dir of arrayify(rulesDirectories)) { + Rule = loadCachedRule(dir, camelizedName, true); if (Rule != null) { - return Rule; + break; } } } - return undefined; + return Rule; } function transformName(name: string) { @@ -127,17 +127,42 @@ function transformName(name: string) { */ function loadRule(directory: string, ruleName: string) { const fullPath = path.join(directory, ruleName); - if (fs.existsSync(fullPath + ".js")) { const ruleModule = require(fullPath); if (ruleModule && ruleModule.Rule) { return ruleModule.Rule; } } - return undefined; } +function loadCachedRule(directory: string, ruleName: string, isCustomPath = false) { + // use cached value if available + const fullPath = path.join(directory, ruleName); + const cachedRule = cachedRules.get(fullPath); + if (cachedRule !== undefined) { + return cachedRule; + } + + // get absolute path + let absolutePath: string | undefined = directory; + if (isCustomPath) { + absolutePath = getRelativePath(directory); + if (absolutePath != null) { + if (!fs.existsSync(absolutePath)) { + throw new Error(`Could not find custom rule directory: ${directory}`); + } + } + } + + let Rule: typeof AbstractRule | null = null; + if (absolutePath != null) { + Rule = loadRule(absolutePath, ruleName); + } + cachedRules.set(fullPath, Rule); + return Rule; +} + /** * creates disabled intervals for rule based on list of switchers for it * @param ruleSpecificList - contains all switchers for rule states sorted top-down and strictly alternating between enabled and disabled diff --git a/src/runner.ts b/src/runner.ts index 5e767d3e651..af8c1c24a58 100644 --- a/src/runner.ts +++ b/src/runner.ts @@ -24,6 +24,7 @@ import { CONFIG_FILENAME, DEFAULT_CONFIG, findConfiguration, + IConfigurationFile, } from "./configuration"; import { FatalError } from "./error"; import * as Linter from "./linter"; @@ -213,6 +214,8 @@ export class Runner { rulesDirectory: this.options.rulesDirectory || "", }, program); + let lastFolder: string | undefined; + let configFile: IConfigurationFile | undefined; for (const file of files) { if (!fs.existsSync(file)) { console.error(`Unable to open file: ${file}`); @@ -237,8 +240,12 @@ export class Runner { } const contents = fs.readFileSync(file, "utf8"); - const configLoad = findConfiguration(possibleConfigAbsolutePath, file); - linter.lint(file, contents, configLoad.results); + const folder = path.dirname(file); + if (lastFolder !== folder) { + configFile = findConfiguration(possibleConfigAbsolutePath, folder).results; + lastFolder = folder; + } + linter.lint(file, contents, configFile); } const lintResult = linter.getResult(); diff --git a/test/eofLineRuleTests.ts b/test/eofLineRuleTests.ts index b45f9bf2f50..800d4794bb3 100644 --- a/test/eofLineRuleTests.ts +++ b/test/eofLineRuleTests.ts @@ -14,12 +14,13 @@ * limitations under the License. */ -import {TestUtils} from "./lint"; +import { Rule } from "../src/rules/eoflineRule"; +import { TestUtils } from "./lint"; describe("", () => { - const EofLineRule = TestUtils.getRule("eofline"); + const EofLineRule = TestUtils.getRule("eofline") as Rule | null; const fileName = "eofline.test.ts"; - const failureString = EofLineRule.FAILURE_STRING; + const failureString = Rule.FAILURE_STRING; it("ensures a trailing newline at EOF", () => { const actualFailures = TestUtils.applyRuleOnFile(fileName, EofLineRule); diff --git a/test/ruleLoaderTests.ts b/test/ruleLoaderTests.ts index 2c5eac97e96..2c81f20b18f 100644 --- a/test/ruleLoaderTests.ts +++ b/test/ruleLoaderTests.ts @@ -32,11 +32,11 @@ describe("Rule Loader", () => { "eofline": true, "forin": false, "no-debugger": true, - "quotemark": "single", + "quotemark": [true, "single"], }; const rules = loadRules(validConfiguration, {}, builtRulesDir); - assert.equal(rules.length, 5); + assert.equal(rules.length, 4); }); it("ignores invalid rules", () => { @@ -50,17 +50,30 @@ describe("Rule Loader", () => { assert.equal(rules.length, 1); }); + it("loads disabled rules if rule in enableDisableRuleMap", () => { + const validConfiguration: {[name: string]: any} = { + "class-name": true, + "eofline": true, + "forin": false, + "no-debugger": true, + "quotemark": [true, "single"], + }; + + const rules = loadRules(validConfiguration, {forin: [{isEnabled: true, position: 4}]}, builtRulesDir); + assert.equal(rules.length, 5); + }); + it("works with rulesDirectory argument as an Array", () => { const validConfiguration: {[name: string]: any} = { "class-name": true, "eofline": true, "forin": false, "no-debugger": true, - "quotemark": "single", + "quotemark": [true, "single"], }; const rules = loadRules(validConfiguration, {}, [builtRulesDir]); - assert.equal(rules.length, 5); + assert.equal(rules.length, 4); }); it("loads js rules", () => { From a8c9a107562abd14951492a80bfaac0d94c807a1 Mon Sep 17 00:00:00 2001 From: Minko Gechev Date: Mon, 30 Jan 2017 20:08:57 -0800 Subject: [PATCH 090/131] Add `no-import-side-effect` rule (#2155) Fix #2116 --- src/rules/noImportSideEffectRule.ts | 78 +++++++++++++++++++ .../default/test.js.lint | 21 +++++ .../default/test.ts.lint | 21 +++++ .../no-import-side-effect/default/tslint.json | 11 +++ .../ignore-module/test.js.lint | 18 +++++ .../ignore-module/test.ts.lint | 18 +++++ .../ignore-module/tslint.json | 21 +++++ 7 files changed, 188 insertions(+) create mode 100644 src/rules/noImportSideEffectRule.ts create mode 100644 test/rules/no-import-side-effect/default/test.js.lint create mode 100644 test/rules/no-import-side-effect/default/test.ts.lint create mode 100644 test/rules/no-import-side-effect/default/tslint.json create mode 100644 test/rules/no-import-side-effect/ignore-module/test.js.lint create mode 100644 test/rules/no-import-side-effect/ignore-module/test.ts.lint create mode 100644 test/rules/no-import-side-effect/ignore-module/tslint.json diff --git a/src/rules/noImportSideEffectRule.ts b/src/rules/noImportSideEffectRule.ts new file mode 100644 index 00000000000..b5e628325dc --- /dev/null +++ b/src/rules/noImportSideEffectRule.ts @@ -0,0 +1,78 @@ +/** + * @license + * Copyright 2017 Palantir Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as ts from "typescript"; + +import * as Lint from "tslint"; + +const OPTION_IGNORE_MODULE = "ignore-module"; + +export class Rule extends Lint.Rules.AbstractRule { + public static metadata: Lint.IRuleMetadata = { + description: "Avoid import statements with side-effect.", + optionExamples: ["true", `[true, { "${OPTION_IGNORE_MODULE}": "(\\.html|\\.css)$" }]`], + options: { + items: { + properties: { + "ignore-module": { + type: "string", + }, + }, + type: "object", + }, + maxLength: 1, + minLength: 0, + type: "array", + }, + optionsDescription: Lint.Utils.dedent` + One argument may be optionally provided: + + * \`${OPTION_IGNORE_MODULE}\` allows to specify a regex and ignore modules which it matches.`, + rationale: "Imports with side effects may have behavior which is hard for static verification.", + ruleName: "no-import-side-effect", + type: "typescript", + typescriptOnly: false, + }; + public static FAILURE_STRING = "import with explicit side-effect"; + + public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { + return this.applyWithWalker(new NoImportSideEffectWalker(sourceFile, this.getOptions())); + } +} + +class NoImportSideEffectWalker extends Lint.SkippableTokenAwareRuleWalker { + private scanner: ts.Scanner; + private ignorePattern: RegExp | null; + + constructor(sourceFile: ts.SourceFile, options: Lint.IOptions) { + super(sourceFile, options); + const patternConfig = this.getOptions().pop(); + this.ignorePattern = patternConfig ? new RegExp(patternConfig[OPTION_IGNORE_MODULE]) : null; + this.scanner = ts.createScanner(ts.ScriptTarget.ES5, false, ts.LanguageVariant.Standard, sourceFile.text); + } + + public visitImportDeclaration(node: ts.ImportDeclaration) { + const importClause = node.importClause; + if (importClause === undefined) { + const specifier = node.moduleSpecifier.getText(); + if (this.ignorePattern === null || !this.ignorePattern.test(specifier.substring(1, specifier.length - 1))) { + this.addFailureAtNode(node, Rule.FAILURE_STRING); + } + } + super.visitImportDeclaration(node); + } +} diff --git a/test/rules/no-import-side-effect/default/test.js.lint b/test/rules/no-import-side-effect/default/test.js.lint new file mode 100644 index 00000000000..7c6568222fc --- /dev/null +++ b/test/rules/no-import-side-effect/default/test.js.lint @@ -0,0 +1,21 @@ +// valid cases +import {Injectable, Component, Directive} from '@angular/core'; +import {Inject} from '@angular/core'; +import {Observable} from 'rxjs/Observable'; +import {Observable as Obs} from 'rxjs/Observable'; +import * as _ from 'underscore'; +import DefaultExport from 'module-with-default'; + +// invalid cases +import './allow-side-effect'; +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] +import './styles.css'; +~~~~~~~~~~~~~~~~~~~~~~ [0] +import 'rxjs/add/observable/of'; +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] +import 'rxjs/add/operator/mapTo'; +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] +import 'random-side-effect'; +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + +[0]: import with explicit side-effect diff --git a/test/rules/no-import-side-effect/default/test.ts.lint b/test/rules/no-import-side-effect/default/test.ts.lint new file mode 100644 index 00000000000..7c6568222fc --- /dev/null +++ b/test/rules/no-import-side-effect/default/test.ts.lint @@ -0,0 +1,21 @@ +// valid cases +import {Injectable, Component, Directive} from '@angular/core'; +import {Inject} from '@angular/core'; +import {Observable} from 'rxjs/Observable'; +import {Observable as Obs} from 'rxjs/Observable'; +import * as _ from 'underscore'; +import DefaultExport from 'module-with-default'; + +// invalid cases +import './allow-side-effect'; +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] +import './styles.css'; +~~~~~~~~~~~~~~~~~~~~~~ [0] +import 'rxjs/add/observable/of'; +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] +import 'rxjs/add/operator/mapTo'; +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] +import 'random-side-effect'; +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + +[0]: import with explicit side-effect diff --git a/test/rules/no-import-side-effect/default/tslint.json b/test/rules/no-import-side-effect/default/tslint.json new file mode 100644 index 00000000000..841b04699d5 --- /dev/null +++ b/test/rules/no-import-side-effect/default/tslint.json @@ -0,0 +1,11 @@ +{ + "rules": { + "no-import-side-effect": [true] + }, + "jsRules": { + "no-import-side-effect": [true] + }, + "linterOptions": { + "typeCheck": false + } +} diff --git a/test/rules/no-import-side-effect/ignore-module/test.js.lint b/test/rules/no-import-side-effect/ignore-module/test.js.lint new file mode 100644 index 00000000000..971b14817a2 --- /dev/null +++ b/test/rules/no-import-side-effect/ignore-module/test.js.lint @@ -0,0 +1,18 @@ +// valid cases +import {Injectable, Component, Directive} from '@angular/core'; +import {Observable} from 'rxjs/Observable'; +import {Observable as Obs} from 'rxjs/Observable'; +import * as _ from 'underscore'; +import DefaultExport from 'module-with-default'; +import './allow-side-effect'; +import './styles.css'; + +// invalid cases +import 'rxjs/add/observable/of'; +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] +import 'rxjs/add/operator/mapTo'; +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] +import 'random-side-effect'; +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + +[0]: import with explicit side-effect diff --git a/test/rules/no-import-side-effect/ignore-module/test.ts.lint b/test/rules/no-import-side-effect/ignore-module/test.ts.lint new file mode 100644 index 00000000000..971b14817a2 --- /dev/null +++ b/test/rules/no-import-side-effect/ignore-module/test.ts.lint @@ -0,0 +1,18 @@ +// valid cases +import {Injectable, Component, Directive} from '@angular/core'; +import {Observable} from 'rxjs/Observable'; +import {Observable as Obs} from 'rxjs/Observable'; +import * as _ from 'underscore'; +import DefaultExport from 'module-with-default'; +import './allow-side-effect'; +import './styles.css'; + +// invalid cases +import 'rxjs/add/observable/of'; +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] +import 'rxjs/add/operator/mapTo'; +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] +import 'random-side-effect'; +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] + +[0]: import with explicit side-effect diff --git a/test/rules/no-import-side-effect/ignore-module/tslint.json b/test/rules/no-import-side-effect/ignore-module/tslint.json new file mode 100644 index 00000000000..8b553b95b81 --- /dev/null +++ b/test/rules/no-import-side-effect/ignore-module/tslint.json @@ -0,0 +1,21 @@ +{ + "rules": { + "no-import-side-effect": [ + true, + { + "ignore-module": "(allow-side-effect|\\.css)$" + } + ] + }, + "jsRules": { + "no-import-side-effect": [ + true, + { + "ignore-module": "(allow-side-effect|\\.css)$" + } + ] + }, + "linterOptions": { + "typeCheck": false + } +} From 482d30c8cad4372b7d2caba9b84e02d2f79c5fa8 Mon Sep 17 00:00:00 2001 From: Noah Chen Date: Tue, 31 Jan 2017 15:26:08 -0500 Subject: [PATCH 091/131] Fix `prefer-for-of` false positive (#2160) --- src/rules/preferForOfRule.ts | 4 ++-- test/rules/prefer-for-of/test.ts.lint | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/rules/preferForOfRule.ts b/src/rules/preferForOfRule.ts index 9749e333d64..fab472a2bb4 100644 --- a/src/rules/preferForOfRule.ts +++ b/src/rules/preferForOfRule.ts @@ -94,10 +94,10 @@ class PreferForOfWalker extends Lint.BlockScopeAwareRuleWalker x.data[i]); +} + function sampleFunc() { // issue #1931, should not error const value1 = [1]; From 72134c322bcc9f7dc943791b20a2eb47eb6bf0b0 Mon Sep 17 00:00:00 2001 From: Noah Chen Date: Tue, 31 Jan 2017 16:16:29 -0500 Subject: [PATCH 092/131] Update yarn.lock and use yarn in CircleCI (#2161) --- circle.yml | 4 +++- yarn.lock | 46 +++++++++++++++++++++++----------------------- 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/circle.yml b/circle.yml index 292df0344f8..4a2f85346de 100644 --- a/circle.yml +++ b/circle.yml @@ -6,9 +6,11 @@ dependencies: pre: - node ./scripts/assertMinCircleNodes.js $CIRCLE_NODE_TOTAL - case $CIRCLE_NODE_INDEX in 0) nvm use 4.1.2 ;; 1) nvm use 5.7 ;; [2-3]) nvm use 6.1 ;; esac + override: + - yarn test: override: - - case $CIRCLE_NODE_INDEX in [0-2]) npm run verify ;; 3) npm run clean && npm run compile && npm i typescript@2.0.10 && npm run test ;; esac: + - case $CIRCLE_NODE_INDEX in [0-2]) yarn verify ;; 3) npm run clean && yarn compile && yarn add typescript@2.0.10 && yarn test ;; esac: parallel: true deployment: npm-latest: diff --git a/yarn.lock b/yarn.lock index fee9777b8f0..b8f96e924d0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -40,12 +40,12 @@ resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-2.0.29.tgz#5002e14f75e2d71e564281df0431c8c1b4a2a36a" "@types/mocha@^2.2.35": - version "2.2.35" - resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-2.2.35.tgz#dd3150b62a6de73368109308515c3aa86f81f1ec" + version "2.2.38" + resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-2.2.38.tgz#8c188f6e34c2e7c3f1d0127d908d5a36e5a60dc9" "@types/node@*", "@types/node@^6.0.56": - version "6.0.56" - resolved "https://registry.yarnpkg.com/@types/node/-/node-6.0.56.tgz#12bc7fff825e72807f55dcbce17e9db6177713dd" + version "6.0.62" + resolved "https://registry.yarnpkg.com/@types/node/-/node-6.0.62.tgz#85222c077b54f25b57417bb708b9f877bda37f89" "@types/optimist@0.0.29": version "0.0.29" @@ -59,7 +59,7 @@ "@types/update-notifier@^1.0.0": version "1.0.0" - resolved "https://https://registry.yarnpkg.com/@types/update-notifier/-/update-notifier-1.0.0.tgz#3ae6206a6d67c01ffddb9a1eac4cd9b518d534ee" + resolved "https://registry.yarnpkg.com/@types/update-notifier/-/update-notifier-1.0.0.tgz#3ae6206a6d67c01ffddb9a1eac4cd9b518d534ee" ansi-align@^1.1.0: version "1.1.0" @@ -68,8 +68,8 @@ ansi-align@^1.1.0: string-width "^1.0.1" ansi-regex@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.0.0.tgz#c5061b6e0ef8a81775e50f5d66151bf6bf371107" + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" ansi-styles@^2.2.1: version "2.2.1" @@ -98,12 +98,12 @@ assertion-error@^1.0.1: resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.0.2.tgz#13ca515d86206da0bac66e834dd397d87581094c" babel-code-frame@^6.20.0: - version "6.20.0" - resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.20.0.tgz#b968f839090f9a8bc6d41938fb96cb84f7387b26" + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.22.0.tgz#027620bee567a88c32561574e7fd0801d33118e4" dependencies: chalk "^1.1.0" esutils "^2.0.2" - js-tokens "^2.0.0" + js-tokens "^3.0.0" balanced-match@^0.4.1: version "0.4.2" @@ -275,8 +275,8 @@ error-ex@^1.2.0: is-arrayish "^0.2.1" es-abstract@^1.4.3: - version "1.6.1" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.6.1.tgz#bb8a2064120abcf928a086ea3d9043114285ec99" + version "1.7.0" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.7.0.tgz#dfade774e01bfcd97f96180298c449c8623fb94c" dependencies: es-to-primitive "^1.1.1" function-bind "^1.1.0" @@ -400,7 +400,7 @@ got@^5.0.0: unzip-response "^1.0.2" url-parse-lax "^1.0.0" -graceful-fs@^4.1.2: +graceful-fs@^4.1.11, graceful-fs@^4.1.2: version "4.1.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" @@ -515,9 +515,9 @@ isexe@^1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/isexe/-/isexe-1.1.2.tgz#36f3e22e60750920f5e7241a476a8c6a42275ad0" -js-tokens@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-2.0.0.tgz#79903f5563ee778cc1162e6dcf1a0027c97f9cb5" +js-tokens@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.1.tgz#08e9f132484a2c45a30907e9dc4d5567b7f114d7" js-yaml@^3.7.0: version "3.7.0" @@ -693,8 +693,8 @@ number-is-nan@^1.0.0: resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" object-assign@^4.0.1: - version "4.1.0" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.0.tgz#7a3b3d0e98063d43f4c03f2e8ae6cd51a86883a0" + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" object-keys@^1.0.8: version "1.0.11" @@ -981,8 +981,8 @@ timed-out@^3.0.0: version "0.0.1" tslint@latest: - version "4.4.0" - resolved "https://https://registry.yarnpkg.com/tslint/-/tslint-4.4.0.tgz#748d17bf8598dd044cca5771a03353ea422da29e" + version "4.4.2" + resolved "https://registry.yarnpkg.com/tslint/-/tslint-4.4.2.tgz#b14cb79ae039c72471ab4c2627226b940dda19c6" dependencies: babel-code-frame "^6.20.0" colors "^1.1.2" @@ -1064,10 +1064,10 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" write-file-atomic@^1.1.2: - version "1.2.0" - resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-1.2.0.tgz#14c66d4e4cb3ca0565c28cf3b7a6f3e4d5938fab" + version "1.3.1" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-1.3.1.tgz#7d45ba32316328dd1ec7d90f60ebc0d845bb759a" dependencies: - graceful-fs "^4.1.2" + graceful-fs "^4.1.11" imurmurhash "^0.1.4" slide "^1.1.5" From 6fa30eff61beb933b0a4eb215a05bdb3634b808b Mon Sep 17 00:00:00 2001 From: Irfan Hudda Date: Thu, 2 Feb 2017 21:51:32 +0530 Subject: [PATCH 093/131] Add docs for interface-over-type-literal (#2170) Add rationale and example for interface-over-type-literal --- src/rules/interfaceOverTypeLiteralRule.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/rules/interfaceOverTypeLiteralRule.ts b/src/rules/interfaceOverTypeLiteralRule.ts index 880269f0c62..ef5858fe4a5 100644 --- a/src/rules/interfaceOverTypeLiteralRule.ts +++ b/src/rules/interfaceOverTypeLiteralRule.ts @@ -23,9 +23,10 @@ export class Rule extends Lint.Rules.AbstractRule { public static metadata: Lint.IRuleMetadata = { ruleName: "interface-over-type-literal", description: "Prefer an interface declaration over a type literal (`type T = { ... }`)", - rationale: "style", + rationale: `Interfaces are generally preferred over type literals because interfaces can be implemented, extended and merged.`, optionsDescription: "Not configurable.", options: null, + optionExamples: ["true"], type: "style", typescriptOnly: true, }; From 8a7cfbf585b5d2a1ad05e55e336669703424f452 Mon Sep 17 00:00:00 2001 From: Klaus Meinhardt Date: Fri, 3 Feb 2017 17:55:04 +0100 Subject: [PATCH 094/131] Introduce AbstractWalker for performance (#2093) --- src/language/rule/abstractRule.ts | 32 ++++++++++-------- src/language/rule/rule.ts | 16 +++++++++ src/language/rule/typedRule.ts | 2 +- src/language/walker/index.ts | 1 + src/language/walker/walkContext.ts | 47 ++++++++++++++++++++++++++ src/language/walker/walker.ts | 16 ++++++++- src/rules/noMagicNumbersRule.ts | 53 ++++++++++-------------------- src/rules/noNullKeywordRule.ts | 24 +++++--------- 8 files changed, 126 insertions(+), 65 deletions(-) create mode 100644 src/language/walker/walkContext.ts diff --git a/src/language/rule/abstractRule.ts b/src/language/rule/abstractRule.ts index 242562560d3..7713fefd8ee 100644 --- a/src/language/rule/abstractRule.ts +++ b/src/language/rule/abstractRule.ts @@ -18,12 +18,12 @@ import * as ts from "typescript"; import {doesIntersect} from "../utils"; -import {IWalker} from "../walker"; +import {IWalker, WalkContext} from "../walker"; import {IDisabledInterval, IOptions, IRule, IRuleMetadata, RuleFailure} from "./rule"; export abstract class AbstractRule implements IRule { public static metadata: IRuleMetadata; - private options: IOptions; + protected readonly ruleArguments: any[]; public static isRuleEnabled(ruleConfigValue: any): boolean { if (typeof ruleConfigValue === "boolean") { @@ -37,22 +37,20 @@ export abstract class AbstractRule implements IRule { return false; } - constructor(ruleName: string, private value: any, private disabledIntervals: IDisabledInterval[]) { - let ruleArguments: any[] = []; - + constructor(protected readonly ruleName: string, private value: any, private disabledIntervals: IDisabledInterval[]) { if (Array.isArray(value) && value.length > 1) { - ruleArguments = value.slice(1); + this.ruleArguments = value.slice(1); + } else { + this.ruleArguments = []; } - - this.options = { - disabledIntervals, - ruleArguments, - ruleName, - }; } public getOptions(): IOptions { - return this.options; + return { + disabledIntervals: this.disabledIntervals, + ruleArguments: this.ruleArguments, + ruleName: this.ruleName, + }; } public abstract apply(sourceFile: ts.SourceFile, languageService: ts.LanguageService): RuleFailure[]; @@ -66,6 +64,14 @@ export abstract class AbstractRule implements IRule { return AbstractRule.isRuleEnabled(this.value); } + protected applyWithFunction(sourceFile: ts.SourceFile, walkFn: (ctx: WalkContext) => void): RuleFailure[]; + protected applyWithFunction(sourceFile: ts.SourceFile, walkFn: (ctx: WalkContext) => void, options: T): RuleFailure[]; + protected applyWithFunction(sourceFile: ts.SourceFile, walkFn: (ctx: WalkContext) => void, options?: T): RuleFailure[] { + const ctx = new WalkContext(sourceFile, this.ruleName, options); + walkFn(ctx); + return this.filterFailures(ctx.failures); + } + protected filterFailures(failures: RuleFailure[]): RuleFailure[] { const result: RuleFailure[] = []; for (const failure of failures) { diff --git a/src/language/rule/rule.ts b/src/language/rule/rule.ts index a6b2ad119cd..26ec45b85a3 100644 --- a/src/language/rule/rule.ts +++ b/src/language/rule/rule.ts @@ -126,6 +126,22 @@ export class Replacement { return replacements.reduce((text, r) => r.apply(text), content); } + public static replaceFromTo(start: number, end: number, text: string) { + return new Replacement(start, end - start, text); + } + + public static deleteText(start: number, length: number) { + return new Replacement(start, length, ""); + } + + public static deleteFromTo(start: number, end: number) { + return new Replacement(start, end - start, ""); + } + + public static appendText(start: number, text: string) { + return new Replacement(start, 0, text); + } + constructor(private innerStart: number, private innerLength: number, private innerText: string) { } diff --git a/src/language/rule/typedRule.ts b/src/language/rule/typedRule.ts index 2e91c092bca..62cdaf9b516 100644 --- a/src/language/rule/typedRule.ts +++ b/src/language/rule/typedRule.ts @@ -28,7 +28,7 @@ export abstract class TypedRule extends AbstractRule { public apply(): RuleFailure[] { // if no program is given to the linter, throw an error - throw new Error(`${this.getOptions().ruleName} requires type checking`); + throw new Error(`${this.ruleName} requires type checking`); } public abstract applyWithProgram(sourceFile: ts.SourceFile, languageService: ts.LanguageService): RuleFailure[]; diff --git a/src/language/walker/index.ts b/src/language/walker/index.ts index bf1ab5df58b..75e254fe056 100644 --- a/src/language/walker/index.ts +++ b/src/language/walker/index.ts @@ -21,4 +21,5 @@ export * from "./ruleWalker"; export * from "./scopeAwareRuleWalker"; export * from "./skippableTokenAwareRuleWalker"; export * from "./syntaxWalker"; +export * from "./walkContext"; export * from "./walker"; diff --git a/src/language/walker/walkContext.ts b/src/language/walker/walkContext.ts new file mode 100644 index 00000000000..3af20a74c4b --- /dev/null +++ b/src/language/walker/walkContext.ts @@ -0,0 +1,47 @@ +/** + * @license + * Copyright 2017 Palantir Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as ts from "typescript"; + +import {Fix, Replacement, RuleFailure} from "../rule/rule"; + +export class WalkContext { + public readonly failures: RuleFailure[] = []; + + constructor(public readonly sourceFile: ts.SourceFile, public readonly ruleName: string, public readonly options: T) {} + + /** Add a failure with any arbitrary span. Prefer `addFailureAtNode` if possible. */ + public addFailureAt(start: number, width: number, failure: string, fix?: Fix) { + this.addFailure(start, start + width, failure, fix); + } + + public addFailure(start: number, end: number, failure: string, fix?: Fix) { + const fileLength = this.sourceFile.end; + this.failures.push( + new RuleFailure(this.sourceFile, Math.min(start, fileLength), Math.min(end, fileLength), failure, this.ruleName, fix), + ); + } + + /** Add a failure using a node's span. */ + public addFailureAtNode(node: ts.Node, failure: string, fix?: Fix) { + this.addFailure(node.getStart(this.sourceFile), node.getEnd(), failure, fix); + } + + public createFix(...replacements: Replacement[]) { + return new Fix(this.ruleName, replacements); + } +} diff --git a/src/language/walker/walker.ts b/src/language/walker/walker.ts index 3c07585e418..2613a1a5f48 100644 --- a/src/language/walker/walker.ts +++ b/src/language/walker/walker.ts @@ -18,9 +18,23 @@ import * as ts from "typescript"; import {RuleFailure} from "../rule/rule"; +import {WalkContext} from "./walkContext"; +import {IWalker} from "./walker"; export interface IWalker { getSourceFile(): ts.SourceFile; - walk(node: ts.Node): void; + walk(sourceFile: ts.SourceFile): void; getFailures(): RuleFailure[]; } + +export abstract class AbstractWalker extends WalkContext implements IWalker { + public abstract walk(sourceFile: ts.SourceFile): void; + + public getSourceFile() { + return this.sourceFile; + } + + public getFailures() { + return this.failures; + } +} diff --git a/src/rules/noMagicNumbersRule.ts b/src/rules/noMagicNumbersRule.ts index bfc37e2fad5..999fc3e81f5 100644 --- a/src/rules/noMagicNumbersRule.ts +++ b/src/rules/noMagicNumbersRule.ts @@ -19,8 +19,6 @@ import * as ts from "typescript"; import * as Lint from "../index"; -import { IOptions } from "../language/rule/rule"; - export class Rule extends Lint.Rules.AbstractRule { /* tslint:disable:object-literal-sort-keys */ public static metadata: Lint.IRuleMetadata = { @@ -63,44 +61,29 @@ export class Rule extends Lint.Rules.AbstractRule { public static DEFAULT_ALLOWED = [ -1, 0, 1 ]; public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { - return this.applyWithWalker(new NoMagicNumbersWalker(sourceFile, this.getOptions())); + const allowedNumbers = this.ruleArguments.length > 0 ? this.ruleArguments : Rule.DEFAULT_ALLOWED; + return this.applyWithWalker(new NoMagicNumbersWalker(sourceFile, this.ruleName, new Set(allowedNumbers.map(String)))); } } -class NoMagicNumbersWalker extends Lint.RuleWalker { - // allowed magic numbers - private allowed: Set; - constructor(sourceFile: ts.SourceFile, options: IOptions) { - super(sourceFile, options); - - const configOptions = this.getOptions(); - const allowedNumbers: number[] = configOptions.length > 0 ? configOptions : Rule.DEFAULT_ALLOWED; - this.allowed = new Set(allowedNumbers.map(String)); - } - - public visitNode(node: ts.Node) { - const num = getLiteralNumber(node); - if (num !== undefined) { - if (!Rule.ALLOWED_NODES.has(node.parent!.kind) && !this.allowed.has(num)) { - this.addFailureAtNode(node, Rule.FAILURE_STRING); +class NoMagicNumbersWalker extends Lint.AbstractWalker> { + public walk(sourceFile: ts.SourceFile) { + const cb = (node: ts.Node): void => { + if (node.kind === ts.SyntaxKind.NumericLiteral) { + this.checkNumericLiteral(node, (node as ts.NumericLiteral).text); + } else if (node.kind === ts.SyntaxKind.PrefixUnaryExpression && + (node as ts.PrefixUnaryExpression).operator === ts.SyntaxKind.MinusToken) { + this.checkNumericLiteral(node, "-" + ((node as ts.PrefixUnaryExpression).operand as ts.NumericLiteral).text); + } else { + ts.forEachChild(node, cb); } - } else { - super.visitNode(node); - } + }; + return ts.forEachChild(sourceFile, cb); } -} -/** If node is a number literal, return a string representation of that number. */ -function getLiteralNumber(node: ts.Node): string | undefined { - if (node.kind === ts.SyntaxKind.NumericLiteral) { - return (node as ts.NumericLiteral).text; - } - if (node.kind !== ts.SyntaxKind.PrefixUnaryExpression) { - return undefined; - } - const { operator, operand } = node as ts.PrefixUnaryExpression; - if (operator === ts.SyntaxKind.MinusToken && operand.kind === ts.SyntaxKind.NumericLiteral) { - return "-" + (operand as ts.NumericLiteral).text; + private checkNumericLiteral(node: ts.Node, num: string) { + if (!Rule.ALLOWED_NODES.has(node.parent!.kind) && !this.options.has(num)) { + this.addFailureAtNode(node, Rule.FAILURE_STRING); + } } - return undefined; } diff --git a/src/rules/noNullKeywordRule.ts b/src/rules/noNullKeywordRule.ts index 44f66987331..b3078f1efa8 100644 --- a/src/rules/noNullKeywordRule.ts +++ b/src/rules/noNullKeywordRule.ts @@ -40,25 +40,19 @@ export class Rule extends Lint.Rules.AbstractRule { public static FAILURE_STRING = "Use 'undefined' instead of 'null'"; public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { - return this.applyWithWalker(new NullWalker(sourceFile, this.getOptions())); + return this.applyWithFunction(sourceFile, walk); } } -class NullWalker extends Lint.RuleWalker { - public visitNode(node: ts.Node) { - super.visitNode(node); - if (node.kind === ts.SyntaxKind.NullKeyword && !isPartOfType(node)) { - this.addFailureAtNode(node, Rule.FAILURE_STRING); +function walk(ctx: Lint.WalkContext) { + return ts.forEachChild(ctx.sourceFile, cb); + function cb(node: ts.Node): void { + if (node.kind >= ts.SyntaxKind.FirstTypeNode && node.kind <= ts.SyntaxKind.LastTypeNode) { + return; // skip type nodes } - } -} - -function isPartOfType({ parent }: ts.Node) { - while (parent != null) { - if (ts.SyntaxKind.FirstTypeNode <= parent.kind && parent.kind <= ts.SyntaxKind.LastTypeNode) { - return true; + if (node.kind === ts.SyntaxKind.NullKeyword) { + return ctx.addFailureAtNode(node, Rule.FAILURE_STRING); } - parent = parent.parent; + return ts.forEachChild(node, cb); } - return false; } From dc7c47344c15c11513dc1253ed14bab259edd3ce Mon Sep 17 00:00:00 2001 From: Martin Probst Date: Sun, 5 Feb 2017 04:15:23 +0100 Subject: [PATCH 095/131] Add fixers for no-any and objectLiteralShorthand (for keys). (#2165) --- src/rules/noAnyRule.ts | 6 ++- src/rules/objectLiteralShorthandRule.ts | 8 +++- test/rules/no-any/test.ts.fix | 11 ++++++ .../object-literal-shorthand/test.ts.fix | 37 +++++++++++++++++++ 4 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 test/rules/no-any/test.ts.fix create mode 100644 test/rules/object-literal-shorthand/test.ts.fix diff --git a/src/rules/noAnyRule.ts b/src/rules/noAnyRule.ts index 0f843334b38..9ce84704125 100644 --- a/src/rules/noAnyRule.ts +++ b/src/rules/noAnyRule.ts @@ -24,6 +24,7 @@ export class Rule extends Lint.Rules.AbstractRule { public static metadata: Lint.IRuleMetadata = { ruleName: "no-any", description: "Diallows usages of `any` as a type declaration.", + hasFix: true, rationale: "Using `any` as a type declaration nullifies the compile-time benefits of the type system.", optionsDescription: "Not configurable.", options: null, @@ -42,7 +43,10 @@ export class Rule extends Lint.Rules.AbstractRule { class NoAnyWalker extends Lint.RuleWalker { public visitAnyKeyword(node: ts.Node) { - this.addFailureAtNode(node, Rule.FAILURE_STRING); + const fix = this.createFix( + this.createReplacement(node.getStart(), node.getWidth(), "{}"), + ); + this.addFailureAtNode(node, Rule.FAILURE_STRING, fix); super.visitAnyKeyword(node); } } diff --git a/src/rules/objectLiteralShorthandRule.ts b/src/rules/objectLiteralShorthandRule.ts index fa40cc160c5..31c17f5f545 100644 --- a/src/rules/objectLiteralShorthandRule.ts +++ b/src/rules/objectLiteralShorthandRule.ts @@ -23,6 +23,7 @@ export class Rule extends Lint.Rules.AbstractRule { public static metadata: Lint.IRuleMetadata = { ruleName: "object-literal-shorthand", description: "Enforces use of ES6 object literal shorthand when possible.", + hasFix: true, optionsDescription: "Not configurable.", options: null, optionExamples: ["true"], @@ -49,7 +50,12 @@ class ObjectLiteralShorthandWalker extends Lint.RuleWalker { if (name.kind === ts.SyntaxKind.Identifier && value.kind === ts.SyntaxKind.Identifier && name.getText() === value.getText()) { - this.addFailureAtNode(node, Rule.LONGHAND_PROPERTY); + // Delete from name start up to value to include the ':'. + const lengthToValueStart = value.getStart() - name.getStart(); + const fix = this.createFix( + this.deleteText(name.getStart(), lengthToValueStart), + ); + this.addFailureAtNode(node, Rule.LONGHAND_PROPERTY, fix); } if (value.kind === ts.SyntaxKind.FunctionExpression) { diff --git a/test/rules/no-any/test.ts.fix b/test/rules/no-any/test.ts.fix new file mode 100644 index 00000000000..2e28a65aeca --- /dev/null +++ b/test/rules/no-any/test.ts.fix @@ -0,0 +1,11 @@ +var x: {}; // error + +function foo(a: {}) : {} { // 2 errors + return; +} + +let a: {} = 2, // error + b: {} = 4; // error + +let {a: c, b: d}: {c: {}, d: number} = {c: 99, d: 100}; // error + diff --git a/test/rules/object-literal-shorthand/test.ts.fix b/test/rules/object-literal-shorthand/test.ts.fix new file mode 100644 index 00000000000..a0e5b014b3a --- /dev/null +++ b/test/rules/object-literal-shorthand/test.ts.fix @@ -0,0 +1,37 @@ +const bad = { + w: function() {}, + x: function *() {}, + [y]: function() {}, + z +}; + +const good = { + w() {}, + *x() {}, + [y]() {}, + z +}; + +const arrows = { + x: (y) => y // this is OK. +}; + +const namedFunctions = { + x: function y() {} // named function expressions are also OK. +}; + +const quotes = { + "foo-bar": function() {}, + "foo-bar"() {} +}; + +const extraCases = { + x, + a: 123, + b: "hello", + c: 'c', + ["a" + "nested"]: { + x + } +}; + From 290d597e7f92f8075f5be225e0ac1936a6bc40de Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Tue, 7 Feb 2017 06:58:11 -0800 Subject: [PATCH 096/131] Added location, type, and privacy to completed-docs (#2095) Also adds interfaces, enums, types, and a few other doc-able contructs. --- src/rules/completedDocsRule.ts | 388 ++++++++++++++++-- test/rules/completed-docs/all/test.ts.lint | 59 --- .../completed-docs/defaults/test.ts.lint | 21 + .../{all => defaults}/tslint.json | 0 .../completed-docs/functions/test.ts.lint | 50 --- .../completed-docs/functions/tslint.json | 8 - .../completed-docs/locations/test.ts.lint | 31 ++ .../completed-docs/locations/tslint.json | 15 + .../completed-docs/privacies/test.ts.lint | 33 ++ .../completed-docs/privacies/tslint.json | 15 + test/rules/completed-docs/types/test.ts.lint | 223 ++++++++++ test/rules/completed-docs/types/tslint.json | 19 + .../completed-docs/visibilities/test.ts.lint | 26 ++ .../completed-docs/visibilities/tslint.json | 15 + 14 files changed, 747 insertions(+), 156 deletions(-) delete mode 100644 test/rules/completed-docs/all/test.ts.lint create mode 100644 test/rules/completed-docs/defaults/test.ts.lint rename test/rules/completed-docs/{all => defaults}/tslint.json (100%) delete mode 100644 test/rules/completed-docs/functions/test.ts.lint delete mode 100644 test/rules/completed-docs/functions/tslint.json create mode 100644 test/rules/completed-docs/locations/test.ts.lint create mode 100644 test/rules/completed-docs/locations/tslint.json create mode 100644 test/rules/completed-docs/privacies/test.ts.lint create mode 100644 test/rules/completed-docs/privacies/tslint.json create mode 100644 test/rules/completed-docs/types/test.ts.lint create mode 100644 test/rules/completed-docs/types/tslint.json create mode 100644 test/rules/completed-docs/visibilities/test.ts.lint create mode 100644 test/rules/completed-docs/visibilities/tslint.json diff --git a/src/rules/completedDocsRule.ts b/src/rules/completedDocsRule.ts index 3d4aa5db9c0..02e75d15652 100644 --- a/src/rules/completedDocsRule.ts +++ b/src/rules/completedDocsRule.ts @@ -19,100 +19,410 @@ import * as ts from "typescript"; import * as Lint from "../index"; +export interface IBlockRequirementDescriptor { + visibilities?: Visibility[]; +} + +export interface IClassRequirementDescriptor { + locations?: Location[]; + privacies?: Privacy[]; +} + +export type RequirementDescriptor = IBlockRequirementDescriptor | IClassRequirementDescriptor; + +export interface IRequirementDescriptors { + [type: string /* DocType */]: RequirementDescriptor; +} + +export const ALL = "all"; + +export const ARGUMENT_CLASSES = "classes"; +export const ARGUMENT_ENUMS = "enums"; +export const ARGUMENT_FUNCTIONS = "functions"; +export const ARGUMENT_INTERFACES = "interfaces"; +export const ARGUMENT_METHODS = "methods"; +export const ARGUMENT_NAMESPACES = "namespaces"; +export const ARGUMENT_PROPERTIES = "properties"; +export const ARGUMENT_TYPES = "types"; +export const ARGUMENT_VARIABLES = "variables"; + +export const DESCRIPTOR_LOCATIONS = "locations"; +export const DESCRIPTOR_PRIVACIES = "privacies"; +export const DESCRIPTOR_VISIBILITIES = "visibilities"; + +export const LOCATION_INSTANCE = "instance"; +export const LOCATION_STATIC = "static"; + +export const PRIVACY_PRIVATE = "private"; +export const PRIVACY_PROTECTED = "protected"; +export const PRIVACY_PUBLIC = "public"; + +export const VISIBILITY_EXPORTED = "exported"; +export const VISIBILITY_INTERNAL = "internal"; + +export type All = typeof ALL; + +export type DocType = All + | typeof ARGUMENT_CLASSES + | typeof ARGUMENT_ENUMS + | typeof ARGUMENT_FUNCTIONS + | typeof ARGUMENT_INTERFACES + | typeof ARGUMENT_METHODS + | typeof ARGUMENT_NAMESPACES + | typeof ARGUMENT_PROPERTIES + | typeof ARGUMENT_TYPES + | typeof ARGUMENT_VARIABLES; + +export type Location = All + | typeof LOCATION_INSTANCE + | typeof LOCATION_STATIC; + +export type Privacy = All + | typeof PRIVACY_PRIVATE + | typeof PRIVACY_PROTECTED + | typeof PRIVACY_PUBLIC; + +export type Visibility = All + | typeof VISIBILITY_EXPORTED + | typeof VISIBILITY_INTERNAL; + +type BlockOrClassRequirement = BlockRequirement | ClassRequirement; + export class Rule extends Lint.Rules.TypedRule { + public static FAILURE_STRING_EXIST = "Documentation must exist for "; + + public static defaultArguments = [ + ARGUMENT_CLASSES, + ARGUMENT_FUNCTIONS, + ARGUMENT_METHODS, + ARGUMENT_PROPERTIES, + ] as DocType[]; + + public static ARGUMENT_DESCRIPTOR_BLOCK = { + properties: { + [DESCRIPTOR_VISIBILITIES]: { + enum: [ + ALL, + VISIBILITY_EXPORTED, + VISIBILITY_INTERNAL, + ], + type: "string", + }, + }, + type: "object", + }; + + public static ARGUMENT_DESCRIPTOR_CLASS = { + properties: { + [DESCRIPTOR_LOCATIONS]: { + enum: [ + ALL, + LOCATION_INSTANCE, + LOCATION_STATIC, + ], + type: "string", + }, + [DESCRIPTOR_PRIVACIES]: { + enum: [ + ALL, + PRIVACY_PRIVATE, + PRIVACY_PROTECTED, + PRIVACY_PUBLIC, + ], + type: "string", + }, + }, + type: "object", + }; + /* tslint:disable:object-literal-sort-keys */ public static metadata: Lint.IRuleMetadata = { ruleName: "completed-docs", description: "Enforces documentation for important items be filled out.", optionsDescription: Lint.Utils.dedent` - Either \`true\` to enable for all, or any of - \`["classes", "functions", "methods", "properties"] - to choose individual ones.\``, + \`true\` to enable for ["${ARGUMENT_CLASSES}", "${ARGUMENT_FUNCTIONS}", "${ARGUMENT_METHODS}", "${ARGUMENT_PROPERTIES}"], + or an array with each item in one of two formats: + + * \`string\` to enable for that type + * \`object\` keying types to when their documentation is required: + * \`"${ARGUMENT_METHODS}"\` and \`"${ARGUMENT_PROPERTIES} may specify: + * \`"${DESCRIPTOR_PRIVACIES}": + * \`"${ALL}"\` + * \`"${PRIVACY_PRIVATE}"\` + * \`"${PRIVACY_PROTECTED}"\` + * \`"${PRIVACY_PUBLIC}"\` + * \`"${DESCRIPTOR_LOCATIONS}: + * \`"${ALL}"\` + * \`"${LOCATION_INSTANCE}"\` + * \`"${LOCATION_STATIC}"\` + * All other types may specify \`"${DESCRIPTOR_VISIBILITIES}"\`: + * \`"${ALL}"\` + * \`"${VISIBILITY_EXPORTED}"\` + * \`"${VISIBILITY_INTERNAL}"\` + + Types that may be enabled are: + + * \`"${ARGUMENT_CLASSES}"\` + * \`"${ARGUMENT_ENUMS}"\` + * \`"${ARGUMENT_FUNCTIONS}"\` + * \`"${ARGUMENT_INTERFACES}"\` + * \`"${ARGUMENT_METHODS}"\` + * \`"${ARGUMENT_NAMESPACES}"\` + * \`"${ARGUMENT_PROPERTIES}"\` + * \`"${ARGUMENT_TYPES}"\` + * \`"${ARGUMENT_VARIABLES}"\``, options: { type: "array", items: { - type: "string", - enum: ["classes", "functions", "methods", "properties"], + anyOf: [ + { + enum: Rule.defaultArguments, + type: "string", + }, + { + type: "object", + properties: { + [ARGUMENT_CLASSES]: Rule.ARGUMENT_DESCRIPTOR_BLOCK, + [ARGUMENT_ENUMS]: Rule.ARGUMENT_DESCRIPTOR_BLOCK, + [ARGUMENT_FUNCTIONS]: Rule.ARGUMENT_DESCRIPTOR_BLOCK, + [ARGUMENT_INTERFACES]: Rule.ARGUMENT_DESCRIPTOR_BLOCK, + [ARGUMENT_METHODS]: Rule.ARGUMENT_DESCRIPTOR_CLASS, + [ARGUMENT_NAMESPACES]: Rule.ARGUMENT_DESCRIPTOR_BLOCK, + [ARGUMENT_PROPERTIES]: Rule.ARGUMENT_DESCRIPTOR_CLASS, + [ARGUMENT_TYPES]: Rule.ARGUMENT_DESCRIPTOR_BLOCK, + [ARGUMENT_VARIABLES]: Rule.ARGUMENT_DESCRIPTOR_BLOCK, + }, + }, + ], }, }, - optionExamples: ["true", `[true, "classes", "functions"]`], + optionExamples: [ + "true", + `[true, "${ARGUMENT_ENUMS}", "${ARGUMENT_FUNCTIONS}", "${ARGUMENT_METHODS}"]`, + `[true, { + "${ARGUMENT_ENUMS}": true, + "${ARGUMENT_FUNCTIONS}": { + "${DESCRIPTOR_VISIBILITIES}": ["${VISIBILITY_EXPORTED}"] + }, + "${ARGUMENT_METHODS}": { + "${DESCRIPTOR_LOCATIONS}": ["${LOCATION_INSTANCE}"] + "${DESCRIPTOR_PRIVACIES}": ["${PRIVACY_PUBLIC}", "${PRIVACY_PROTECTED}"] + } + }]`], type: "style", typescriptOnly: false, }; /* tslint:enable:object-literal-sort-keys */ - public static FAILURE_STRING_EXIST = " must have documentation."; - - public static ARGUMENT_CLASSES = "classes"; - public static ARGUMENT_FUNCTIONS = "functions"; - public static ARGUMENT_METHODS = "methods"; - public static ARGUMENT_PROPERTIES = "properties"; - - public static defaultArguments = [ - Rule.ARGUMENT_CLASSES, - Rule.ARGUMENT_FUNCTIONS, - Rule.ARGUMENT_METHODS, - Rule.ARGUMENT_PROPERTIES, - ]; - public applyWithProgram(sourceFile: ts.SourceFile, langSvc: ts.LanguageService): Lint.RuleFailure[] { const options = this.getOptions(); const completedDocsWalker = new CompletedDocsWalker(sourceFile, options, langSvc.getProgram()); - const nodesToCheck = this.getNodesToCheck(options.ruleArguments); - completedDocsWalker.setNodesToCheck(nodesToCheck); + completedDocsWalker.setRequirements(this.getRequirements(options.ruleArguments)); + return this.applyWithWalker(completedDocsWalker); } - private getNodesToCheck(ruleArguments: string[]) { - return ruleArguments.length === 0 ? Rule.defaultArguments : ruleArguments; + private getRequirements(ruleArguments: Array): Map { + if (ruleArguments.length === 0) { + ruleArguments = Rule.defaultArguments; + } + + return Requirement.constructRequirements(ruleArguments); } } -export class CompletedDocsWalker extends Lint.ProgramAwareRuleWalker { - private nodesToCheck: Set; +abstract class Requirement { + public static constructRequirements(ruleArguments: Array): Map { + const requirements: Map = new Map(); - public setNodesToCheck(nodesToCheck: string[]): void { - this.nodesToCheck = new Set(nodesToCheck); + for (const ruleArgument of ruleArguments) { + Requirement.addRequirements(requirements, ruleArgument); + } + + return requirements; + } + + private static addRequirements(requirements: Map, descriptor: DocType | IRequirementDescriptors) { + if (typeof descriptor === "string") { + requirements.set(descriptor, new BlockRequirement()); + return; + } + + for (const type in descriptor) { + if (descriptor.hasOwnProperty(type)) { + requirements.set( + type as DocType, + (type === "methods" || type === "properties") + ? new ClassRequirement(descriptor[type]) + : new BlockRequirement(descriptor[type])); + } + } + } + + protected constructor(public readonly descriptor: TDescriptor = {} as TDescriptor) { } + + public abstract shouldNodeBeDocumented(node: ts.Declaration): boolean; + + protected createSet(values?: T[]): Set { + if (!values || values.length === 0) { + values = [ALL as T]; + } + + return new Set(values); + } +} + +class BlockRequirement extends Requirement { + public readonly visibilities: Set = this.createSet(this.descriptor.visibilities); + + public shouldNodeBeDocumented(node: ts.Declaration): boolean { + if (this.visibilities.has(ALL)) { + return true; + } + + if (Lint.hasModifier(node.modifiers, ts.SyntaxKind.ExportKeyword)) { + return this.visibilities.has(VISIBILITY_EXPORTED); + } + + return this.visibilities.has(VISIBILITY_INTERNAL); + } +} + +class ClassRequirement extends Requirement { + public readonly locations: Set = this.createSet(this.descriptor.locations); + public readonly privacies: Set = this.createSet(this.descriptor.privacies); + + public shouldNodeBeDocumented(node: ts.Declaration) { + return this.shouldLocationBeDocumented(node) && this.shouldPrivacyBeDocumented(node); + } + + private shouldLocationBeDocumented(node: ts.Declaration) { + if (this.locations.has(ALL)) { + return true; + } + + if (Lint.hasModifier(node.modifiers, ts.SyntaxKind.StaticKeyword)) { + return this.locations.has(LOCATION_STATIC); + } + + return this.locations.has(LOCATION_INSTANCE); + } + + private shouldPrivacyBeDocumented(node: ts.Declaration) { + if (this.privacies.has(ALL)) { + return true; + } + + if (Lint.hasModifier(node.modifiers, ts.SyntaxKind.PrivateKeyword)) { + return this.privacies.has(PRIVACY_PRIVATE); + } + + if (Lint.hasModifier(node.modifiers, ts.SyntaxKind.ProtectedKeyword)) { + return this.privacies.has(PRIVACY_PROTECTED); + } + + return Lint.hasModifier(node.modifiers, ts.SyntaxKind.PublicKeyword); + } +} + +class CompletedDocsWalker extends Lint.ProgramAwareRuleWalker { + private static modifierAliases: { [i: string]: string } = { + export: "exported", + }; + + private requirements: Map; + + public setRequirements(requirements: Map): void { + this.requirements = requirements; } public visitClassDeclaration(node: ts.ClassDeclaration): void { - this.checkComments(node, Rule.ARGUMENT_CLASSES); + this.checkNode(node, ARGUMENT_CLASSES); super.visitClassDeclaration(node); } + public visitEnumDeclaration(node: ts.EnumDeclaration): void { + this.checkNode(node, ARGUMENT_ENUMS); + super.visitEnumDeclaration(node); + } + public visitFunctionDeclaration(node: ts.FunctionDeclaration): void { - this.checkComments(node, Rule.ARGUMENT_FUNCTIONS); + this.checkNode(node, ARGUMENT_FUNCTIONS); super.visitFunctionDeclaration(node); } - public visitPropertyDeclaration(node: ts.PropertyDeclaration): void { - this.checkComments(node, Rule.ARGUMENT_PROPERTIES); - super.visitPropertyDeclaration(node); + public visitInterfaceDeclaration(node: ts.InterfaceDeclaration): void { + this.checkNode(node, ARGUMENT_INTERFACES); + super.visitInterfaceDeclaration(node); } public visitMethodDeclaration(node: ts.MethodDeclaration): void { - this.checkComments(node, Rule.ARGUMENT_METHODS); + this.checkNode(node, ARGUMENT_METHODS); super.visitMethodDeclaration(node); } - private checkComments(node: ts.Declaration, nodeToCheck: string): void { - if (!this.nodesToCheck.has(nodeToCheck) || node.name === undefined) { + public visitModuleDeclaration(node: ts.ModuleDeclaration): void { + this.checkNode(node, ARGUMENT_NAMESPACES); + super.visitModuleDeclaration(node); + } + + public visitPropertyDeclaration(node: ts.PropertyDeclaration): void { + this.checkNode(node, ARGUMENT_PROPERTIES); + super.visitPropertyDeclaration(node); + } + + public visitTypeAliasDeclaration(node: ts.TypeAliasDeclaration): void { + this.checkNode(node, ARGUMENT_TYPES); + super.visitTypeAliasDeclaration(node); + } + + public visitVariableDeclaration(node: ts.VariableDeclaration): void { + this.checkNode(node, ARGUMENT_VARIABLES); + super.visitVariableDeclaration(node); + } + + private checkNode(node: ts.Declaration, nodeType: DocType): void { + if (node.name === undefined) { + return; + } + + const requirement = this.requirements.get(nodeType); + if (!requirement || !requirement.shouldNodeBeDocumented(node)) { return; } const comments = this.getTypeChecker().getSymbolAtLocation(node.name).getDocumentationComment(); + this.checkComments(node, nodeType, comments); + } + private checkComments(node: ts.Declaration, nodeDescriptor: string, comments: ts.SymbolDisplayPart[]) { if (comments.map((comment: ts.SymbolDisplayPart) => comment.text).join("").trim() === "") { - this.addDocumentationFailure(node, nodeToCheck); + this.addDocumentationFailure(node, nodeDescriptor); } } - private addDocumentationFailure(node: ts.Declaration, nodeToCheck: string): void { + private addDocumentationFailure(node: ts.Declaration, nodeType: string): void { const start = node.getStart(); const width = node.getText().split(/\r|\n/g)[0].length; - const description = nodeToCheck[0].toUpperCase() + nodeToCheck.substring(1) + Rule.FAILURE_STRING_EXIST; + const description = this.describeDocumentationFailure(node, nodeType); this.addFailureAt(start, width, description); } + + private describeDocumentationFailure(node: ts.Declaration, nodeType: string): string { + let description = Rule.FAILURE_STRING_EXIST; + + if (node.modifiers) { + description += node.modifiers.map((modifier) => this.describeModifier(modifier.kind)) + " "; + } + + return description + nodeType + "."; + } + + private describeModifier(kind: ts.SyntaxKind) { + const description = ts.SyntaxKind[kind].toLowerCase().split("keyword")[0]; + + return CompletedDocsWalker.modifierAliases[description] || description; + } } diff --git a/test/rules/completed-docs/all/test.ts.lint b/test/rules/completed-docs/all/test.ts.lint deleted file mode 100644 index 0cdc5889ed6..00000000000 --- a/test/rules/completed-docs/all/test.ts.lint +++ /dev/null @@ -1,59 +0,0 @@ -class BadClass { -~~~~~~~~~~~~~~~~ [0] - badProperty; - ~~~~~~~~~~~~ [1] - badMethod() { } - ~~~~~~~~~~~~~~~ [2] -} - -/** - * - */ -class EmptyClass { -~~~~~~~~~~~~~~~~~~ [0] - /** - * - */ - emptyProperty; - ~~~~~~~~~~~~~~ [1] - - /** - * - */ - emptyMethod() { } - ~~~~~~~~~~~~~~~~~ [2] -} - -/** - * ... - */ -class GoodClass { - /** - * ... - */ - goodProperty; - - /** - * ... - */ - goodMethod() { } -} - -function BadFunction() { } -~~~~~~~~~~~~~~~~~~~~~~~~~~ [3] - -/** - * - */ -function EmptyFunction() { } -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [3] - -/** - * ... - */ -function GoodFunction() { } - -[0]: Classes must have documentation. -[1]: Properties must have documentation. -[2]: Methods must have documentation. -[3]: Functions must have documentation. diff --git a/test/rules/completed-docs/defaults/test.ts.lint b/test/rules/completed-docs/defaults/test.ts.lint new file mode 100644 index 00000000000..d77d0af495b --- /dev/null +++ b/test/rules/completed-docs/defaults/test.ts.lint @@ -0,0 +1,21 @@ +export class Aaa { +~~~~~~~~~~~~~~~~~~ [Documentation must exist for exported classes.] + public bbb; + ~~~~~~~~~~~ [Documentation must exist for public properties.] + + public ccc() { } + ~~~~~~~~~~~~~~~~ [Documentation must exist for public methods.] +} + +export enum Ddd { } + +export function Eee() { } +~~~~~~~~~~~~~~~~~~~~~~~~~ [Documentation must exist for exported functions.] + +export interface Fff { } + +export namespace Ggg { } + +export type Hhh = any; + +export const i = true; diff --git a/test/rules/completed-docs/all/tslint.json b/test/rules/completed-docs/defaults/tslint.json similarity index 100% rename from test/rules/completed-docs/all/tslint.json rename to test/rules/completed-docs/defaults/tslint.json diff --git a/test/rules/completed-docs/functions/test.ts.lint b/test/rules/completed-docs/functions/test.ts.lint deleted file mode 100644 index 4fbe4304713..00000000000 --- a/test/rules/completed-docs/functions/test.ts.lint +++ /dev/null @@ -1,50 +0,0 @@ -class BadClass { - badMember; - badFunction() { } -} - -/** - * - */ -class EmptyClass { - /** - * - */ - emptyProperty; - - /** - * - */ - emptyMethod() { } -} - -/** - * ... - */ -class GoodClass { - /** - * ... - */ - goodProperty; - - /** - * ... - */ - goodMethod() { } -} - -function BadFunction() { } -~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] - -/** - * - */ -function EmptyFunction() { } -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0] - -/** - * ... - */ -function GoodFunction() { } - -[0]: Functions must have documentation. diff --git a/test/rules/completed-docs/functions/tslint.json b/test/rules/completed-docs/functions/tslint.json deleted file mode 100644 index 47a6dcf43f6..00000000000 --- a/test/rules/completed-docs/functions/tslint.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "linterOptions": { - "typeCheck": true - }, - "rules": { - "completed-docs": [true, "functions"] - } -} diff --git a/test/rules/completed-docs/locations/test.ts.lint b/test/rules/completed-docs/locations/test.ts.lint new file mode 100644 index 00000000000..032f3e89bd1 --- /dev/null +++ b/test/rules/completed-docs/locations/test.ts.lint @@ -0,0 +1,31 @@ +class Class { + badInstanceProperty = 1; + + /** + * ... + */ + goodInstanceProperty = 1; + + /** + * ... + */ + static badStaticProperty = 1; + + static goodStaticProperty = 1; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Documentation must exist for static properties.] + + badInstanceMethod() { } + ~~~~~~~~~~~~~~~~~~~~~~~ [Documentation must exist for methods.] + + /** + * ... + */ + goodInstanceMethod() { } + + static badStaticMethod() { } + + /** + * ... + */ + static goodStaticMethod() { } +} \ No newline at end of file diff --git a/test/rules/completed-docs/locations/tslint.json b/test/rules/completed-docs/locations/tslint.json new file mode 100644 index 00000000000..aafbf0ef568 --- /dev/null +++ b/test/rules/completed-docs/locations/tslint.json @@ -0,0 +1,15 @@ +{ + "linterOptions": { + "typeCheck": true + }, + "rules": { + "completed-docs": [true, { + "methods": { + "locations": ["instance"] + }, + "properties": { + "locations": ["static"] + } + }] + } +} diff --git a/test/rules/completed-docs/privacies/test.ts.lint b/test/rules/completed-docs/privacies/test.ts.lint new file mode 100644 index 00000000000..de127670856 --- /dev/null +++ b/test/rules/completed-docs/privacies/test.ts.lint @@ -0,0 +1,33 @@ +class Class { + badDefaultMethod() { } + ~~~~~~~~~~~~~~~~~~~~~~ [Documentation must exist for methods.] + + /** + * ... + */ + goodDefaultMethod() { } + + public badPublicMethod() { } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Documentation must exist for public methods.] + + /** + * ... + */ + public goodPublicMethod() { } + + protected badProtectedMethod() { } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Documentation must exist for protected methods.] + + /** + * ... + */ + protected goodProtectedMethod() { } + + private badPrivateMethod() { } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Documentation must exist for private methods.] + + /** + * ... + */ + private goodPrivateMethod() { } +} \ No newline at end of file diff --git a/test/rules/completed-docs/privacies/tslint.json b/test/rules/completed-docs/privacies/tslint.json new file mode 100644 index 00000000000..48ce03aff9e --- /dev/null +++ b/test/rules/completed-docs/privacies/tslint.json @@ -0,0 +1,15 @@ +{ + "linterOptions": { + "typeCheck": true + }, + "rules": { + "completed-docs": [true, { + "methods": { + "visibilities": ["public", "protected"] + }, + "properties": { + "visibilities": ["public"] + } + }] + } +} diff --git a/test/rules/completed-docs/types/test.ts.lint b/test/rules/completed-docs/types/test.ts.lint new file mode 100644 index 00000000000..ca176b038ff --- /dev/null +++ b/test/rules/completed-docs/types/test.ts.lint @@ -0,0 +1,223 @@ +class BadClass { +~~~~~~~~~~~~~~~~ [Documentation must exist for classes.] + badDefaultProperty; + ~~~~~~~~~~~~~~~~~~~ [Documentation must exist for properties.] + + public badPublicProperty; + ~~~~~~~~~~~~~~~~~~~~~~~~~ [Documentation must exist for public properties.] + + protected badProtectedProperty; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Documentation must exist for protected properties.] + + private badPrivateProperty; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Documentation must exist for private properties.] + + badDefaultMethod() { } + ~~~~~~~~~~~~~~~~~~~~~~ [Documentation must exist for methods.] + + public badPublicMethod() { } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Documentation must exist for public methods.] + + protected badProtectedMethod() { } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Documentation must exist for protected methods.] + + private badPrivateMethod() { } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Documentation must exist for private methods.] +} + +/** + * + */ +class EmptyClass { +~~~~~~~~~~~~~~~~~~ [Documentation must exist for classes.] + /** + * + */ + emptyDefaultProperty; + ~~~~~~~~~~~~~~~~~~~~~ [Documentation must exist for properties.] + + /** + * + */ + public emptyPublicProperty; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Documentation must exist for public properties.] + + /** + * + */ + protected emptyProtectedProperty; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Documentation must exist for protected properties.] + + /** + * + */ + private emptyPrivateProperty; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Documentation must exist for private properties.] + + /** + * + */ + emptyDefaultMethod() { } + ~~~~~~~~~~~~~~~~~~~~~~~~ [Documentation must exist for methods.] + + /** + * + */ + public emptyPublicMethod() { } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Documentation must exist for public methods.] + + /** + * + */ + protected emptyProtectedMethod() { } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Documentation must exist for protected methods.] + + /** + * + */ + private emptyPrivateMethod() { } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Documentation must exist for private methods.] +} + +/** + * ... + */ +class GoodClass { + /** + * ... + */ + goodDefaultProperty; + + /** + * ... + */ + public goodPublicProperty; + + /** + * ... + */ + protected goodProtectedProperty; + + /** + * ... + */ + private goodPrivateProperty; + + /** + * ... + */ + goodDefaultMethod() { } + + /** + * ... + */ + public goodPublicMethod() { } + + /** + * ... + */ + protected goodProtectedMethod() { } + + /** + * ... + */ + private goodPrivateMethod() { } +} + +enum BadEnum { } +~~~~~~~~~~~~~~~~ [Documentation must exist for enums.] + +/** + * + */ +enum EmptyEnum { } +~~~~~~~~~~~~~~~~~~ [Documentation must exist for enums.] + +/** + * ... + */ +enum GoodEnum { } + +function BadFunction() { } +~~~~~~~~~~~~~~~~~~~~~~~~~~ [Documentation must exist for functions.] + +/** + * + */ +function EmptyFunction() { } +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Documentation must exist for functions.] + +/** + * ... + */ +function GoodFunction() { } + +interface BadInterface { } +~~~~~~~~~~~~~~~~~~~~~~~~~~ [Documentation must exist for interfaces.] + +/** + * + */ +interface EmptyInterface { } +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Documentation must exist for interfaces.] + +/** + * ... + */ +interface GoodInterface { } + +namespace BadNamespace { } +~~~~~~~~~~~~~~~~~~~~~~~~~~ [Documentation must exist for namespaces.] + +/** + * + */ +namespace EmptyNamespace { } +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Documentation must exist for namespaces.] + +/** + * ... + */ +namespace GoodNamespace { } + +type BadType = 1; +~~~~~~~~~~~~~~~~~ [Documentation must exist for types.] + +/** + * + */ +type EmptyType = 1; +~~~~~~~~~~~~~~~~~~~ [Documentation must exist for types.] + +/** + * ... + */ +type GoodType = 1; + +type BadType = 1; +~~~~~~~~~~~~~~~~~ [Documentation must exist for types.] + +/** + * + */ +type EmptyType = 1; +~~~~~~~~~~~~~~~~~~~ [Documentation must exist for types.] + +/** + * ... + */ +type GoodType = 1; + +const BadVariable = 1; + ~~~~~~~~~~~~~~~ [Documentation must exist for variables.] + +/** + * + */ +const EmptyVariable = 1; + ~~~~~~~~~~~~~~~~~ [Documentation must exist for variables.] + +/** + * ... + */ +const GoodVariable = 1; diff --git a/test/rules/completed-docs/types/tslint.json b/test/rules/completed-docs/types/tslint.json new file mode 100644 index 00000000000..0f8ac6f9c46 --- /dev/null +++ b/test/rules/completed-docs/types/tslint.json @@ -0,0 +1,19 @@ +{ + "linterOptions": { + "typeCheck": true + }, + "rules": { + "completed-docs": [ + true, + "classes", + "enums", + "functions", + "interfaces", + "methods", + "namespaces", + "properties", + "types", + "variables" + ] + } +} diff --git a/test/rules/completed-docs/visibilities/test.ts.lint b/test/rules/completed-docs/visibilities/test.ts.lint new file mode 100644 index 00000000000..e33368dbb21 --- /dev/null +++ b/test/rules/completed-docs/visibilities/test.ts.lint @@ -0,0 +1,26 @@ +export enum BadExportedEnum { } +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Documentation must exist for exported enums.] + +/** + * ... + */ +export enum GoodExportedEnum { } + +enum BadInternalEnum { } + +/** + * ... + */ +enum GoodInternalEnum { } + +export interface BadExportedInterface { } + +export interface GoodExportedInterface { } + +interface BadInternalInterface { } +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Documentation must exist for interfaces.] + +/** + * ... + */ +interface GoodInternalInterface { } diff --git a/test/rules/completed-docs/visibilities/tslint.json b/test/rules/completed-docs/visibilities/tslint.json new file mode 100644 index 00000000000..cb0a666b870 --- /dev/null +++ b/test/rules/completed-docs/visibilities/tslint.json @@ -0,0 +1,15 @@ +{ + "linterOptions": { + "typeCheck": true + }, + "rules": { + "completed-docs": [true, { + "enums": { + "visibilities": ["exported"] + }, + "interfaces": { + "visibilities": ["internal"] + } + }] + } +} From 8193753c2d8ff91e349bd4223123e4d483ea0e70 Mon Sep 17 00:00:00 2001 From: Naoto Usuyama Date: Tue, 7 Feb 2017 08:02:27 -0800 Subject: [PATCH 097/131] Fix document bug in strict-boolean-expressions and the contributing doc (#2171) --- docs/develop/contributing/index.md | 2 +- src/rules/strictBooleanExpressionsRule.ts | 29 ++++++++++++----------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/docs/develop/contributing/index.md b/docs/develop/contributing/index.md index 6983f7cef14..053376e13a7 100644 --- a/docs/develop/contributing/index.md +++ b/docs/develop/contributing/index.md @@ -41,4 +41,4 @@ The current debug configurations are: - Debug CLI: Used to debug TSLint using command line arguments. Modify the `args` array in `.vscode/launch.json` to add arguments. - Debug Mocha Tests: Runs non-rule tests - Debug Rule Tests: Runs rule tests (under `test/rules`) -- Deubg Document Generation: Debug the `scripts/buildDocs.ts` script. +- Debug Document Generation: Debug the `scripts/buildDocs.ts` script. diff --git a/src/rules/strictBooleanExpressionsRule.ts b/src/rules/strictBooleanExpressionsRule.ts index e48e0a84ff0..3fc7d5a0ffc 100644 --- a/src/rules/strictBooleanExpressionsRule.ts +++ b/src/rules/strictBooleanExpressionsRule.ts @@ -34,25 +34,26 @@ export class Rule extends Lint.Rules.TypedRule { Restricts the types allowed in boolean expressions. By default only booleans are allowed. The following nodes are checked: - * Arguments to the '!', '&&', and '||' operators - * The condition in a conditional expression ('cond ? x : y') - * Conditions for 'if', 'for', 'while', and 'do-while' statements.`, + + * Arguments to the \`!\`, \`&&\`, and \`||\` operators + * The condition in a conditional expression (\`cond ? x : y\`) + * Conditions for \`if\`, \`for\`, \`while\`, and \`do-while\` statements.`, optionsDescription: Lint.Utils.dedent` These options may be provided: - * '${OPTION_ALLOW_NULL_UNION} allows union types containing 'null'. - - It does *not* allow 'null' itself. - * '${OPTION_ALLOW_UNDEFINED_UNION} allows union types containing 'undefined'. - - It does *not* allow 'undefined' itself. - * '${OPTION_ALLOW_STRING} allows strings. - - It does *not* allow unions containing 'string'. + * \`${OPTION_ALLOW_NULL_UNION}\` allows union types containing \`null\`. + - It does *not* allow \`null\` itself. + * \`${OPTION_ALLOW_UNDEFINED_UNION}\` allows union types containing \`undefined\`. + - It does *not* allow \`undefined\` itself. + * \`${OPTION_ALLOW_STRING}\` allows strings. + - It does *not* allow unions containing \`string\`. - It does *not* allow string literal types. - * '${OPTION_ALLOW_NUMBER} allows numbers. - - It does *not* allow unions containing 'number'. + * \`${OPTION_ALLOW_NUMBER}\` allows numbers. + - It does *not* allow unions containing \`number\`. - It does *not* allow enums or number literal types. - * '${OPTION_ALLOW_MIX} allow multiple of the above to appear together. - - For example, 'string | number' or 'RegExp | null | undefined' would normally not be allowed. - - A type like '"foo" | "bar" | undefined' is always allowed, because it has only one way to be false.`, + * \`${OPTION_ALLOW_MIX}\` allow multiple of the above to appear together. + - For example, \`string | number\` or \`RegExp | null | undefined\` would normally not be allowed. + - A type like \`"foo" | "bar" | undefined\` is always allowed, because it has only one way to be false.`, options: { type: "array", items: { From 66c7de8a7a8d1c928b3bcb4461d703491f5a793f Mon Sep 17 00:00:00 2001 From: Klaus Meinhardt Date: Tue, 7 Feb 2017 17:41:06 +0100 Subject: [PATCH 098/131] Add ignore-jsdoc to no-trailing-whitespace (#2177) --- src/rules/noTrailingWhitespaceRule.ts | 116 ++++++++++-------- .../default/test.ts.fix | 10 ++ .../default/test.ts.lint | 14 +++ .../ignore-comments/test.ts.fix | 10 ++ .../ignore-comments/test.ts.lint | 10 ++ .../ignore-jsdoc/test.ts.fix | 30 +++++ .../ignore-jsdoc/test.ts.lint | 42 +++++++ .../ignore-jsdoc/tslint.json | 5 + 8 files changed, 186 insertions(+), 51 deletions(-) create mode 100644 test/rules/no-trailing-whitespace/ignore-jsdoc/test.ts.fix create mode 100644 test/rules/no-trailing-whitespace/ignore-jsdoc/test.ts.lint create mode 100644 test/rules/no-trailing-whitespace/ignore-jsdoc/tslint.json diff --git a/src/rules/noTrailingWhitespaceRule.ts b/src/rules/noTrailingWhitespaceRule.ts index ef6a2578fb5..a6b80204bcf 100644 --- a/src/rules/noTrailingWhitespaceRule.ts +++ b/src/rules/noTrailingWhitespaceRule.ts @@ -20,6 +20,13 @@ import * as ts from "typescript"; import * as Lint from "../index"; const OPTION_IGNORE_COMMENTS = "ignore-comments"; +const OPTION_IGNORE_JSDOC = "ignore-jsdoc"; + +const enum IgnoreOption { + None, + Comments, + JsDoc, +} export class Rule extends Lint.Rules.AbstractRule { /* tslint:disable:object-literal-sort-keys */ @@ -27,18 +34,23 @@ export class Rule extends Lint.Rules.AbstractRule { ruleName: "no-trailing-whitespace", description: "Disallows trailing whitespace at the end of a line.", rationale: "Keeps version control diffs clean as it prevents accidental whitespace from being committed.", - optionsDescription: "Not configurable.", + optionsDescription: Lint.Utils.dedent` + Possible settings are: + + * \`"${OPTION_IGNORE_COMMENTS}"\`: Allows trailing whitespace in comments. + * \`"${OPTION_IGNORE_JSDOC}"\`: Allows trailing whitespace only in JSDoc comments.`, hasFix: true, options: { type: "array", items: { type: "string", - enum: [OPTION_IGNORE_COMMENTS], + enum: [OPTION_IGNORE_COMMENTS, OPTION_IGNORE_JSDOC], }, }, optionExamples: [ "true", `[true, "${OPTION_IGNORE_COMMENTS}"]`, + `[true, "${OPTION_IGNORE_JSDOC}"]`, ], type: "style", typescriptOnly: false, @@ -48,62 +60,64 @@ export class Rule extends Lint.Rules.AbstractRule { public static FAILURE_STRING = "trailing whitespace"; public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { - return this.applyWithWalker(new NoTrailingWhitespaceWalker(sourceFile, this.getOptions())); + let option = IgnoreOption.None; + if (this.ruleArguments.indexOf(OPTION_IGNORE_COMMENTS) !== -1) { + option = IgnoreOption.Comments; + } else if (this.ruleArguments.indexOf(OPTION_IGNORE_JSDOC) !== -1) { + option = IgnoreOption.JsDoc; + } + return this.applyWithFunction(sourceFile, walk, option); } } -class NoTrailingWhitespaceWalker extends Lint.RuleWalker { - public visitSourceFile(node: ts.SourceFile) { - let lastSeenWasWhitespace = false; - let lastSeenWhitespacePosition = 0; - Lint.forEachToken(node, false, (fullText, kind, pos) => { - if (kind === ts.SyntaxKind.NewLineTrivia || kind === ts.SyntaxKind.EndOfFileToken) { - if (lastSeenWasWhitespace) { - this.reportFailure(lastSeenWhitespacePosition, pos.tokenStart); - } - lastSeenWasWhitespace = false; - } else if (kind === ts.SyntaxKind.WhitespaceTrivia) { - lastSeenWasWhitespace = true; - lastSeenWhitespacePosition = pos.tokenStart; - } else { - if (!this.hasOption(OPTION_IGNORE_COMMENTS)) { - if (kind === ts.SyntaxKind.SingleLineCommentTrivia) { - const commentText = fullText.substring(pos.tokenStart + 2, pos.end); - const match = /\s+$/.exec(commentText); - if (match !== null) { - this.reportFailure(pos.end - match[0].length, pos.end); +function walk(ctx: Lint.WalkContext) { + let lastSeenWasWhitespace = false; + let lastSeenWhitespacePosition = 0; + Lint.forEachToken(ctx.sourceFile, false, (fullText, kind, pos) => { + if (kind === ts.SyntaxKind.NewLineTrivia || kind === ts.SyntaxKind.EndOfFileToken) { + if (lastSeenWasWhitespace) { + reportFailure(ctx, lastSeenWhitespacePosition, pos.tokenStart); + } + lastSeenWasWhitespace = false; + } else if (kind === ts.SyntaxKind.WhitespaceTrivia) { + lastSeenWasWhitespace = true; + lastSeenWhitespacePosition = pos.tokenStart; + } else { + if (ctx.options !== IgnoreOption.Comments) { + if (kind === ts.SyntaxKind.SingleLineCommentTrivia) { + const commentText = fullText.substring(pos.tokenStart + 2, pos.end); + const match = /\s+$/.exec(commentText); + if (match !== null) { + reportFailure(ctx, pos.end - match[0].length, pos.end); + } + } else if (kind === ts.SyntaxKind.MultiLineCommentTrivia && + (ctx.options !== IgnoreOption.JsDoc || + fullText[pos.tokenStart + 2] !== "*" || + fullText[pos.tokenStart + 3] === "*")) { + let startPos = pos.tokenStart + 2; + const commentText = fullText.substring(startPos, pos.end - 2); + const lines = commentText.split("\n"); + // we don't want to check the content of the last comment line, as it is always followed by */ + const len = lines.length - 1; + for (let i = 0; i < len; ++i) { + let line = lines[i]; + // remove carriage return at the end, it is does not account to trailing whitespace + if (line.endsWith("\r")) { + line = line.substr(0, line.length - 1); } - } else if (kind === ts.SyntaxKind.MultiLineCommentTrivia) { - let startPos = pos.tokenStart + 2; - const commentText = fullText.substring(startPos, pos.end - 2); - const lines = commentText.split("\n"); - // we don't want to check the content of the last comment line, as it is always followed by */ - const len = lines.length - 1; - for (let i = 0; i < len; ++i) { - let line = lines[i]; - // remove carriage return at the end, it is does not account to trailing whitespace - if (line.endsWith("\r")) { - line = line.substr(0, line.length - 1); - } - const start = line.search(/\s+$/); - if (start !== -1) { - this.reportFailure(startPos + start, startPos + line.length); - } - startPos += lines[i].length + 1; + const start = line.search(/\s+$/); + if (start !== -1) { + reportFailure(ctx, startPos + start, startPos + line.length); } + startPos += lines[i].length + 1; } } - lastSeenWasWhitespace = false; } - }); - } + lastSeenWasWhitespace = false; + } + }); +} - private reportFailure(start: number, end: number) { - this.addFailureFromStartToEnd( - start, - end, - Rule.FAILURE_STRING, - this.createFix(this.deleteText(start, end - start)), - ); - } +function reportFailure(ctx: Lint.WalkContext, start: number, end: number) { + ctx.addFailure(start, end, Rule.FAILURE_STRING, ctx.createFix(Lint.Replacement.deleteFromTo(start, end))); } diff --git a/test/rules/no-trailing-whitespace/default/test.ts.fix b/test/rules/no-trailing-whitespace/default/test.ts.fix index 9ca5d289dde..35e9e07a9eb 100644 --- a/test/rules/no-trailing-whitespace/default/test.ts.fix +++ b/test/rules/no-trailing-whitespace/default/test.ts.fix @@ -17,4 +17,14 @@ class Clazz { /* */ +/** + * JSDoc comment with trailing whitespace + * + */ + +/*** + * not a JSDoc comment + * + */ + // following line checks for trailing whitespace before EOF diff --git a/test/rules/no-trailing-whitespace/default/test.ts.lint b/test/rules/no-trailing-whitespace/default/test.ts.lint index 7067008dc5d..caad3526df9 100644 --- a/test/rules/no-trailing-whitespace/default/test.ts.lint +++ b/test/rules/no-trailing-whitespace/default/test.ts.lint @@ -25,6 +25,20 @@ class Clazz { /* */ +/** + * JSDoc comment with trailing whitespace + ~ [trailing whitespace] + * + ~ [trailing whitespace] + */ + +/*** + * not a JSDoc comment + ~ [trailing whitespace] + * + ~ [trailing whitespace] + */ + // following line checks for trailing whitespace before EOF ~~~ [trailing whitespace] \ No newline at end of file diff --git a/test/rules/no-trailing-whitespace/ignore-comments/test.ts.fix b/test/rules/no-trailing-whitespace/ignore-comments/test.ts.fix index 10912132870..18bfde2eeb4 100644 --- a/test/rules/no-trailing-whitespace/ignore-comments/test.ts.fix +++ b/test/rules/no-trailing-whitespace/ignore-comments/test.ts.fix @@ -17,4 +17,14 @@ class Clazz { /* */ +/** + * JSDoc comment with trailing whitespace + * + */ + +/*** + * not a JSDoc comment + * + */ + // following line checks for trailing whitespace before EOF diff --git a/test/rules/no-trailing-whitespace/ignore-comments/test.ts.lint b/test/rules/no-trailing-whitespace/ignore-comments/test.ts.lint index be0264853af..db700e685d1 100644 --- a/test/rules/no-trailing-whitespace/ignore-comments/test.ts.lint +++ b/test/rules/no-trailing-whitespace/ignore-comments/test.ts.lint @@ -23,6 +23,16 @@ class Clazz { /* */ +/** + * JSDoc comment with trailing whitespace + * + */ + +/*** + * not a JSDoc comment + * + */ + // following line checks for trailing whitespace before EOF ~~~ [trailing whitespace] \ No newline at end of file diff --git a/test/rules/no-trailing-whitespace/ignore-jsdoc/test.ts.fix b/test/rules/no-trailing-whitespace/ignore-jsdoc/test.ts.fix new file mode 100644 index 00000000000..dfd0fadad70 --- /dev/null +++ b/test/rules/no-trailing-whitespace/ignore-jsdoc/test.ts.fix @@ -0,0 +1,30 @@ +class Clazz { + public funcxion() { + console.log("test") ; + } + + + private foobar() { + } +} +// single line comment without trailing whitespace +// single line comment with trailing whitespace + /* single line multiline comment */ +/* whitespace after comment */ +/* first line has trailing whitespace + second line is ok + last line is not checked */ +/* + */ + +/** + * JSDoc comment with trailing whitespace + * + */ + +/*** + * not a JSDoc comment + * + */ + +// following line checks for trailing whitespace before EOF diff --git a/test/rules/no-trailing-whitespace/ignore-jsdoc/test.ts.lint b/test/rules/no-trailing-whitespace/ignore-jsdoc/test.ts.lint new file mode 100644 index 00000000000..0bf15bf9b69 --- /dev/null +++ b/test/rules/no-trailing-whitespace/ignore-jsdoc/test.ts.lint @@ -0,0 +1,42 @@ +class Clazz { + public funcxion() { + ~~~~ [trailing whitespace] + console.log("test") ; + ~~~~ [trailing whitespace] + } + +~~~~ [trailing whitespace] + +~~~~ [trailing whitespace] + private foobar() { + } +} + ~~~~ [trailing whitespace] +// single line comment without trailing whitespace +// single line comment with trailing whitespace + ~~~ [trailing whitespace] + /* single line multiline comment */ +/* whitespace after comment */ + ~ [trailing whitespace] +/* first line has trailing whitespace + ~~ [trailing whitespace] + second line is ok + last line is not checked */ +/* + */ + +/** + * JSDoc comment with trailing whitespace + * + */ + +/*** + * not a JSDoc comment + ~ [trailing whitespace] + * + ~ [trailing whitespace] + */ + +// following line checks for trailing whitespace before EOF + +~~~ [trailing whitespace] \ No newline at end of file diff --git a/test/rules/no-trailing-whitespace/ignore-jsdoc/tslint.json b/test/rules/no-trailing-whitespace/ignore-jsdoc/tslint.json new file mode 100644 index 00000000000..fc15bef2af0 --- /dev/null +++ b/test/rules/no-trailing-whitespace/ignore-jsdoc/tslint.json @@ -0,0 +1,5 @@ +{ + "rules": { + "no-trailing-whitespace": [true, "ignore-jsdoc"] + } +} From 6cb93697af2e2dbbe8eaa9c98ce96dd0804b4a1c Mon Sep 17 00:00:00 2001 From: Caleb Eggensperger Date: Tue, 7 Feb 2017 15:55:39 -0500 Subject: [PATCH 099/131] Add a rule to ban specified types from being used in type annotations or type assertions. (#2175) --- src/rules/banTypesRule.ts | 77 +++++++++++++++++++++++++++++++ test/rules/ban-types/test.ts.lint | 20 ++++++++ test/rules/ban-types/tslint.json | 10 ++++ 3 files changed, 107 insertions(+) create mode 100644 src/rules/banTypesRule.ts create mode 100644 test/rules/ban-types/test.ts.lint create mode 100644 test/rules/ban-types/tslint.json diff --git a/src/rules/banTypesRule.ts b/src/rules/banTypesRule.ts new file mode 100644 index 00000000000..bab1c710edf --- /dev/null +++ b/src/rules/banTypesRule.ts @@ -0,0 +1,77 @@ +/** + * @license + * Copyright 2017 Palantir Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as ts from "typescript"; + +import * as Lint from "../index"; + +export class Rule extends Lint.Rules.AbstractRule { + /* tslint:disable:object-literal-sort-keys */ + public static metadata: Lint.IRuleMetadata = { + ruleName: "ban-types", + description: Lint.Utils.dedent` + Bans specific types from being used. Does not ban the + corresponding runtime objects from being used.`, + options: { + type: "list", + listType: { + type: "array", + items: { type: "string" }, + minLength: 1, + maxLength: 2, + }, + }, + optionsDescription: Lint.Utils.dedent` + A list of \`["regex", "optional explanation here"]\`, which bans + types that match \`regex\``, + optionExamples: [`[true, ["Object", "Use {} instead."], ["String"]]`], + type: "typescript", + typescriptOnly: true, + }; + /* tslint:enable:object-literal-sort-keys */ + + public static FAILURE_STRING_FACTORY(typeName: string, messageAddition?: string) { + return `Don't use '${typeName}' as a type.` + + (messageAddition ? " " + messageAddition : ""); + } + + public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { + return this.applyWithWalker( + new BanTypeWalker(sourceFile, this.getOptions())); + } +} + +class BanTypeWalker extends Lint.RuleWalker { + private bans: string[][]; + constructor(sourceFile: ts.SourceFile, options: Lint.IOptions) { + super(sourceFile, options); + this.bans = options.ruleArguments!; + } + + public visitTypeReference(node: ts.TypeReferenceNode) { + const typeName = node.typeName.getText(); + const ban = + this.bans.find(([bannedType]) => + typeName.match(`^${bannedType}$`) != null) as string[]; + if (ban) { + this.addFailure(this.createFailure( + node.getStart(), node.getWidth(), + Rule.FAILURE_STRING_FACTORY(typeName, ban[1]))); + } + super.visitTypeReference(node); + } +} diff --git a/test/rules/ban-types/test.ts.lint b/test/rules/ban-types/test.ts.lint new file mode 100644 index 00000000000..4e813ab2ccd --- /dev/null +++ b/test/rules/ban-types/test.ts.lint @@ -0,0 +1,20 @@ +let a: Object; + ~~~~~~ [Don't use 'Object' as a type.] +let b: {c: String}; + ~~~~~~ [Don't use 'String' as a type. Use 'string' instead.] +function foo(a: String) {} + ~~~~~~ [Don't use 'String' as a type. Use 'string' instead.] +'a' as String; + ~~~~~~ [Don't use 'String' as a type. Use 'string' instead.] +let c: F; + ~ [Don't use 'F' as a type.] +let d: Foooooo; + ~~~~~~~ [Don't use 'Foooooo' as a type.] + +// no warning for separately scoped types +let e: foo.String; + +// no warning for actual uses. +let f = Object(); +let g = Object.create(null); +let h = String(false); \ No newline at end of file diff --git a/test/rules/ban-types/tslint.json b/test/rules/ban-types/tslint.json new file mode 100644 index 00000000000..738570950f6 --- /dev/null +++ b/test/rules/ban-types/tslint.json @@ -0,0 +1,10 @@ +{ + "rules": { + "ban-types": [ + true, + ["String", "Use 'string' instead."], + ["Object"], + ["Fo*"] + ] + } +} From 44f74e9a54e9e5430dcf8ea7b82e9c14dbc900e5 Mon Sep 17 00:00:00 2001 From: Arturs Vonda Date: Tue, 7 Feb 2017 23:00:35 +0200 Subject: [PATCH 100/131] New Rule: newline-before-return (#2173) --- src/rules/newlineBeforeReturnRule.ts | 81 +++++++++++++++++ .../default/test.ts.lint | 90 +++++++++++++++++++ .../default/test.tsx.lint | 18 ++++ .../newline-before-return/default/tslint.json | 5 ++ 4 files changed, 194 insertions(+) create mode 100644 src/rules/newlineBeforeReturnRule.ts create mode 100644 test/rules/newline-before-return/default/test.ts.lint create mode 100644 test/rules/newline-before-return/default/test.tsx.lint create mode 100644 test/rules/newline-before-return/default/tslint.json diff --git a/src/rules/newlineBeforeReturnRule.ts b/src/rules/newlineBeforeReturnRule.ts new file mode 100644 index 00000000000..3465652c6bf --- /dev/null +++ b/src/rules/newlineBeforeReturnRule.ts @@ -0,0 +1,81 @@ +/** + * @license + * Copyright 2017 Palantir Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as ts from "typescript"; +import * as Lint from "../index"; + +export class Rule extends Lint.Rules.AbstractRule { + /* tslint:disable:object-literal-sort-keys */ + public static metadata: Lint.IRuleMetadata = { + ruleName: "newline-before-return", + description: "Enforces blank line before return when not the only line in the block.", + rationale: "Helps maintain a readable style in your codebase.", + optionsDescription: "Not configurable.", + options: {}, + optionExamples: ["true"], + type: "style", + typescriptOnly: false, + }; + /* tslint:enable:object-literal-sort-keys */ + + public static FAILURE_STRING_FACTORY() { + return "Missing blank line before return"; + }; + + public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { + return this.applyWithWalker(new NewlineBeforeReturnWalker(sourceFile, this.getOptions())); + } +} + +class NewlineBeforeReturnWalker extends Lint.RuleWalker { + + public visitReturnStatement(node: ts.ReturnStatement) { + super.visitReturnStatement(node); + + const parent = node.parent!; + if (!isBlockLike(parent)) { + // `node` is the only statement within this "block scope". No need to do any further validation. + return; + } + + const index = parent.statements.indexOf(node as ts.Statement); + if (index === 0) { + // First element in the block so no need to check for the blank line. + return; + } + + let start = node.getStart(); + const comments = ts.getLeadingCommentRanges(this.getSourceFile().text, node.getFullStart()); + if (comments) { + // In case the return statement is preceded by a comment, we use comments start as the starting position + start = comments[0].pos; + } + const lc = this.getLineAndCharacterOfPosition(start); + + const prev = parent.statements[index - 1]; + const prevLine = this.getLineAndCharacterOfPosition(prev.getEnd()).line; + + if (prevLine >= lc.line - 1) { + // Previous statement is on the same or previous line + this.addFailureFromStartToEnd(start, start, Rule.FAILURE_STRING_FACTORY()); + } + } +} + +function isBlockLike(node: ts.Node): node is ts.BlockLike { + return "statements" in node; +} diff --git a/test/rules/newline-before-return/default/test.ts.lint b/test/rules/newline-before-return/default/test.ts.lint new file mode 100644 index 00000000000..fd406dfe7aa --- /dev/null +++ b/test/rules/newline-before-return/default/test.ts.lint @@ -0,0 +1,90 @@ +function foo(bar) { + if (!bar) { + return; + } + return bar; + ~nil [0] +} + +function foo(bar) { + if (!bar) { + var statement = ''; + return statement; + ~nil [0] + } + + return bar; +} + +function foo(bar) { + if (!bar) { + return; + } + /* multi-line + ~nil [0] + comment */ + return bar; +} + +var fn = () => null; +function foo() { + fn(); + return; + ~nil [0] +} + +function foo(fn) { + fn(); return; + ~nil [0] +} + +function foo() { + return; +} + +function foo() { + + return; +} + +function foo(bar) { + if (!bar) return; +} + +function foo(bar) { + let someCall; + if (!bar) return; +} + +function foo(bar) { + if (!bar) { return }; +} + +function foo(bar) { + if (!bar) { + return; + } +} + +function foo(bar) { + if (!bar) { + return; + } + + return bar; +} + +function foo(bar) { + if (!bar) { + + return; + } +} + +function foo() { + + // comment + return; +} + +[0]: Missing blank line before return diff --git a/test/rules/newline-before-return/default/test.tsx.lint b/test/rules/newline-before-return/default/test.tsx.lint new file mode 100644 index 00000000000..ae18dcfb7d0 --- /dev/null +++ b/test/rules/newline-before-return/default/test.tsx.lint @@ -0,0 +1,18 @@ +import * as React from 'react'; + +
{ [].map((child: any) => { + let i = 0; + return ; + ~nil [0] +}) }
+ +
{ [].map((child: any) => { + return ; +}) }
+ +
{ [].map((child: any) => + ; +) }
+ + +[0]: Missing blank line before return diff --git a/test/rules/newline-before-return/default/tslint.json b/test/rules/newline-before-return/default/tslint.json new file mode 100644 index 00000000000..fdefd1ad13b --- /dev/null +++ b/test/rules/newline-before-return/default/tslint.json @@ -0,0 +1,5 @@ +{ + "rules": { + "newline-before-return": true + } +} From 0def9f76ca4d3f7c7256cd35f88f28ec0a19a973 Mon Sep 17 00:00:00 2001 From: Klaus Meinhardt Date: Tue, 7 Feb 2017 22:19:54 +0100 Subject: [PATCH 101/131] Add ignore-properties to no-inferrable-types (#2178) --- src/rules/noInferrableTypesRule.ts | 71 ++++++++++++------- .../no-inferrable-types/default/test.ts.fix | 31 ++++++++ .../no-inferrable-types/default/test.ts.lint | 9 +++ .../ignore-params/test.ts.fix | 32 +++++++++ .../ignore-params/test.ts.lint | 9 +++ .../ignore-properties/test.ts.fix | 31 ++++++++ .../ignore-properties/test.ts.lint | 40 +++++++++++ .../ignore-properties/tslint.json | 5 ++ 8 files changed, 204 insertions(+), 24 deletions(-) create mode 100644 test/rules/no-inferrable-types/default/test.ts.fix create mode 100644 test/rules/no-inferrable-types/ignore-params/test.ts.fix create mode 100644 test/rules/no-inferrable-types/ignore-properties/test.ts.fix create mode 100644 test/rules/no-inferrable-types/ignore-properties/test.ts.lint create mode 100644 test/rules/no-inferrable-types/ignore-properties/tslint.json diff --git a/src/rules/noInferrableTypesRule.ts b/src/rules/noInferrableTypesRule.ts index 1a5c8c6b32c..feb7beca17e 100644 --- a/src/rules/noInferrableTypesRule.ts +++ b/src/rules/noInferrableTypesRule.ts @@ -20,6 +20,12 @@ import * as ts from "typescript"; import * as Lint from "../index"; const OPTION_IGNORE_PARMS = "ignore-params"; +const OPTION_IGNORE_PROPERTIES = "ignore-properties"; + +interface IOptions { + ignoreParameters: boolean; + ignoreProperties: boolean; +} export class Rule extends Lint.Rules.AbstractRule { /* tslint:disable:object-literal-sort-keys */ @@ -28,20 +34,26 @@ export class Rule extends Lint.Rules.AbstractRule { description: "Disallows explicit type declarations for variables or parameters initialized to a number, string, or boolean.", rationale: "Explicit types where they can be easily infered by the compiler make code more verbose.", optionsDescription: Lint.Utils.dedent` - One argument may be optionally provided: + Two argument may be optionally provided: * \`${OPTION_IGNORE_PARMS}\` allows specifying an inferrable type annotation for function params. - This can be useful when combining with the \`typedef\` rule.`, + This can be useful when combining with the \`typedef\` rule. + * \`${OPTION_IGNORE_PROPERTIES}\` allows specifying an inferrable type annotation for class properties.`, options: { type: "array", items: { type: "string", - enum: [OPTION_IGNORE_PARMS], + enum: [OPTION_IGNORE_PARMS, OPTION_IGNORE_PROPERTIES], }, minLength: 0, - maxLength: 1, + maxLength: 2, }, - optionExamples: ["true", `[true, "${OPTION_IGNORE_PARMS}"]`], + hasFix: true, + optionExamples: [ + "true", + `[true, "${OPTION_IGNORE_PARMS}"]`, + `[true, "${OPTION_IGNORE_PARMS}", "${OPTION_IGNORE_PROPERTIES}"]`, + ], type: "typescript", typescriptOnly: true, }; @@ -52,29 +64,37 @@ export class Rule extends Lint.Rules.AbstractRule { } public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { - return this.applyWithWalker(new NoInferrableTypesWalker(sourceFile, this.getOptions())); + return this.applyWithWalker(new NoInferrableTypesWalker(sourceFile, this.ruleName, { + ignoreParameters: this.ruleArguments.indexOf(OPTION_IGNORE_PARMS) !== -1, + ignoreProperties: this.ruleArguments.indexOf(OPTION_IGNORE_PROPERTIES) !== -1, + })); } } -class NoInferrableTypesWalker extends Lint.RuleWalker { - public visitVariableDeclaration(node: ts.VariableDeclaration) { - this.checkDeclaration(node); - super.visitVariableDeclaration(node); - } - - public visitParameterDeclaration(node: ts.ParameterDeclaration) { - if (!this.hasOption(OPTION_IGNORE_PARMS)) { - this.checkDeclaration(node); - } - super.visitParameterDeclaration(node); - } - - public visitPropertyDeclaration(node: ts.PropertyDeclaration) { - this.checkDeclaration(node); - super.visitPropertyDeclaration(node); +class NoInferrableTypesWalker extends Lint.AbstractWalker { + public walk(sourceFile: ts.SourceFile) { + const cb = (node: ts.Node): void => { + switch (node.kind) { + case ts.SyntaxKind.Parameter: + if (!this.options.ignoreParameters) { + this.checkDeclaration(node as ts.ParameterDeclaration); + } + break; + case ts.SyntaxKind.PropertyDeclaration: + if (this.options.ignoreProperties) { + break; + } + /* falls through*/ + case ts.SyntaxKind.VariableDeclaration: + this.checkDeclaration(node as ts.VariableLikeDeclaration); + default: + } + return ts.forEachChild(node, cb); + }; + return ts.forEachChild(sourceFile, cb); } - private checkDeclaration(node: ts.ParameterDeclaration | ts.VariableDeclaration | ts.PropertyDeclaration) { + private checkDeclaration(node: ts.VariableLikeDeclaration) { if (node.type != null && node.initializer != null) { let failure: string | null = null; @@ -105,7 +125,10 @@ class NoInferrableTypesWalker extends Lint.RuleWalker { } if (failure != null) { - this.addFailureAtNode(node.type, Rule.FAILURE_STRING_FACTORY(failure)); + this.addFailureAtNode(node.type, + Rule.FAILURE_STRING_FACTORY(failure), + this.createFix(Lint.Replacement.deleteFromTo(node.name.end, node.type.end)), + ); } } } diff --git a/test/rules/no-inferrable-types/default/test.ts.fix b/test/rules/no-inferrable-types/default/test.ts.fix new file mode 100644 index 00000000000..651ee3f0a4a --- /dev/null +++ b/test/rules/no-inferrable-types/default/test.ts.fix @@ -0,0 +1,31 @@ +// errors, inferrable type is declared +let x = 7; +let y = false; +let z = "foo"; +class C { + x = 1; +} + +// errors, types are inferrable +function foo (a = 5, b = true, c = "bah") { } + +class Foo { + bar = 0; + baz = true, + bas = "moar"; +} + +// not errors, inferrable type is not declared +let _x = 7; +let _y = false; +let _z = "foo"; + +// not error, type is not inferrable +let weird: any = 123; + +// not errors, inferrable type is not declared +function bar(a = 5, b = true, c = "bah") { } + +// not errors, types are not inferrable +function baz(a: any = 5, b: any = true, c: any = "bah") { } + diff --git a/test/rules/no-inferrable-types/default/test.ts.lint b/test/rules/no-inferrable-types/default/test.ts.lint index 22dbc0281df..73583fe726f 100644 --- a/test/rules/no-inferrable-types/default/test.ts.lint +++ b/test/rules/no-inferrable-types/default/test.ts.lint @@ -16,6 +16,15 @@ function foo (a: number = 5, b: boolean = true, c: string = "bah") { } ~~~~~~~ [boolean] ~~~~~~ [string] +class Foo { + bar: number = 0; + ~~~~~~ [number] + baz: boolean = true, + ~~~~~~~ [boolean] + bas: string = "moar"; + ~~~~~~ [string] +} + // not errors, inferrable type is not declared let _x = 7; let _y = false; diff --git a/test/rules/no-inferrable-types/ignore-params/test.ts.fix b/test/rules/no-inferrable-types/ignore-params/test.ts.fix new file mode 100644 index 00000000000..d662f26846e --- /dev/null +++ b/test/rules/no-inferrable-types/ignore-params/test.ts.fix @@ -0,0 +1,32 @@ +// errors, inferrable type is declared +let x = 7; +let y = false; +let z = "foo"; + +// not errors, we are skipping params +function foo (a: number = 5, b: boolean = true, c: string = "bah") { } + +class TestClass { + doSomething(a: number = 5, b: boolean = true, c: string = "bah") {} +} + +class Foo { + bar = 0; + baz = true, + bas = "moar"; +} + +// not errors, inferrable type is not declared +let _x = 7; +let _y = false; +let _z = "foo"; + +// not error, type is not inferrable +let weird: any = 123; + +// not errors, inferrable type is not declared +function bar(a = 5, b = true, c = "bah") { } + +// not errors, types are not inferrable +function baz(a: any = 5, b: any = true, c: any = "bah") { } + diff --git a/test/rules/no-inferrable-types/ignore-params/test.ts.lint b/test/rules/no-inferrable-types/ignore-params/test.ts.lint index 3442fd4ebb8..17923fc85fb 100644 --- a/test/rules/no-inferrable-types/ignore-params/test.ts.lint +++ b/test/rules/no-inferrable-types/ignore-params/test.ts.lint @@ -13,6 +13,15 @@ class TestClass { doSomething(a: number = 5, b: boolean = true, c: string = "bah") {} } +class Foo { + bar: number = 0; + ~~~~~~ [number] + baz: boolean = true, + ~~~~~~~ [boolean] + bas: string = "moar"; + ~~~~~~ [string] +} + // not errors, inferrable type is not declared let _x = 7; let _y = false; diff --git a/test/rules/no-inferrable-types/ignore-properties/test.ts.fix b/test/rules/no-inferrable-types/ignore-properties/test.ts.fix new file mode 100644 index 00000000000..4b5631c1597 --- /dev/null +++ b/test/rules/no-inferrable-types/ignore-properties/test.ts.fix @@ -0,0 +1,31 @@ +// errors, inferrable type is declared +let x = 7; +let y = false; +let z = "foo"; +class C { + x: number = 1; +} + +// errors, types are inferrable +function foo (a = 5, b = true, c = "bah") { } + +class Foo { + bar: number = 0; + baz: boolean = true, + bas: string = "moar"; +} + +// not errors, inferrable type is not declared +let _x = 7; +let _y = false; +let _z = "foo"; + +// not error, type is not inferrable +let weird: any = 123; + +// not errors, inferrable type is not declared +function bar(a = 5, b = true, c = "bah") { } + +// not errors, types are not inferrable +function baz(a: any = 5, b: any = true, c: any = "bah") { } + diff --git a/test/rules/no-inferrable-types/ignore-properties/test.ts.lint b/test/rules/no-inferrable-types/ignore-properties/test.ts.lint new file mode 100644 index 00000000000..08028559107 --- /dev/null +++ b/test/rules/no-inferrable-types/ignore-properties/test.ts.lint @@ -0,0 +1,40 @@ +// errors, inferrable type is declared +let x: number = 7; + ~~~~~~ [number] +let y: boolean = false; + ~~~~~~~ [boolean] +let z: string = "foo"; + ~~~~~~ [string] +class C { + x: number = 1; +} + +// errors, types are inferrable +function foo (a: number = 5, b: boolean = true, c: string = "bah") { } + ~~~~~~ [number] + ~~~~~~~ [boolean] + ~~~~~~ [string] + +class Foo { + bar: number = 0; + baz: boolean = true, + bas: string = "moar"; +} + +// not errors, inferrable type is not declared +let _x = 7; +let _y = false; +let _z = "foo"; + +// not error, type is not inferrable +let weird: any = 123; + +// not errors, inferrable type is not declared +function bar(a = 5, b = true, c = "bah") { } + +// not errors, types are not inferrable +function baz(a: any = 5, b: any = true, c: any = "bah") { } + +[number]: Type number trivially inferred from a number literal, remove type annotation +[boolean]: Type boolean trivially inferred from a boolean literal, remove type annotation +[string]: Type string trivially inferred from a string literal, remove type annotation diff --git a/test/rules/no-inferrable-types/ignore-properties/tslint.json b/test/rules/no-inferrable-types/ignore-properties/tslint.json new file mode 100644 index 00000000000..2d9fd532f2c --- /dev/null +++ b/test/rules/no-inferrable-types/ignore-properties/tslint.json @@ -0,0 +1,5 @@ +{ + "rules": { + "no-inferrable-types": [true, "ignore-properties"] + } +} From 02dd75e0febb82f491cc6551391083798a85798d Mon Sep 17 00:00:00 2001 From: Noah Chen Date: Tue, 7 Feb 2017 20:48:10 -0500 Subject: [PATCH 102/131] Don't die when running a rule that requires type checking but doesn't have it (#2188) --- src/error.ts | 9 +++++++++ src/language/rule/abstractRule.ts | 2 +- src/linter.ts | 9 +++++++-- src/ruleLoader.ts | 7 +++---- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/error.ts b/src/error.ts index f18aacd485c..5f83ca43f37 100644 --- a/src/error.ts +++ b/src/error.ts @@ -15,6 +15,8 @@ * limitations under the License. */ +const shownWarnings = new Set(); + /** * Generic error typing for EcmaScript errors * Define `Error` here to avoid using `Error` from @types/node. @@ -38,3 +40,10 @@ export class FatalError extends Error { this.stack = new Error().stack; } } + +export function showWarningOnce(message: string) { + if (!shownWarnings.has(message)) { + console.warn(message); + shownWarnings.add(message); + } +} diff --git a/src/language/rule/abstractRule.ts b/src/language/rule/abstractRule.ts index 7713fefd8ee..3a051a6ecbd 100644 --- a/src/language/rule/abstractRule.ts +++ b/src/language/rule/abstractRule.ts @@ -37,7 +37,7 @@ export abstract class AbstractRule implements IRule { return false; } - constructor(protected readonly ruleName: string, private value: any, private disabledIntervals: IDisabledInterval[]) { + constructor(public readonly ruleName: string, private value: any, private disabledIntervals: IDisabledInterval[]) { if (Array.isArray(value) && value.length > 1) { this.ruleArguments = value.slice(1); } else { diff --git a/src/linter.ts b/src/linter.ts index c6d76947c5d..66770812a25 100644 --- a/src/linter.ts +++ b/src/linter.ts @@ -29,6 +29,7 @@ import { loadConfigurationFromPath, } from "./configuration"; import { EnableDisableRulesWalker } from "./enableDisableRules"; +import { showWarningOnce } from "./error"; import { findFormatter } from "./formatterLoader"; import { ILinterOptions, LintResult } from "./index"; import { IFormatter } from "./language/formatter/formatter"; @@ -162,8 +163,12 @@ class Linter { private applyRule(rule: IRule, sourceFile: ts.SourceFile) { let ruleFailures: RuleFailure[] = []; - if (this.program && TypedRule.isTypedRule(rule)) { - ruleFailures = rule.applyWithProgram(sourceFile, this.languageService); + if (TypedRule.isTypedRule(rule)) { + if (this.program) { + ruleFailures = rule.applyWithProgram(sourceFile, this.languageService); + } else { + showWarningOnce(`Warning: The '${rule.ruleName}' rule requires type checking. This rule will be ignored.`); + } } else { ruleFailures = rule.apply(sourceFile, this.languageService); } diff --git a/src/ruleLoader.ts b/src/ruleLoader.ts index e7183e5547f..f95f728a499 100644 --- a/src/ruleLoader.ts +++ b/src/ruleLoader.ts @@ -19,13 +19,13 @@ import * as fs from "fs"; import * as path from "path"; import { getRelativePath } from "./configuration"; +import { showWarningOnce } from "./error"; import { AbstractRule } from "./language/rule/abstractRule"; import { IDisabledInterval, IRule } from "./language/rule/rule"; import { arrayify, camelize, dedent } from "./utils"; const moduleDirectory = path.dirname(module.filename); const CORE_RULES_DIRECTORY = path.resolve(moduleDirectory, ".", "rules"); -const shownDeprecations: string[] = []; const cachedRules = new Map(); // null indicates that the rule was not found export interface IEnableDisablePosition { @@ -56,9 +56,8 @@ export function loadRules(ruleConfiguration: {[name: string]: any}, const disabledIntervals = buildDisabledIntervalsFromSwitches(ruleSpecificList); rules.push(new (Rule as any)(ruleName, ruleValue, disabledIntervals)); - if (Rule.metadata && Rule.metadata.deprecationMessage && shownDeprecations.indexOf(Rule.metadata.ruleName) === -1) { - console.warn(`${Rule.metadata.ruleName} is deprecated. ${Rule.metadata.deprecationMessage}`); - shownDeprecations.push(Rule.metadata.ruleName); + if (Rule.metadata && Rule.metadata.deprecationMessage) { + showWarningOnce(`${Rule.metadata.ruleName} is deprecated. ${Rule.metadata.deprecationMessage}`); } } } From d68ba8ecf954783e4377e9bb62c9d8f186251bca Mon Sep 17 00:00:00 2001 From: Noah Chen Date: Wed, 8 Feb 2017 13:24:49 -0500 Subject: [PATCH 103/131] Handle rule implementing both apply and applyWithProgram (#2192) --- src/error.ts | 4 ++++ src/language/rule/typedRule.ts | 2 +- src/linter.ts | 17 +++++++++++------ 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/error.ts b/src/error.ts index 5f83ca43f37..0c8a3d2eebd 100644 --- a/src/error.ts +++ b/src/error.ts @@ -41,6 +41,10 @@ export class FatalError extends Error { } } +export function isError(possibleError: any): possibleError is Error { + return possibleError != null && possibleError.message !== undefined; +} + export function showWarningOnce(message: string) { if (!shownWarnings.has(message)) { console.warn(message); diff --git a/src/language/rule/typedRule.ts b/src/language/rule/typedRule.ts index 62cdaf9b516..0077b42272e 100644 --- a/src/language/rule/typedRule.ts +++ b/src/language/rule/typedRule.ts @@ -28,7 +28,7 @@ export abstract class TypedRule extends AbstractRule { public apply(): RuleFailure[] { // if no program is given to the linter, throw an error - throw new Error(`${this.ruleName} requires type checking`); + throw new Error(`The '${this.ruleName}' rule requires type checking`); } public abstract applyWithProgram(sourceFile: ts.SourceFile, languageService: ts.LanguageService): RuleFailure[]; diff --git a/src/linter.ts b/src/linter.ts index 66770812a25..d4b9cbb2294 100644 --- a/src/linter.ts +++ b/src/linter.ts @@ -29,7 +29,7 @@ import { loadConfigurationFromPath, } from "./configuration"; import { EnableDisableRulesWalker } from "./enableDisableRules"; -import { showWarningOnce } from "./error"; +import { isError, showWarningOnce } from "./error"; import { findFormatter } from "./formatterLoader"; import { ILinterOptions, LintResult } from "./index"; import { IFormatter } from "./language/formatter/formatter"; @@ -163,15 +163,20 @@ class Linter { private applyRule(rule: IRule, sourceFile: ts.SourceFile) { let ruleFailures: RuleFailure[] = []; - if (TypedRule.isTypedRule(rule)) { - if (this.program) { + try { + if (TypedRule.isTypedRule(rule) && this.program) { ruleFailures = rule.applyWithProgram(sourceFile, this.languageService); } else { - showWarningOnce(`Warning: The '${rule.ruleName}' rule requires type checking. This rule will be ignored.`); + ruleFailures = rule.apply(sourceFile, this.languageService); + } + } catch (error) { + if (isError(error)) { + showWarningOnce(`Warning: ${error.message}`); + } else { + console.warn(`Warning: ${error}`); } - } else { - ruleFailures = rule.apply(sourceFile, this.languageService); } + const fileFailures: RuleFailure[] = []; for (const ruleFailure of ruleFailures) { if (!this.containsRule(this.failures, ruleFailure)) { From bca26e4c79e7600bcd08f15f06ed3f45c5343247 Mon Sep 17 00:00:00 2001 From: Klaus Meinhardt Date: Thu, 9 Feb 2017 04:54:06 +0100 Subject: [PATCH 104/131] Add documentation for AbstractWalker and Performance best practices (#2179) --- docs/develop/custom-rules/migration.md | 124 ++++++++++++++++++++ docs/develop/custom-rules/performance.md | 139 +++++++++++++++++++++++ 2 files changed, 263 insertions(+) create mode 100644 docs/develop/custom-rules/migration.md create mode 100644 docs/develop/custom-rules/performance.md diff --git a/docs/develop/custom-rules/migration.md b/docs/develop/custom-rules/migration.md new file mode 100644 index 00000000000..2f8d19e1f7d --- /dev/null +++ b/docs/develop/custom-rules/migration.md @@ -0,0 +1,124 @@ +## Using `WalkContext` and `Rule#applyWithFunction` +If you have a rule with a pretty simple implementation, you don't need to declare a class which extends the `Walker` class. Instead, you can define a callback function that accepts following argument: + +- `ctx: WalkContext`: An object containing rule information, an object `options: T` containing the parsed rule arguments, the `ts.sourceFile` object, and functions for adding failures + +Use this callback as an argument to `applyWithFunction`. You can also pass your parsed rule arguments as optional 3rd parameter. + +Let's look at `no-null-keyword` as an example: +```ts +import * as ts from "typescript"; +import * as Lint from "tslint"; + +export class Rule extends Lint.Rules.AbstractRule { + public static FAILURE_STRING = "Use 'undefined' instead of 'null'"; + + public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { + // Call `applyWithFunction` with your callback function, `walk`. + // This creates a `WalkContext` and passes it in as an argument. + // An optional 3rd parameter allows you to pass in a parsed version of `this.ruleArguments`. If used, it is not recommended to + // simply pass in `this.getOptions()`, but to parse it into a more useful object instead. + return this.applyWithFunction(sourceFile, walk); + } +} + +// Here, the options object type is `void` because we don't pass any options in this example. +function walk(ctx: Lint.WalkContext) { + // Recursively walk the AST starting with root node, `ctx.sourceFile`. + // Call the function `cb` (defined below) for each child. + return ts.forEachChild(ctx.sourceFile, cb); + + function cb(node: ts.Node): void { + // Stop recursing further into the AST by returning early. Here, we ignore type nodes. + if (node.kind >= ts.SyntaxKind.FirstTypeNode && node.kind <= ts.SyntaxKind.LastTypeNode) { + return; + } + + // Add failures using the `WalkContext` object. Here, we add a failure if we find the null keyword. + if (node.kind === ts.SyntaxKind.NullKeyword) { + return ctx.addFailureAtNode(node, Rule.FAILURE_STRING); + } + + // Continue recursion into the AST by calling function `cb` for every child of the current node. + return ts.forEachChild(node, cb); + } +} +``` + +## Using `AbstractWalker` +If your rule implementation is a bit more involved than the above example, you can also implement it as a class. +Simply extend `AbstractWalker` and implement the `walk` method. + +```ts +import * as ts from "typescript"; +import * as Lint from "tslint"; + +export class Rule extends Lint.Rules.AbstractRule { + public static FAILURE_STRING = "'magic numbers' are not allowed"; + + public static ALLOWED_NODES = new Set([ + ... + ]); + + public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { + // We convert the `ruleArguments` into a useful format before passing it to the constructor of AbstractWalker. + return this.applyWithWalker(new NoMagicNumbersWalker(sourceFile, this.ruleName, new Set(this.ruleArguments.map(String)))); + } +} + +// The type parameter of AbstractWalker corresponds to the third constructor parameter. +class NoMagicNumbersWalker extends Lint.AbstractWalker> { + public walk(sourceFile: ts.SourceFile) { + const cb = (node: ts.Node): void => { + // Finds specific node types and do checking. + if (node.kind === ts.SyntaxKind.NumericLiteral) { + this.checkNumericLiteral(node, (node as ts.NumericLiteral).text); + } else if (node.kind === ts.SyntaxKind.PrefixUnaryExpression && + (node as ts.PrefixUnaryExpression).operator === ts.SyntaxKind.MinusToken) { + this.checkNumericLiteral(node, "-" + ((node as ts.PrefixUnaryExpression).operand as ts.NumericLiteral).text); + } else { + // Continue rescursion: call function `cb` for all children of the current node. + return ts.forEachChild(node, cb); + } + }; + + // Start recursion for all children of `sourceFile`. + return ts.forEachChild(sourceFile, cb); + } + + private checkNumericLiteral(node: ts.Node, num: string) { + // `this.options` is the third constructor parameter from above (the Set we created in `Rule.apply`) + if (!Rule.ALLOWED_NODES.has(node.parent!.kind) && !this.options.has(num)) { + // Add failures to the Walker. + this.addFailureAtNode(node, Rule.FAILURE_STRING); + } + } +} +``` + +## Migrating from `RuleWalker` to `AbstractWalker` +The main difference between `RuleWalker` and `AbstractWalker` is that you need to implement the AST recursion yourself. But why would you want to do that? +__Performance!__ `RuleWalker` wants to be "one walker to rule them all" (pun intended). It's easy to use but that convenience +makes it slow by default. When implementing the walking yourself, you only need to do as much work as needed. + +Besides that you *should* convert the `ruleArguments` to a useful format before passing it to `AbstractWalker` as seen above. + +This table describes the equivalent methods between the two classes: + +`RuleWalker` | `AbstractWalker` +------------ | -------------- +`this.createFailure()` and `this.addFailure()` | `this.addFailureAt()` +`this.addFailureFromStartToEnd()` | `this.addFailure()` +`this.createReplacement()` | `new Lint.Replacement()` +`this.deleteText()` | `Lint.Replacement.deleteText()` +`this.deleteFromTo()` | `Lint.Replacement.deleteFromTo()` +`this.appendText()` | `Lint.Replacement.appendText()` +`this.hasOption()` and `this.getOptions()` | use `this.options` directly +`this.getLineAndCharacterOfPosition()` | `ts.getLineAndCharacterOfPosition(this.sourceFile, ...)` +`this.getLimit()` | `this.sourceFile.end` +`this.getSourceFile()` | is available to be compatible, but prefer `this.sourceFile` +`this.getFailures()` | is available to be compatible, but prefer `this.failures` +`this.skip()` | just don't use it, it's a noop +`this.getRuleName()` | `this.ruleName` + + diff --git a/docs/develop/custom-rules/performance.md b/docs/develop/custom-rules/performance.md new file mode 100644 index 00000000000..6b17c5cab45 --- /dev/null +++ b/docs/develop/custom-rules/performance.md @@ -0,0 +1,139 @@ +## Performance tips + +### Don't call the LanguageService repeatedly +The LanguageService is designed to serve editors. By design it does as little work to serve requests as possible. +For most requests no cache is used. + +Let's say you need all usages of a variable. The LanguageService needs to check the whole AST subtree in which the variable is in scope. +Doing that once is barely noticable. But doing it over and over again, will result in pretty bad performance (looking at you `no-unused-variable`). + +### Use the TypeChecker only when needed +The TypeChecker is a really mighty tool, but that comes with a cost. To create a TypeChecker the Program first has to locate, read, parse and bind all SourceFiles referenced. +To avoid that cost, try to avoid the TypeChecker where possible. + +If you are interested in the JSDoc of a function for example, you *could* ask the TypeChecker. +But there's another way: call `.getChildren()` on the FunctionDeclaration and search for nodes of kind `ts.SyntaxKind.JSDocComment`. +Those nodes will precede other nodes in the array. + +### Avoid walking the AST if possible +Some rules work directly on the content of the source file. + +`max-file-line-count` and `linebreak-style` don't need to walk the AST at all. + +Other rules define exceptions: `no-consecutive-blank-lines` ignores template strings. +To optimize for the best case, this rule can first look for failures in the source. +If and only if there are any failures, walk the AST to find the location of all template strings to filter the failures. + +### Implement your own walking algorithm +Convenience comes with a price. When using `SyntaxWalker` or any subclass thereof like `RuleWalker` you pay the price for the +big switch statement in `visitNode` which then calls the appropriate `visitXXX` method for **every** node in the AST, even if you don't use them. + +Use `AbstractWalker` instead and implement the `walk` method to fit the needs of your rule. +It's as simple as this: +```ts +class MyWalker extends Lint.AbstractWalker { + public walk(sourceFile: ts.SourceFile) { + const cb = (node: ts.Node): void => { + if (someCondition) { + // do stuff + } + // Wondering why return is used below? Refer to "Make use of tail calls" + return ts.forEachChild(node, cb); // recurse deeper + }; + return ts.forEachChild(sourceFile, cb); // start recursion with children of sourceFile + } +``` + +### Don't walk the whole AST if possible +__The Spec is your friend:__ +The language spec defines where each statement can occur. If you are interested in `import` statements for example, you only need to search +in `sourceFile.statements` and nested `NamespaceDeclaration`s. + +__Don't visit AST branches you're not interested in:__ +For example `no-null-keyword` creates no failure if the null keyword is part of another type. +There are two ways to achieve this: + +* Recurse into the AST until you find a token of kind NullKeyword and then walk up its parent chain to find out if it is part of a type node +* Stop recursing deeper into that branch as soon as you hit a type node (preferred) + +### Avoid frequently creating one-time closures in the hot path +```ts +class SomeClass { + // this is a simplified version of what SyntaxWalker does under the hood + doStuff(node: ts.Node) { + // do stuff ... + + ts.forEachChild(node, (n) => this.doStuff(n)); + // ~~~~~~~~~~~~~~~~~~~~~~ [a new closure is created for EVERY node in the AST and remains on the call stack + // until processing of all children is done] + } +} +``` +Instead use the same closure for every call like the example in [Implement your own walking algorithm](#Implement_your_own_walking_algorithm). + +### Create small specialized functions / methods +Instead of stuffing the whole logic in a single closure, consider splitting it up into smaller functions or methods. +Each function should handle similar kinds of nodes. Don't worry too much about the function call, since V8 eventually inlines the function +if possible. + +The AST nodes have different properties, therefore they have a different hidden class in V8. A function can only be optimized for a certain +amount of different hidden classes. Above that threshold the function will be deoptimized and is never optimized again. + +### Pass the optional `sourceFile` parameter +There are serveral methods that have an optional parameter `sourceFile`. Don't omit this parameter if you care for performance. +If ommitted, typescript needs to walk up the node's parent chain until it reaches the SourceFile. This *can* be quite costly when done +frequently on deeply nested nodes. + +Some examples: + +* `node.getStart()` +* `node.getWidth()` +* `node.getText()` +* `node.getChildren()` +* `node.getFirstToken()` +* `node.getLeadingTriviaWidth()` + +### Avoid excessive calls to `node.getStart()`, `node.getWidth()` and `node.getText()` +`node.getStart()` scans the source to skip all the leading trivia. Although barely noticeable, this operation is not for free. +If you need the start position of a node more than once per function, consider caching it. + +`node.getWidth()` is most of the time used together with `node.getStart()` to get the node's span. Internally it uses `node.getStart() - node.getEnd()` which effectively doubles the calls to `node.getStart()`. Consider using `node.getEnd()` instead and calculate the width yourself if necessary. + +`node.getText()` calculates the start of the node and returns a substring until the end of the token. +Most of the time this not needed, because this substring is already contained in the node. +```ts +declare node: ts.Identifier; +node.getText() === node.text; // prefer node.text where available +``` + +__Bonus points:__ If you know the width of the node (either from the `text` property or because it is a keyword of known width), +you can use `node.getEnd() - width` to calculate the node's start. +`node.getEnd()` is effectively for free as it only returns the `end` property. This way you avoid the cost of skipping leading trivia. + +### Make use of tail calls +Tail calls are function or method calls at the end of the control flow of a function. It's only a tail call if the return value of that call +is directly returned unchanged. Browsers can optimize this pattern for performance. +Further optimization is specced in ES2015 as "Proper Tail Calls". +With proper tail calls the browser reuses the stack frame of the current function. When done right this allows for infinite recursion. +```ts +function foo() { + if (condition) + return bar(); // tail call + if (someOtherCondition) + return foo() + 1; // no tail call, return value is modified + return baz(); // tail call +} +function bas() { + if (cond) + return someGlobalVariable = bar(); // no tail call, return value is stored in value before it is returned + foo(); // no tail call because there is no return +} +``` + +### Typeguards +Typeguard functions are very small by default. These functions will be inlined into the containing function. +After inlining you no longer pay the cost of the function call. + +But beware of the inlining limit. If a function is big enough or already has many inlined functions, V8 will stop inlining other functions. + +Try to use a discriminated union if possible. A typeguard makes sense if you can save up multiple type assertions. From e2a4c23d9e614acbcd5b1498746e179323cba33a Mon Sep 17 00:00:00 2001 From: Martin Probst Date: Thu, 9 Feb 2017 04:56:43 +0100 Subject: [PATCH 105/131] Improve error messages for no-any and object-literal-shorthand. (#2164) * Improve error messages for no-any and object-literal-shorthand. Users often use `any` when they could equally well use `{}`, because they are not aware of the empty type. Users also often don't know what object shorthand is - giving them a hint as to what syntax is expected makes it easier to fix the lint warning. * Make the no-any rule more verbose. * Include actual syntax suggestions in the objectLiteralShorthand rule. --- src/rules/noAnyRule.ts | 4 +++- src/rules/objectLiteralShorthandRule.ts | 10 +++++----- test/rules/no-any/test.ts.lint | 2 +- test/rules/object-literal-shorthand/test.js.lint | 15 ++++++--------- test/rules/object-literal-shorthand/test.ts.lint | 16 ++++++++-------- 5 files changed, 23 insertions(+), 24 deletions(-) diff --git a/src/rules/noAnyRule.ts b/src/rules/noAnyRule.ts index 9ce84704125..27e1eb53b42 100644 --- a/src/rules/noAnyRule.ts +++ b/src/rules/noAnyRule.ts @@ -34,7 +34,9 @@ export class Rule extends Lint.Rules.AbstractRule { }; /* tslint:enable:object-literal-sort-keys */ - public static FAILURE_STRING = "Type declaration of 'any' is forbidden"; + public static FAILURE_STRING = "Type declaration of 'any' loses type-safety. " + + "Consider replacing it with a more precise type, the empty type ('{}'), " + + "or suppress this occurrence."; public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { return this.applyWithWalker(new NoAnyWalker(sourceFile, this.getOptions())); diff --git a/src/rules/objectLiteralShorthandRule.ts b/src/rules/objectLiteralShorthandRule.ts index 31c17f5f545..1f4f1d95048 100644 --- a/src/rules/objectLiteralShorthandRule.ts +++ b/src/rules/objectLiteralShorthandRule.ts @@ -32,8 +32,8 @@ export class Rule extends Lint.Rules.AbstractRule { }; /* tslint:enable:object-literal-sort-keys */ - public static LONGHAND_PROPERTY = "Expected property shorthand in object literal."; - public static LONGHAND_METHOD = "Expected method shorthand in object literal."; + public static LONGHAND_PROPERTY = "Expected property shorthand in object literal "; + public static LONGHAND_METHOD = "Expected method shorthand in object literal "; public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { const objectLiteralShorthandWalker = new ObjectLiteralShorthandWalker(sourceFile, this.getOptions()); @@ -55,7 +55,7 @@ class ObjectLiteralShorthandWalker extends Lint.RuleWalker { const fix = this.createFix( this.deleteText(name.getStart(), lengthToValueStart), ); - this.addFailureAtNode(node, Rule.LONGHAND_PROPERTY, fix); + this.addFailureAtNode(node, Rule.LONGHAND_PROPERTY + `('{${name.getText()}}').`, fix); } if (value.kind === ts.SyntaxKind.FunctionExpression) { @@ -63,8 +63,8 @@ class ObjectLiteralShorthandWalker extends Lint.RuleWalker { if (fnNode.name) { return; // named function expressions are OK. } - - this.addFailureAtNode(node, Rule.LONGHAND_METHOD); + const star = fnNode.asteriskToken ? fnNode.asteriskToken.getText() : ""; + this.addFailureAtNode(node, Rule.LONGHAND_METHOD + `('{${name.getText()}${star}() {...}}').`); } super.visitPropertyAssignment(node); diff --git a/test/rules/no-any/test.ts.lint b/test/rules/no-any/test.ts.lint index cb772f4d3a6..b5b9b9c7c2c 100644 --- a/test/rules/no-any/test.ts.lint +++ b/test/rules/no-any/test.ts.lint @@ -15,4 +15,4 @@ let a: any = 2, // error let {a: c, b: d}: {c: any, d: number} = {c: 99, d: 100}; // error ~~~ [0] -[0]: Type declaration of 'any' is forbidden +[0]: Type declaration of 'any' loses type-safety. Consider replacing it with a more precise type, the empty type ('{}'), or suppress this occurrence. diff --git a/test/rules/object-literal-shorthand/test.js.lint b/test/rules/object-literal-shorthand/test.js.lint index 91966f41e6b..25362e1a047 100644 --- a/test/rules/object-literal-shorthand/test.js.lint +++ b/test/rules/object-literal-shorthand/test.js.lint @@ -1,12 +1,12 @@ const bad = { w: function() {}, - ~~~~~~~~~~~~~~~~ [method] + ~~~~~~~~~~~~~~~~ [Expected method shorthand in object literal ('{w() {...}}').] x: function *() {}, - ~~~~~~~~~~~~~~~~~~ [method] + ~~~~~~~~~~~~~~~~~~ [Expected method shorthand in object literal ('{x*() {...}}').] [y]: function() {}, - ~~~~~~~~~~~~~~~~~~ [method] + ~~~~~~~~~~~~~~~~~~ [Expected method shorthand in object literal ('{[y]() {...}}').] z: z - ~~~~ [property] + ~~~~ [Expected property shorthand in object literal ('{z}').] }; const good = { @@ -26,7 +26,7 @@ const namedFunctions = { const quotes = { "foo-bar": function() {}, - ~~~~~~~~~~~~~~~~~~~~~~~~ [method] + ~~~~~~~~~~~~~~~~~~~~~~~~ [Expected method shorthand in object literal ('{"foo-bar"() {...}}').] "foo-bar"() {} }; @@ -37,10 +37,7 @@ const extraCases = { c: 'c', ["a" + "nested"]: { x: x - ~~~~ [property] + ~~~~ [Expected property shorthand in object literal ('{x}').] } }; - -[property]: Expected property shorthand in object literal. -[method]: Expected method shorthand in object literal. \ No newline at end of file diff --git a/test/rules/object-literal-shorthand/test.ts.lint b/test/rules/object-literal-shorthand/test.ts.lint index 91966f41e6b..6b8cf667405 100644 --- a/test/rules/object-literal-shorthand/test.ts.lint +++ b/test/rules/object-literal-shorthand/test.ts.lint @@ -1,12 +1,12 @@ const bad = { w: function() {}, - ~~~~~~~~~~~~~~~~ [method] + ~~~~~~~~~~~~~~~~ [Expected method shorthand in object literal ('{w() {...}}').] x: function *() {}, - ~~~~~~~~~~~~~~~~~~ [method] + ~~~~~~~~~~~~~~~~~~ [Expected method shorthand in object literal ('{x*() {...}}').] [y]: function() {}, - ~~~~~~~~~~~~~~~~~~ [method] + ~~~~~~~~~~~~~~~~~~ [Expected method shorthand in object literal ('{[y]() {...}}').] z: z - ~~~~ [property] + ~~~~ [Expected property shorthand in object literal ('{z}').] }; const good = { @@ -26,7 +26,7 @@ const namedFunctions = { const quotes = { "foo-bar": function() {}, - ~~~~~~~~~~~~~~~~~~~~~~~~ [method] + ~~~~~~~~~~~~~~~~~~~~~~~~ [Expected method shorthand in object literal ('{"foo-bar"() {...}}').] "foo-bar"() {} }; @@ -37,10 +37,10 @@ const extraCases = { c: 'c', ["a" + "nested"]: { x: x - ~~~~ [property] + ~~~~ [Expected property shorthand in object literal ('{x}').] } }; -[property]: Expected property shorthand in object literal. -[method]: Expected method shorthand in object literal. \ No newline at end of file +[property]: Expected property shorthand in object literal ('{foo, bar}'). +[method]: Expected method shorthand in object literal ('{foo() {...}}'). \ No newline at end of file From 0520cde0a57aeab513f14003c43451d61ebf728c Mon Sep 17 00:00:00 2001 From: Noah Chen Date: Fri, 10 Feb 2017 11:36:43 -0500 Subject: [PATCH 106/131] `tslint -i` emits file that only extends `tslint:recommended` (#2199) --- src/configuration.ts | 53 +------------------------------------------- 1 file changed, 1 insertion(+), 52 deletions(-) diff --git a/src/configuration.ts b/src/configuration.ts index 8037fb2a067..8e48c507e47 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -41,58 +41,7 @@ export interface IConfigurationLoadResult { export const CONFIG_FILENAME = "tslint.json"; /* tslint:disable:object-literal-key-quotes */ export const DEFAULT_CONFIG = { - "jsRules": { - "class-name": true, - "comment-format": [true, "check-space"], - "indent": [true, "spaces"], - "no-duplicate-variable": true, - "no-eval": true, - "no-trailing-whitespace": true, - "no-unsafe-finally": true, - "one-line": [true, "check-open-brace", "check-whitespace"], - "quotemark": [true, "double"], - "semicolon": [true, "always"], - "triple-equals": [true, "allow-null-check"], - "variable-name": [true, "ban-keywords"], - "whitespace": [true, - "check-branch", - "check-decl", - "check-operator", - "check-separator", - "check-type", - ], - }, - "rules": { - "class-name": true, - "comment-format": [true, "check-space"], - "indent": [true, "spaces"], - "no-eval": true, - "no-internal-module": true, - "no-trailing-whitespace": true, - "no-unsafe-finally": true, - "no-var-keyword": true, - "one-line": [true, "check-open-brace", "check-whitespace"], - "quotemark": [true, "double"], - "semicolon": [true, "always"], - "triple-equals": [true, "allow-null-check"], - "typedef-whitespace": [ - true, { - "call-signature": "nospace", - "index-signature": "nospace", - "parameter": "nospace", - "property-declaration": "nospace", - "variable-declaration": "nospace", - }, - ], - "variable-name": [true, "ban-keywords"], - "whitespace": [true, - "check-branch", - "check-decl", - "check-operator", - "check-separator", - "check-type", - ], - }, + "extends": "tslint:recommended", }; /* tslint:enable:object-literal-key-quotes */ From da72a2cc49153a67a675f30f633f69904068edcc Mon Sep 17 00:00:00 2001 From: Irfan Hudda Date: Fri, 17 Feb 2017 12:42:32 +0530 Subject: [PATCH 107/131] Fix typographical errors in rule docs (#2211) --- src/rules/classNameRule.ts | 2 +- src/rules/forinRule.ts | 2 +- src/rules/indentRule.ts | 2 +- src/rules/interfaceNameRule.ts | 2 +- src/rules/noAnyRule.ts | 2 +- src/rules/noInferrableTypesRule.ts | 4 ++-- src/rules/semicolonRule.ts | 2 +- src/rules/variableNameRule.ts | 2 +- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/rules/classNameRule.ts b/src/rules/classNameRule.ts index f934d4d3580..97365a74a77 100644 --- a/src/rules/classNameRule.ts +++ b/src/rules/classNameRule.ts @@ -24,7 +24,7 @@ export class Rule extends Lint.Rules.AbstractRule { public static metadata: Lint.IRuleMetadata = { ruleName: "class-name", description: "Enforces PascalCased class and interface names.", - rationale: "Makes it easy to differentitate classes from regular variables at a glance.", + rationale: "Makes it easy to differentiate classes from regular variables at a glance.", optionsDescription: "Not configurable.", options: null, optionExamples: ["true"], diff --git a/src/rules/forinRule.ts b/src/rules/forinRule.ts index aa9a88fea5c..e75d1da425a 100644 --- a/src/rules/forinRule.ts +++ b/src/rules/forinRule.ts @@ -32,7 +32,7 @@ export class Rule extends Lint.Rules.AbstractRule { } } \`\`\` - Prevents accidental interation over properties inherited from an object's prototype. + Prevents accidental iteration over properties inherited from an object's prototype. See [MDN's \`for...in\`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...in) documentation for more information about \`for...in\` loops.`, optionsDescription: "Not configurable.", diff --git a/src/rules/indentRule.ts b/src/rules/indentRule.ts index dba6442095d..682444384c1 100644 --- a/src/rules/indentRule.ts +++ b/src/rules/indentRule.ts @@ -29,7 +29,7 @@ export class Rule extends Lint.Rules.AbstractRule { description: "Enforces indentation with tabs or spaces.", rationale: Lint.Utils.dedent` Using only one of tabs or spaces for indentation leads to more consistent editor behavior, - cleaner diffs in version control, and easier programatic manipulation.`, + cleaner diffs in version control, and easier programmatic manipulation.`, optionsDescription: Lint.Utils.dedent` One of the following arguments must be provided: diff --git a/src/rules/interfaceNameRule.ts b/src/rules/interfaceNameRule.ts index 8a8fa0025d4..8d43abd2de1 100644 --- a/src/rules/interfaceNameRule.ts +++ b/src/rules/interfaceNameRule.ts @@ -27,7 +27,7 @@ export class Rule extends Lint.Rules.AbstractRule { public static metadata: Lint.IRuleMetadata = { ruleName: "interface-name", description: "Requires interface names to begin with a capital 'I'", - rationale: "Makes it easy to differentitate interfaces from regular classes at a glance.", + rationale: "Makes it easy to differentiate interfaces from regular classes at a glance.", optionsDescription: Lint.Utils.dedent` One of the following two options must be provided: diff --git a/src/rules/noAnyRule.ts b/src/rules/noAnyRule.ts index 27e1eb53b42..0a7c097ab71 100644 --- a/src/rules/noAnyRule.ts +++ b/src/rules/noAnyRule.ts @@ -23,7 +23,7 @@ export class Rule extends Lint.Rules.AbstractRule { /* tslint:disable:object-literal-sort-keys */ public static metadata: Lint.IRuleMetadata = { ruleName: "no-any", - description: "Diallows usages of `any` as a type declaration.", + description: "Disallows usages of `any` as a type declaration.", hasFix: true, rationale: "Using `any` as a type declaration nullifies the compile-time benefits of the type system.", optionsDescription: "Not configurable.", diff --git a/src/rules/noInferrableTypesRule.ts b/src/rules/noInferrableTypesRule.ts index feb7beca17e..b36930e5432 100644 --- a/src/rules/noInferrableTypesRule.ts +++ b/src/rules/noInferrableTypesRule.ts @@ -32,9 +32,9 @@ export class Rule extends Lint.Rules.AbstractRule { public static metadata: Lint.IRuleMetadata = { ruleName: "no-inferrable-types", description: "Disallows explicit type declarations for variables or parameters initialized to a number, string, or boolean.", - rationale: "Explicit types where they can be easily infered by the compiler make code more verbose.", + rationale: "Explicit types where they can be easily inferred by the compiler make code more verbose.", optionsDescription: Lint.Utils.dedent` - Two argument may be optionally provided: + Two arguments may be optionally provided: * \`${OPTION_IGNORE_PARMS}\` allows specifying an inferrable type annotation for function params. This can be useful when combining with the \`typedef\` rule. diff --git a/src/rules/semicolonRule.ts b/src/rules/semicolonRule.ts index bb139424437..1ea60b4b4ce 100644 --- a/src/rules/semicolonRule.ts +++ b/src/rules/semicolonRule.ts @@ -36,7 +36,7 @@ export class Rule extends Lint.Rules.AbstractRule { * \`"${OPTION_ALWAYS}"\` enforces semicolons at the end of every statement. * \`"${OPTION_NEVER}"\` disallows semicolons at the end of every statement except for when they are necessary. - The following arguments may be optionaly provided: + The following arguments may be optionally provided: * \`"${OPTION_IGNORE_INTERFACES}"\` skips checking semicolons at the end of interface members. * \`"${OPTION_IGNORE_BOUND_CLASS_METHODS}"\` skips checking semicolons at the end of bound class methods.`, diff --git a/src/rules/variableNameRule.ts b/src/rules/variableNameRule.ts index b29e9344797..28d1cb21da6 100644 --- a/src/rules/variableNameRule.ts +++ b/src/rules/variableNameRule.ts @@ -38,7 +38,7 @@ export class Rule extends Lint.Rules.AbstractRule { * \`"${OPTION_CHECK_FORMAT}"\`: allows only camelCased or UPPER_CASED variable names * \`"${OPTION_LEADING_UNDERSCORE}"\` allows underscores at the beginning (only has an effect if "check-format" specified) * \`"${OPTION_TRAILING_UNDERSCORE}"\` allows underscores at the end. (only has an effect if "check-format" specified) - * \`"${OPTION_ALLOW_PASCAL_CASE}"\` allows PascalCase in addtion to camelCase. + * \`"${OPTION_ALLOW_PASCAL_CASE}"\` allows PascalCase in addition to camelCase. * \`"${OPTION_BAN_KEYWORDS}"\`: disallows the use of certain TypeScript keywords (\`any\`, \`Number\`, \`number\`, \`String\`, \`string\`, \`Boolean\`, \`boolean\`, \`undefined\`) as variable or parameter names.`, options: { From f41ad5bb5a8ca4abdd217a284be9bf4df3fe60d5 Mon Sep 17 00:00:00 2001 From: Joscha Feth Date: Sat, 18 Feb 2017 08:50:30 +1100 Subject: [PATCH 108/131] fix: update update-notifier (#2212) --- package.json | 2 +- yarn.lock | 172 +++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 169 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 04bd96c6953..2a325587be6 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "glob": "^7.1.1", "optimist": "~0.6.0", "resolve": "^1.1.7", - "update-notifier": "^1.0.2" + "update-notifier": "^2.0.0" }, "peerDependencies": { "typescript": ">=2.0.0" diff --git a/yarn.lock b/yarn.lock index b8f96e924d0..fb1c7e06704 100644 --- a/yarn.lock +++ b/yarn.lock @@ -123,6 +123,18 @@ boxen@^0.6.0: string-width "^1.0.1" widest-line "^1.0.0" +boxen@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/boxen/-/boxen-1.0.0.tgz#b2694baf1f605f708ff0177c12193b22f29aaaab" + dependencies: + ansi-align "^1.1.0" + camelcase "^4.0.0" + chalk "^1.1.1" + cli-boxes "^1.0.0" + string-width "^2.0.0" + term-size "^0.1.0" + widest-line "^1.0.0" + brace-expansion@^1.0.0: version "1.1.6" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.6.tgz#7197d7eaa9b87e648390ea61fc66c84427420df9" @@ -146,6 +158,10 @@ camelcase@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" +camelcase@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.0.0.tgz#8b0f90d44be5e281b903b9887349b92595ef07f2" + capture-stack-trace@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz#4a6fa07399c26bba47f0b2496b4d0fb408c5550d" @@ -204,16 +220,34 @@ configstore@^2.0.0: write-file-atomic "^1.1.2" xdg-basedir "^2.0.0" +configstore@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/configstore/-/configstore-3.0.0.tgz#e1b8669c1803ccc50b545e92f8e6e79aa80e0196" + dependencies: + dot-prop "^4.1.0" + graceful-fs "^4.1.2" + mkdirp "^0.5.0" + unique-string "^1.0.0" + write-file-atomic "^1.1.2" + xdg-basedir "^3.0.0" + core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" -create-error-class@^3.0.1: +create-error-class@^3.0.0, create-error-class@^3.0.1: version "3.0.2" resolved "https://registry.yarnpkg.com/create-error-class/-/create-error-class-3.0.2.tgz#06be7abef947a3f14a30fd610671d401bca8b7b6" dependencies: capture-stack-trace "^1.0.0" +cross-spawn-async@^2.1.1: + version "2.2.5" + resolved "https://registry.yarnpkg.com/cross-spawn-async/-/cross-spawn-async-2.2.5.tgz#845ff0c0834a3ded9d160daca6d390906bb288cc" + dependencies: + lru-cache "^4.0.0" + which "^1.2.8" + cross-spawn@^4.0.0: version "4.0.2" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-4.0.2.tgz#7b9247621c23adfdd3856004a823cbe397424d41" @@ -221,6 +255,10 @@ cross-spawn@^4.0.0: lru-cache "^4.0.1" which "^1.2.9" +crypto-random-string@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e" + debug@2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da" @@ -258,12 +296,22 @@ dot-prop@^3.0.0: dependencies: is-obj "^1.0.0" +dot-prop@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.1.0.tgz#eb29eac57dfa31fda1edef50ea462ee3d38ff3ab" + dependencies: + is-obj "^1.0.0" + duplexer2@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1" dependencies: readable-stream "^2.0.2" +duplexer3@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" + duplexer@~0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1" @@ -315,6 +363,17 @@ event-stream@~3.3.0: stream-combiner "~0.0.4" through "~2.3.1" +execa@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-0.4.0.tgz#4eb6467a36a095fabb2970ff9d5e3fb7bce6ebc3" + dependencies: + cross-spawn-async "^2.1.1" + is-stream "^1.1.0" + npm-run-path "^1.0.0" + object-assign "^4.0.1" + path-key "^1.0.0" + strip-eof "^1.0.0" + filled-array@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/filled-array/-/filled-array-1.1.0.tgz#c3c4f6c663b923459a9aa29912d2d031f1507f84" @@ -348,6 +407,10 @@ function-bind@^1.0.2, function-bind@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.0.tgz#16176714c801798e4e8f2cf7f7529467bb4a5771" +get-stream@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" + glob@7.0.5: version "7.0.5" resolved "https://registry.yarnpkg.com/glob/-/glob-7.0.5.tgz#b4202a69099bbb4d292a7c1b95b6682b67ebdc95" @@ -400,6 +463,22 @@ got@^5.0.0: unzip-response "^1.0.2" url-parse-lax "^1.0.0" +got@^6.7.1: + version "6.7.1" + resolved "https://registry.yarnpkg.com/got/-/got-6.7.1.tgz#240cd05785a9a18e561dc1b44b41c763ef1e8db0" + dependencies: + create-error-class "^3.0.0" + duplexer3 "^0.1.4" + get-stream "^3.0.0" + is-redirect "^1.0.0" + is-retry-allowed "^1.0.0" + is-stream "^1.0.0" + lowercase-keys "^1.0.0" + safe-buffer "^5.0.1" + timed-out "^4.0.0" + unzip-response "^2.0.1" + url-parse-lax "^1.0.0" + graceful-fs@^4.1.11, graceful-fs@^4.1.2: version "4.1.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" @@ -475,6 +554,10 @@ is-fullwidth-code-point@^1.0.0: dependencies: number-is-nan "^1.0.0" +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + is-npm@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-1.0.0.tgz#f2fb63a65e4905b406c86072765a1a4dc793b9f4" @@ -495,7 +578,7 @@ is-retry-allowed@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz#11a060568b67339444033d0125a61a20d564fb34" -is-stream@^1.0.0: +is-stream@^1.0.0, is-stream@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" @@ -540,10 +623,20 @@ latest-version@^2.0.0: dependencies: package-json "^2.0.0" +latest-version@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-3.0.0.tgz#3104f008c0c391084107f85a344bc61e38970649" + dependencies: + package-json "^3.0.0" + lazy-req@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/lazy-req/-/lazy-req-1.1.0.tgz#bdaebead30f8d824039ce0ce149d4daa07ba1fac" +lazy-req@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/lazy-req/-/lazy-req-2.0.0.tgz#c9450a363ecdda2e6f0c70132ad4f37f8f06f2b4" + load-json-file@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" @@ -605,7 +698,7 @@ lowercase-keys@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306" -lru-cache@^4.0.1: +lru-cache@^4.0.0, lru-cache@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.0.2.tgz#1d17679c069cda5d040991a09dbc2c0db377e55e" dependencies: @@ -688,6 +781,12 @@ npm-run-all@^3.1.0: shell-quote "^1.6.1" string.prototype.padend "^3.0.0" +npm-run-path@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-1.0.0.tgz#f5c32bf595fe81ae927daec52e82f8b000ac3c8f" + dependencies: + path-key "^1.0.0" + number-is-nan@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" @@ -737,6 +836,15 @@ package-json@^2.0.0: registry-url "^3.0.3" semver "^5.1.0" +package-json@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/package-json/-/package-json-3.0.0.tgz#dc11f79ebdb436e55fe2b9b294ee3f2a87a33b13" + dependencies: + got "^6.7.1" + registry-auth-token "^3.0.1" + registry-url "^3.0.3" + semver "^5.1.0" + parse-json@^2.1.0, parse-json@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" @@ -753,6 +861,10 @@ path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" +path-key@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-1.0.0.tgz#5d53d578019646c0d68800db4e146e6bdc2ac7af" + path-type@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" @@ -870,6 +982,10 @@ rimraf@^2.5.4: dependencies: glob "^7.0.5" +safe-buffer@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7" + semver-diff@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36" @@ -931,6 +1047,13 @@ string-width@^1.0.1: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" +string-width@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.0.0.tgz#635c5436cc72a6e0c387ceca278d4e2eec52687e" + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^3.0.0" + string.prototype.padend@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/string.prototype.padend/-/string.prototype.padend-3.0.0.tgz#f3aaef7c1719f170c5eab1c32bf780d96e21f2f0" @@ -955,6 +1078,10 @@ strip-bom@^2.0.0: dependencies: is-utf8 "^0.2.0" +strip-eof@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" + strip-json-comments@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-1.0.4.tgz#1e15fbcac97d3ee99bf2d73b4c656b082bbafb91" @@ -969,6 +1096,12 @@ supports-color@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" +term-size@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/term-size/-/term-size-0.1.1.tgz#87360b96396cab5760963714cda0d0cbeecad9ca" + dependencies: + execa "^0.4.0" + through@2, through@~2.3, through@~2.3.1: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" @@ -977,6 +1110,10 @@ timed-out@^3.0.0: version "3.1.3" resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-3.1.3.tgz#95860bfcc5c76c277f8f8326fd0f5b2e20eba217" +timed-out@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f" + "tslint-test-config-non-relative@file:test/external/tslint-test-config-non-relative": version "0.0.1" @@ -1005,10 +1142,20 @@ typescript@2.1.4: version "2.1.4" resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.1.4.tgz#b53b69fb841126acb1dd4b397d21daba87572251" +unique-string@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-1.0.0.tgz#9e1057cca851abb93398f8b33ae187b99caec11a" + dependencies: + crypto-random-string "^1.0.0" + unzip-response@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-1.0.2.tgz#b984f0877fc0a89c2c773cc1ef7b5b232b5b06fe" +unzip-response@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97" + update-notifier@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-1.0.3.tgz#8f92c515482bd6831b7c93013e70f87552c7cf5a" @@ -1022,6 +1169,19 @@ update-notifier@^1.0.2: semver-diff "^2.0.0" xdg-basedir "^2.0.0" +update-notifier@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-2.0.0.tgz#1f2712fd2079f415980b5af95eb120e4aceea9a6" + dependencies: + boxen "^1.0.0" + chalk "^1.0.0" + configstore "^3.0.0" + is-npm "^1.0.0" + latest-version "^3.0.0" + lazy-req "^2.0.0" + semver-diff "^2.0.0" + xdg-basedir "^3.0.0" + url-parse-lax@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-1.0.0.tgz#7af8f303645e9bd79a272e7a14ac68bc0609da73" @@ -1043,7 +1203,7 @@ validate-npm-package-license@^3.0.1: spdx-correct "~1.0.0" spdx-expression-parse "~1.0.0" -which@^1.2.9: +which@^1.2.8, which@^1.2.9: version "1.2.12" resolved "https://registry.yarnpkg.com/which/-/which-1.2.12.tgz#de67b5e450269f194909ef23ece4ebe416fa1192" dependencies: @@ -1077,6 +1237,10 @@ xdg-basedir@^2.0.0: dependencies: os-homedir "^1.0.0" +xdg-basedir@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4" + yallist@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.0.0.tgz#306c543835f09ee1a4cb23b7bce9ab341c91cdd4" From 248e4042c27ffc8d2d4c51b9c1f3b87c65e7c7c8 Mon Sep 17 00:00:00 2001 From: Moritz Date: Wed, 22 Feb 2017 16:29:21 -0600 Subject: [PATCH 109/131] Preserve newlines and tabs when autofixing quote marks (#2224) --- src/rules/quotemarkRule.ts | 5 +++-- test/rules/quotemark/double-avoid-escape/test.js.lint | 4 +++- test/rules/quotemark/double-avoid-escape/test.ts.fix | 3 ++- test/rules/quotemark/double-avoid-escape/test.ts.lint | 4 +++- test/rules/quotemark/double/test.js.lint | 4 +++- test/rules/quotemark/double/test.ts.fix | 3 ++- test/rules/quotemark/double/test.ts.lint | 4 +++- test/rules/quotemark/jsx-double/test.tsx.fix | 2 +- test/rules/quotemark/jsx-double/test.tsx.lint | 7 ++++--- test/rules/quotemark/jsx-single/test.tsx.fix | 2 +- test/rules/quotemark/jsx-single/test.tsx.lint | 7 ++++--- test/rules/quotemark/single-avoid-escape/test.js.lint | 6 ++++-- test/rules/quotemark/single-avoid-escape/test.ts.fix | 3 ++- test/rules/quotemark/single-avoid-escape/test.ts.lint | 6 ++++-- test/rules/quotemark/single/test.js.lint | 6 ++++-- test/rules/quotemark/single/test.ts.fix | 3 ++- test/rules/quotemark/single/test.ts.lint | 6 ++++-- 17 files changed, 49 insertions(+), 26 deletions(-) diff --git a/src/rules/quotemarkRule.ts b/src/rules/quotemarkRule.ts index 934f9a6e475..eefad4c0414 100644 --- a/src/rules/quotemarkRule.ts +++ b/src/rules/quotemarkRule.ts @@ -82,9 +82,10 @@ class QuotemarkWalker extends Lint.RuleWalker { public visitStringLiteral(node: ts.StringLiteral) { const expectedQuoteMark = node.parent!.kind === ts.SyntaxKind.JsxAttribute ? this.jsxQuoteMark : this.quoteMark; - const actualQuoteMark = node.getText()[0]; + const text = node.getText(); + const actualQuoteMark = text[0]; if (actualQuoteMark !== expectedQuoteMark && !(this.avoidEscape && node.text.includes(expectedQuoteMark))) { - const escapedText = node.text.replace(new RegExp(expectedQuoteMark, "g"), `\\${expectedQuoteMark}`); + const escapedText = text.slice(1, -1).replace(new RegExp(expectedQuoteMark, "g"), `\\${expectedQuoteMark}`); const newText = expectedQuoteMark + escapedText + expectedQuoteMark; this.addFailureAtNode(node, Rule.FAILURE_STRING(actualQuoteMark, expectedQuoteMark), this.createFix(this.createReplacement(node.getStart(), node.getWidth(), newText))); diff --git a/test/rules/quotemark/double-avoid-escape/test.js.lint b/test/rules/quotemark/double-avoid-escape/test.js.lint index 03f86c4826e..720edf1cd5d 100644 --- a/test/rules/quotemark/double-avoid-escape/test.js.lint +++ b/test/rules/quotemark/double-avoid-escape/test.js.lint @@ -1,5 +1,7 @@ var single = 'single'; ~~~~~~~~ [' should be "] - var doublee = "married"; + var double = "married"; var singleWithinDouble = "'singleWithinDouble'"; var doubleWithinSingle = '"doubleWithinSingle"'; +var tabNewlineWithinSingle = 'tab\tNewline\nWithinSingle'; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [' should be "] diff --git a/test/rules/quotemark/double-avoid-escape/test.ts.fix b/test/rules/quotemark/double-avoid-escape/test.ts.fix index a5251c3e457..249ce46b38e 100644 --- a/test/rules/quotemark/double-avoid-escape/test.ts.fix +++ b/test/rules/quotemark/double-avoid-escape/test.ts.fix @@ -1,4 +1,5 @@ var single = "single"; - var doublee = "married"; + var double = "married"; var singleWithinDouble = "'singleWithinDouble'"; var doubleWithinSingle = '"doubleWithinSingle"'; +var tabNewlineWithinSingle = "tab\tNewline\nWithinSingle"; diff --git a/test/rules/quotemark/double-avoid-escape/test.ts.lint b/test/rules/quotemark/double-avoid-escape/test.ts.lint index 03f86c4826e..720edf1cd5d 100644 --- a/test/rules/quotemark/double-avoid-escape/test.ts.lint +++ b/test/rules/quotemark/double-avoid-escape/test.ts.lint @@ -1,5 +1,7 @@ var single = 'single'; ~~~~~~~~ [' should be "] - var doublee = "married"; + var double = "married"; var singleWithinDouble = "'singleWithinDouble'"; var doubleWithinSingle = '"doubleWithinSingle"'; +var tabNewlineWithinSingle = 'tab\tNewline\nWithinSingle'; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [' should be "] diff --git a/test/rules/quotemark/double/test.js.lint b/test/rules/quotemark/double/test.js.lint index 2efd3df18b3..b6f42d9a29c 100644 --- a/test/rules/quotemark/double/test.js.lint +++ b/test/rules/quotemark/double/test.js.lint @@ -1,6 +1,8 @@ var single = 'single'; ~~~~~~~~ [' should be "] - var doublee = "married"; + var double = "married"; var singleWithinDouble = "'singleWithinDouble'"; var doubleWithinSingle = '"doubleWithinSingle"'; ~~~~~~~~~~~~~~~~~~~~~~ [' should be "] +var tabNewlineWithinSingle = 'tab\tNewline\nWithinSingle'; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [' should be "] diff --git a/test/rules/quotemark/double/test.ts.fix b/test/rules/quotemark/double/test.ts.fix index 40f8455f047..ca5365a0330 100644 --- a/test/rules/quotemark/double/test.ts.fix +++ b/test/rules/quotemark/double/test.ts.fix @@ -1,4 +1,5 @@ var single = "single"; - var doublee = "married"; + var double = "married"; var singleWithinDouble = "'singleWithinDouble'"; var doubleWithinSingle = "\"doubleWithinSingle\""; +var tabNewlineWithinSingle = "tab\tNewline\nWithinSingle"; diff --git a/test/rules/quotemark/double/test.ts.lint b/test/rules/quotemark/double/test.ts.lint index 2efd3df18b3..b6f42d9a29c 100644 --- a/test/rules/quotemark/double/test.ts.lint +++ b/test/rules/quotemark/double/test.ts.lint @@ -1,6 +1,8 @@ var single = 'single'; ~~~~~~~~ [' should be "] - var doublee = "married"; + var double = "married"; var singleWithinDouble = "'singleWithinDouble'"; var doubleWithinSingle = '"doubleWithinSingle"'; ~~~~~~~~~~~~~~~~~~~~~~ [' should be "] +var tabNewlineWithinSingle = 'tab\tNewline\nWithinSingle'; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [' should be "] diff --git a/test/rules/quotemark/jsx-double/test.tsx.fix b/test/rules/quotemark/jsx-double/test.tsx.fix index 984fbc4ac7d..52aee4810f8 100644 --- a/test/rules/quotemark/jsx-double/test.tsx.fix +++ b/test/rules/quotemark/jsx-double/test.tsx.fix @@ -1,4 +1,4 @@ import * as React from 'react'; export const a = ( -
+
diff --git a/test/rules/quotemark/jsx-double/test.tsx.lint b/test/rules/quotemark/jsx-double/test.tsx.lint index 926b63afe45..ef33be82556 100644 --- a/test/rules/quotemark/jsx-double/test.tsx.lint +++ b/test/rules/quotemark/jsx-double/test.tsx.lint @@ -2,6 +2,7 @@ import * as React from "react"; ~~~~~~~ [" should be '] export const a = ( -
- ~~~~ [' should be "] - ~~~ [" should be '] +
+ ~~~~ [' should be "] + ~~~ [" should be '] + ~~~~~~~~~ [" should be '] diff --git a/test/rules/quotemark/jsx-single/test.tsx.fix b/test/rules/quotemark/jsx-single/test.tsx.fix index 3c2189fc342..4e9df3a01ce 100644 --- a/test/rules/quotemark/jsx-single/test.tsx.fix +++ b/test/rules/quotemark/jsx-single/test.tsx.fix @@ -1,4 +1,4 @@ import * as React from "react"; export const a = ( -
+
diff --git a/test/rules/quotemark/jsx-single/test.tsx.lint b/test/rules/quotemark/jsx-single/test.tsx.lint index d571e61c69c..9dbc3f68f05 100644 --- a/test/rules/quotemark/jsx-single/test.tsx.lint +++ b/test/rules/quotemark/jsx-single/test.tsx.lint @@ -1,6 +1,7 @@ import * as React from "react"; export const a = ( -
- ~~~~~~~ [" should be '] - ~~~ [' should be "] +
+ ~~~~~~~ [" should be '] + ~~~ [' should be "] + ~~~~~~~~~ [' should be "] diff --git a/test/rules/quotemark/single-avoid-escape/test.js.lint b/test/rules/quotemark/single-avoid-escape/test.js.lint index 52e21fb78a9..2084df32b57 100644 --- a/test/rules/quotemark/single-avoid-escape/test.js.lint +++ b/test/rules/quotemark/single-avoid-escape/test.js.lint @@ -1,5 +1,7 @@ var single = 'single'; - var doublee = "married"; - ~~~~~~~~~ [" should be '] + var double = "married"; + ~~~~~~~~~ [" should be '] var singleWithinDouble = "'singleWithinDouble'"; var doubleWithinSingle = '"doubleWithinSingle"'; +var tabNewlineWithinDouble = "tab\tNewline\nWithinDouble"; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [" should be '] diff --git a/test/rules/quotemark/single-avoid-escape/test.ts.fix b/test/rules/quotemark/single-avoid-escape/test.ts.fix index b02af962658..2b3ee390fe7 100644 --- a/test/rules/quotemark/single-avoid-escape/test.ts.fix +++ b/test/rules/quotemark/single-avoid-escape/test.ts.fix @@ -1,4 +1,5 @@ var single = 'single'; - var doublee = 'married'; + var double = 'married'; var singleWithinDouble = "'singleWithinDouble'"; var doubleWithinSingle = '"doubleWithinSingle"'; +var tabNewlineWithinDouble = 'tab\tNewline\nWithinDouble'; diff --git a/test/rules/quotemark/single-avoid-escape/test.ts.lint b/test/rules/quotemark/single-avoid-escape/test.ts.lint index 52e21fb78a9..2084df32b57 100644 --- a/test/rules/quotemark/single-avoid-escape/test.ts.lint +++ b/test/rules/quotemark/single-avoid-escape/test.ts.lint @@ -1,5 +1,7 @@ var single = 'single'; - var doublee = "married"; - ~~~~~~~~~ [" should be '] + var double = "married"; + ~~~~~~~~~ [" should be '] var singleWithinDouble = "'singleWithinDouble'"; var doubleWithinSingle = '"doubleWithinSingle"'; +var tabNewlineWithinDouble = "tab\tNewline\nWithinDouble"; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [" should be '] diff --git a/test/rules/quotemark/single/test.js.lint b/test/rules/quotemark/single/test.js.lint index bba732c703b..b1715d21272 100644 --- a/test/rules/quotemark/single/test.js.lint +++ b/test/rules/quotemark/single/test.js.lint @@ -1,6 +1,8 @@ var single = 'single'; - var doublee = "married"; - ~~~~~~~~~ [" should be '] + var double = "married"; + ~~~~~~~~~ [" should be '] var singleWithinDouble = "'singleWithinDouble'"; ~~~~~~~~~~~~~~~~~~~~~~ [" should be '] var doubleWithinSingle = '"doubleWithinSingle"'; +var tabNewlineWithinDouble = "tab\tNewline\nWithinDouble"; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [" should be '] \ No newline at end of file diff --git a/test/rules/quotemark/single/test.ts.fix b/test/rules/quotemark/single/test.ts.fix index b73ff7f5bac..9ad916e9142 100644 --- a/test/rules/quotemark/single/test.ts.fix +++ b/test/rules/quotemark/single/test.ts.fix @@ -1,4 +1,5 @@ var single = 'single'; - var doublee = 'married'; + var double = 'married'; var singleWithinDouble = '\'singleWithinDouble\''; var doubleWithinSingle = '"doubleWithinSingle"'; +var tabNewlineWithinDouble = 'tab\tNewline\nWithinDouble'; diff --git a/test/rules/quotemark/single/test.ts.lint b/test/rules/quotemark/single/test.ts.lint index bba732c703b..6062acb2c0a 100644 --- a/test/rules/quotemark/single/test.ts.lint +++ b/test/rules/quotemark/single/test.ts.lint @@ -1,6 +1,8 @@ var single = 'single'; - var doublee = "married"; - ~~~~~~~~~ [" should be '] + var double = "married"; + ~~~~~~~~~ [" should be '] var singleWithinDouble = "'singleWithinDouble'"; ~~~~~~~~~~~~~~~~~~~~~~ [" should be '] var doubleWithinSingle = '"doubleWithinSingle"'; +var tabNewlineWithinDouble = "tab\tNewline\nWithinDouble"; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [" should be '] From eea89e5a5b35d6226c02a9dcffeffbe6dd3ebd22 Mon Sep 17 00:00:00 2001 From: Alexander Rusakov Date: Thu, 23 Feb 2017 01:32:13 +0300 Subject: [PATCH 110/131] New Rule: no-non-null-assertion (#2221) --- src/language/walker/syntaxWalker.ts | 8 ++++ src/rules/noNonNullAssertionRule.ts | 48 +++++++++++++++++++ test/rules/no-non-null-assertion/test.ts.lint | 4 ++ test/rules/no-non-null-assertion/tslint.json | 5 ++ 4 files changed, 65 insertions(+) create mode 100644 src/rules/noNonNullAssertionRule.ts create mode 100644 test/rules/no-non-null-assertion/test.ts.lint create mode 100644 test/rules/no-non-null-assertion/tslint.json diff --git a/src/language/walker/syntaxWalker.ts b/src/language/walker/syntaxWalker.ts index acee72b0e09..09d22289810 100644 --- a/src/language/walker/syntaxWalker.ts +++ b/src/language/walker/syntaxWalker.ts @@ -234,6 +234,10 @@ export class SyntaxWalker { this.walkChildren(node); } + protected visitNonNullExpression(node: ts.NonNullExpression) { + this.walkChildren(node); + } + protected visitNumericLiteral(node: ts.NumericLiteral) { this.walkChildren(node); } @@ -560,6 +564,10 @@ export class SyntaxWalker { this.visitNewExpression(node as ts.NewExpression); break; + case ts.SyntaxKind.NonNullExpression: + this.visitNonNullExpression(node as ts.NonNullExpression); + break; + case ts.SyntaxKind.NumericLiteral: this.visitNumericLiteral(node as ts.NumericLiteral); break; diff --git a/src/rules/noNonNullAssertionRule.ts b/src/rules/noNonNullAssertionRule.ts new file mode 100644 index 00000000000..015f3bdcbea --- /dev/null +++ b/src/rules/noNonNullAssertionRule.ts @@ -0,0 +1,48 @@ +/** + * @license + * Copyright 2017 Palantir Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as ts from "typescript"; + +import * as Lint from "../index"; + +export class Rule extends Lint.Rules.AbstractRule { + /* tslint:disable:object-literal-sort-keys */ + public static metadata: Lint.IRuleMetadata = { + ruleName: "no-non-null-assertion", + description: "Disallows non-null assertions.", + rationale: "Using non-null assertion cancels the benefits of the strict null checking mode.", + optionsDescription: "Not configurable.", + options: null, + optionExamples: ["true"], + type: "typescript", + typescriptOnly: true, + }; + /* tslint:enable:object-literal-sort-keys */ + + public static FAILURE_STRING = "Forbidden non null assertion"; + + public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { + return this.applyWithWalker(new NoNonNullAssertionWalker(sourceFile, this.getOptions())); + } +} + +class NoNonNullAssertionWalker extends Lint.RuleWalker { + public visitNonNullExpression(node: ts.NonNullExpression) { + this.addFailureAtNode(node, Rule.FAILURE_STRING); + super.visitNonNullExpression(node); + } +} diff --git a/test/rules/no-non-null-assertion/test.ts.lint b/test/rules/no-non-null-assertion/test.ts.lint new file mode 100644 index 00000000000..12bb8af725d --- /dev/null +++ b/test/rules/no-non-null-assertion/test.ts.lint @@ -0,0 +1,4 @@ +var x = null + +x!.y = null +~~ [Forbidden non null assertion] diff --git a/test/rules/no-non-null-assertion/tslint.json b/test/rules/no-non-null-assertion/tslint.json new file mode 100644 index 00000000000..6359b34ac55 --- /dev/null +++ b/test/rules/no-non-null-assertion/tslint.json @@ -0,0 +1,5 @@ +{ + "rules": { + "no-non-null-assertion": true + } +} From 0b11b798d08336baf26fdf131af45c1f2141f6ec Mon Sep 17 00:00:00 2001 From: Alex Ryan Date: Wed, 22 Feb 2017 22:25:25 -0800 Subject: [PATCH 111/131] Don't publish yarn.lock in dist (#2232) --- .npmignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.npmignore b/.npmignore index d76bbd86186..721609db878 100644 --- a/.npmignore +++ b/.npmignore @@ -12,6 +12,7 @@ appveyor.yml circle.yml tslint.json +yarn.lock /build/ /docs/ /scripts/ From cae1c9c4f3b5d22bcfa6c626724ca82b2a36444e Mon Sep 17 00:00:00 2001 From: Andy Date: Fri, 24 Feb 2017 08:31:18 -0800 Subject: [PATCH 112/131] Fixes for windows (#2233) * Use 'LF' line endings for '.fix' files to match '.lint' files * Can't run shebang script, so use 'node' explicitly --- .gitattributes | 1 + package.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitattributes b/.gitattributes index 087a23bb799..97de27519a4 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,4 +3,5 @@ # being reported to be 1 character longer *.lint text eol=lf +*.fix text eol=lf /test/rules/linebreak-style/**/CRLF/*.lint text eol=crlf diff --git a/package.json b/package.json index 2a325587be6..04354230821 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "lint": "npm-run-all -p lint:core lint:test lint:from-bin", "lint:core": "tslint \"src/**/*.ts\"", "lint:test": "tslint \"test/**/*.ts\" -e \"test/**/*.test.ts\"", - "lint:from-bin": "bin/tslint \"{src,test}/**/*.ts\" -e \"test/**/*.test.ts\"", + "lint:from-bin": "node bin/tslint \"{src,test}/**/*.ts\" -e \"test/**/*.test.ts\"", "test": "npm-run-all test:pre -p test:mocha test:rules", "test:pre": "cd ./test/config && npm install", "test:mocha": "mocha --reporter spec --colors \"build/test/**/*Tests.js\" build/test/assert.js", From dae82ca4db41cfbdbfa91e46f300feda9089f144 Mon Sep 17 00:00:00 2001 From: Noah Chen Date: Fri, 24 Feb 2017 14:25:59 -0500 Subject: [PATCH 113/131] Write script to generate CHANGELOG.md entries (#2196) Write script to generate CHANGELOG.md entries Update PR/Issue templates to make them easier to fill out (less boilerplate to delete) --- .github/ISSUE_TEMPLATE.md | 15 ++-- .github/PULL_REQUEST_TEMPLATE.md | 10 ++- package.json | 2 + scripts/generate-changelog.ts | 149 +++++++++++++++++++++++++++++++ yarn.lock | 56 +++++++++++- 5 files changed, 218 insertions(+), 14 deletions(-) create mode 100644 scripts/generate-changelog.ts diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 7a7d668984c..e7d5a66abca 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,26 +1,21 @@ ### Bug Report -- __TSLint version__: `` -- __TypeScript version__: `` -- __Running TSLint via__: (please choose one) CLI / Node.js API / grunt-tslint / Atom / Visual Studio / etc +- __TSLint version__: +- __TypeScript version__: +- __Running TSLint via__: (pick one) CLI / Node.js API / VSCode / grunt-tslint / Atom / Visual Studio / etc #### TypeScript code being linted ```ts -(include if relevant) +// code snippet ``` with `tslint.json` configuration: ```json -(include if relevant) + ``` #### Actual behavior -(fill this out) - #### Expected behavior - -(fill this out) - diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index d21f050c4e8..2547d888d82 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -5,10 +5,14 @@ - [ ] Includes tests - [ ] Documentation update -#### What changes did you make? +#### Overview of change: -(give an overview) #### Is there anything you'd like reviewers to focus on? -(optional) + + +#### CHANGELOG.md entry: + + + diff --git a/package.json b/package.json index 04354230821..f3d2159a643 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "@types/colors": "^0.6.33", "@types/diff": "0.0.31", "@types/findup-sync": "^0.3.29", + "@types/github": "^0.0.0", "@types/glob": "^5.0.30", "@types/js-yaml": "^3.5.29", "@types/mocha": "^2.2.35", @@ -62,6 +63,7 @@ "@types/resolve": "0.0.4", "@types/update-notifier": "^1.0.0", "chai": "^3.5.0", + "github": "^8.1.1", "js-yaml": "^3.7.0", "mocha": "^3.2.0", "npm-run-all": "^3.1.0", diff --git a/scripts/generate-changelog.ts b/scripts/generate-changelog.ts new file mode 100644 index 00000000000..049f3ef9d24 --- /dev/null +++ b/scripts/generate-changelog.ts @@ -0,0 +1,149 @@ +/* + * Copyright 2017 Palantir Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Generates entries for CHANGELOG.md from pull requests + * + * Reads changelog entries from pull requests merged since the last release tag. + * Changelog entries are lines within the first PR comment that matches /^\[[a-z\-]+\]/ like `[new-rule]` and `[bugfix]` + */ + +// tslint:disable:no-console + +import GitHubApi = require("github"); +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; + +const github = new GitHubApi({ + host: "api.github.com", + protocol: "https", + timeout: 5000, +}); + +const repoInfo = { + owner: "palantir", + repo: "tslint", +}; + +const tokenFile = path.join(os.homedir(), "github_token.txt"); + +// authenticate +const auth: GitHubApi.Auth = { + token: fs.readFileSync(tokenFile, "utf8").toString().trim(), + type: "oauth", +}; +console.log("Using OAuth token " + auth.token + "\n"); + +github.authenticate(auth); + +const commits: ICommit[] = []; +github.repos.getLatestRelease(repoInfo).then((value) => { + console.log("Getting commits " + value.tag_name + "..master"); + // get the commits between the most recent release and the head of master + return github.repos.compareCommits({ + base: value.tag_name, + head: "master", + ...repoInfo, + }); +}).then((value) => { + // for each commit, get the PR, and extract changelog entries + const promises: Array> = []; + for (const commitInfo of value.commits) { + const commit: ICommit = { + fields: [], + sha: commitInfo.sha, + submitter: commitInfo.commit.author.name != null ? commitInfo.commit.author.name : commitInfo.author.login, + title: commitInfo.commit.message, + }; + commits.push(commit); + + // check for a pull request number in the commit title + const match = (commitInfo.commit.message as string).match(/\(#(\d+)\)/); + if (match && match.length > 1) { + commit.pushRequestNum = Number.parseInt(match[1], 10); + + // get the PR text + promises.push(github.issues.get({ + number: commit.pushRequestNum, + ...repoInfo, + }).then((comment) => { + // extract the changelog entries + const lines = (comment.body as string).split("\r\n"); + for (const line of lines) { + const fieldMatch = line.match(/^(\[[a-z\-]+\])/); + if (fieldMatch) { + commit.fields.push({ + tag: fieldMatch[1], + text: line + " (#" + commit.pushRequestNum + ")", + }); + } + } + })); + } + + } + return Promise.all(promises); +}).then(() => { + const entries: IField[] = []; + const noFields: string[] = []; + const contributors = new Set(); + for (const commit of commits) { + if (commit.fields.length > 0) { + for (const field of commit.fields) { + entries.push(field); + } + } else { + noFields.push(commit.title); + } + contributors.add(commit.submitter); + } + entries.sort((a, b) => { + return a.tag.localeCompare(b.tag); + }); + + console.log("\n---- formatted changelog entries: ----"); + for (const entry of entries) { + console.log("- " + entry.text); + } + + console.log("\n---- PRs with missing changelog entries: ----"); + for (const missing of noFields) { + console.log("- " + missing.replace(/[\r\n]+/, "\r\n ")); + } + + console.log("\n---- thanks ----"); + console.log("Thanks to our contributors!"); + contributors.forEach((contributor) => { + console.log("- " + contributor); + }); +}).catch((error) => { + console.log("Error:" + error); +}); + +interface IField { + tag: string; + text: string; +} + +interface ICommit { + pushRequestBody?: string; + pushRequestNum?: number; + submitter: string; + sha: string; + title: string; + fields: IField[]; +} diff --git a/yarn.lock b/yarn.lock index fb1c7e06704..05c81b8ff31 100644 --- a/yarn.lock +++ b/yarn.lock @@ -24,6 +24,10 @@ dependencies: "@types/minimatch" "*" +"@types/github@^0.0.0": + version "0.0.0" + resolved "https://registry.yarnpkg.com/@types/github/-/github-0.0.0.tgz#f32202bcdb15ac916cd079caba9e9f45bb304e76" + "@types/glob@^5.0.30": version "5.0.30" resolved "https://registry.yarnpkg.com/@types/glob/-/glob-5.0.30.tgz#1026409c5625a8689074602808d082b2867b8a51" @@ -61,6 +65,13 @@ version "1.0.0" resolved "https://registry.yarnpkg.com/@types/update-notifier/-/update-notifier-1.0.0.tgz#3ae6206a6d67c01ffddb9a1eac4cd9b518d534ee" +agent-base@2: + version "2.0.1" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-2.0.1.tgz#bd8f9e86a8eb221fffa07bd14befd55df142815e" + dependencies: + extend "~3.0.0" + semver "~5.0.1" + ansi-align@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-1.1.0.tgz#2f0c1658829739add5ebb15e6b0c6e3423f016ba" @@ -259,7 +270,7 @@ crypto-random-string@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e" -debug@2.2.0: +debug@2, debug@2.2.0, debug@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da" dependencies: @@ -363,6 +374,9 @@ event-stream@~3.3.0: stream-combiner "~0.0.4" through "~2.3.1" +extend@3, extend@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.0.tgz#5a474353b9f3353ddd8176dfd37b91c83a46f1d4" execa@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/execa/-/execa-0.4.0.tgz#4eb6467a36a095fabb2970ff9d5e3fb7bce6ebc3" @@ -391,6 +405,13 @@ findup-sync@~0.3.0: dependencies: glob "~5.0.0" +follow-redirects@0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-0.0.7.tgz#34b90bab2a911aa347571da90f22bd36ecd8a919" + dependencies: + debug "^2.2.0" + stream-consume "^0.1.0" + foreach@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99" @@ -407,6 +428,15 @@ function-bind@^1.0.2, function-bind@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.0.tgz#16176714c801798e4e8f2cf7f7529467bb4a5771" +github@^8.1.1: + version "8.1.1" + resolved "https://registry.yarnpkg.com/github/-/github-8.1.1.tgz#a078c61669b4d4b588bf1b2e2a591eb7c49feb36" + dependencies: + follow-redirects "0.0.7" + https-proxy-agent "^1.0.0" + mime "^1.2.11" + netrc "^0.1.4" + get-stream@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" @@ -505,6 +535,14 @@ hosted-git-info@^2.1.4: version "2.1.5" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.1.5.tgz#0ba81d90da2e25ab34a332e6ec77936e1598118b" +https-proxy-agent@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-1.0.0.tgz#35f7da6c48ce4ddbfa264891ac593ee5ff8671e6" + dependencies: + agent-base "2" + debug "2" + extend "3" + imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" @@ -709,6 +747,10 @@ map-stream@~0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.1.0.tgz#e56aa94c4c8055a16404a0674b78f215f7c8e194" +mime@^1.2.11: + version "1.3.4" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53" + "minimatch@2 || 3", minimatch@^3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.3.tgz#2a4e4090b96b2db06a9d7df01055a62a77c9b774" @@ -753,6 +795,10 @@ ms@0.7.1: version "0.7.1" resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098" +netrc@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/netrc/-/netrc-0.1.4.tgz#6be94fcaca8d77ade0a9670dc460914c94472444" + node-status-codes@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/node-status-codes/-/node-status-codes-1.0.0.tgz#5ae5541d024645d32a58fcddc9ceecea7ae3ac2f" @@ -996,6 +1042,10 @@ semver-diff@^2.0.0: version "5.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" +semver@~5.0.1: + version "5.0.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.0.3.tgz#77466de589cd5d3c95f138aa78bc569a3cb5d27a" + shell-quote@^1.6.1: version "1.6.1" resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.6.1.tgz#f4781949cce402697127430ea3b3c5476f481767" @@ -1039,6 +1089,10 @@ stream-combiner@~0.0.4: dependencies: duplexer "~0.1.1" +stream-consume@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/stream-consume/-/stream-consume-0.1.0.tgz#a41ead1a6d6081ceb79f65b061901b6d8f3d1d0f" + string-width@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" From 7b8a24dab9fbd6235385a2795c046db49d74768e Mon Sep 17 00:00:00 2001 From: Klaus Meinhardt Date: Fri, 24 Feb 2017 21:23:39 +0100 Subject: [PATCH 114/131] Use tsutils package for utilities (#2217) --- package.json | 3 ++- src/enableDisableRules.ts | 28 ++++++++++----------- src/rules/adjacentOverloadSignaturesRule.ts | 26 +++---------------- src/rules/commentFormatRule.ts | 9 ++++--- src/rules/importBlacklistRule.ts | 14 +++-------- src/rules/jsdocFormatRule.ts | 7 +++--- src/rules/newlineBeforeReturnRule.ts | 7 ++---- src/rules/noTrailingWhitespaceRule.ts | 19 +++++++------- src/rules/noUnnecessaryInitializerRule.ts | 7 ++---- src/rules/noUnnecessaryQualifierRule.ts | 8 ++---- src/rules/noUnsafeAnyRule.ts | 10 +++----- src/rules/noUnsafeFinallyRule.ts | 7 ++---- src/rules/typedefRule.ts | 9 +++---- src/rules/unifiedSignaturesRule.ts | 5 ++-- src/rules/whitespaceRule.ts | 5 ++-- yarn.lock | 10 +++++--- 16 files changed, 70 insertions(+), 104 deletions(-) diff --git a/package.json b/package.json index f3d2159a643..f6aa25fa63c 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "glob": "^7.1.1", "optimist": "~0.6.0", "resolve": "^1.1.7", + "tsutils": "^1.0.0", "update-notifier": "^2.0.0" }, "peerDependencies": { @@ -70,7 +71,7 @@ "rimraf": "^2.5.4", "tslint": "latest", "tslint-test-config-non-relative": "file:test/external/tslint-test-config-non-relative", - "typescript": "2.1.4" + "typescript": "^2.1.6" }, "license": "Apache-2.0", "engines": { diff --git a/src/enableDisableRules.ts b/src/enableDisableRules.ts index 031ad1af737..7f6cb0279e9 100644 --- a/src/enableDisableRules.ts +++ b/src/enableDisableRules.ts @@ -15,10 +15,10 @@ * limitations under the License. */ +import * as utils from "tsutils"; import * as ts from "typescript"; import {AbstractRule} from "./language/rule/abstractRule"; -import {forEachComment, TokenPosition} from "./language/utils"; import {IEnableDisablePosition} from "./ruleLoader"; export class EnableDisableRulesWalker { @@ -42,11 +42,11 @@ export class EnableDisableRulesWalker { } public getEnableDisableRuleMap() { - forEachComment(this.sourceFile, (fullText, kind, pos) => { - const commentText = kind === ts.SyntaxKind.SingleLineCommentTrivia - ? fullText.substring(pos.tokenStart + 2, pos.end) - : fullText.substring(pos.tokenStart + 2, pos.end - 2); - return this.handleComment(commentText, pos); + utils.forEachComment(this.sourceFile, (fullText, comment) => { + const commentText = comment.kind === ts.SyntaxKind.SingleLineCommentTrivia + ? fullText.substring(comment.pos + 2, comment.end) + : fullText.substring(comment.pos + 2, comment.end - 2); + return this.handleComment(commentText, comment); }); return this.enableDisableRuleMap; @@ -85,7 +85,7 @@ export class EnableDisableRulesWalker { } } - private handleComment(commentText: string, pos: TokenPosition) { + private handleComment(commentText: string, range: ts.TextRange) { // regex is: start of string followed by any amount of whitespace // followed by tslint and colon // followed by either "enable" or "disable" @@ -110,32 +110,32 @@ export class EnableDisableRulesWalker { rulesList = this.enabledRules; } - this.handleTslintLineSwitch(rulesList, match[1] === "enable", match[2], pos); + this.handleTslintLineSwitch(rulesList, match[1] === "enable", match[2], range); } } - private handleTslintLineSwitch(rules: string[], isEnabled: boolean, modifier: string, pos: TokenPosition) { + private handleTslintLineSwitch(rules: string[], isEnabled: boolean, modifier: string, range: ts.TextRange) { let start: number | undefined; let end: number | undefined; if (modifier === "line") { // start at the beginning of the line where comment starts - start = this.getStartOfLinePosition(pos.tokenStart)!; + start = this.getStartOfLinePosition(range.pos)!; // end at the beginning of the line following the comment - end = this.getStartOfLinePosition(pos.end, 1); + end = this.getStartOfLinePosition(range.end, 1); } else if (modifier === "next-line") { // start at the beginning of the line following the comment - start = this.getStartOfLinePosition(pos.end, 1); + start = this.getStartOfLinePosition(range.end, 1); if (start === undefined) { // no need to switch anything, there is no next line return; } // end at the beginning of the line following the next line - end = this.getStartOfLinePosition(pos.end, 2); + end = this.getStartOfLinePosition(range.end, 2); } else { // switch rule for the rest of the file // start at the current position, but skip end position - start = pos.tokenStart; + start = range.pos; end = undefined; } diff --git a/src/rules/adjacentOverloadSignaturesRule.ts b/src/rules/adjacentOverloadSignaturesRule.ts index c44db5ee8ba..34036d8464f 100644 --- a/src/rules/adjacentOverloadSignaturesRule.ts +++ b/src/rules/adjacentOverloadSignaturesRule.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import * as utils from "tsutils"; import * as ts from "typescript"; import * as Lint from "../index"; @@ -114,32 +115,13 @@ interface Overload { name: string; } -function isLiteralExpression(node: ts.Node): node is ts.LiteralExpression { - return node.kind === ts.SyntaxKind.StringLiteral || node.kind === ts.SyntaxKind.NumericLiteral; -} - export function getOverloadKey(node: ts.SignatureDeclaration): string | undefined { const o = getOverload(node); return o && o.key; } function getOverloadIfSignature(node: ts.TypeElement | ts.ClassElement): Overload | undefined { - return isSignatureDeclaration(node) ? getOverload(node) : undefined; -} - -export function isSignatureDeclaration(node: ts.Node): node is ts.SignatureDeclaration { - switch (node.kind) { - case ts.SyntaxKind.ConstructSignature: - case ts.SyntaxKind.Constructor: - case ts.SyntaxKind.CallSignature: - case ts.SyntaxKind.CallSignature: - case ts.SyntaxKind.MethodSignature: - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.FunctionDeclaration: - return true; - default: - return false; - } + return utils.isSignatureDeclaration(node) ? getOverload(node) : undefined; } function getOverload(node: ts.SignatureDeclaration): Overload | undefined { @@ -173,8 +155,8 @@ function getPropertyInfo(name: ts.PropertyName): { name: string, computed?: bool return { name: (name as ts.Identifier).text }; case ts.SyntaxKind.ComputedPropertyName: const { expression } = (name as ts.ComputedPropertyName); - return isLiteralExpression(expression) ? { name: expression.text } : { name: expression.getText(), computed: true }; + return utils.isLiteralExpression(expression) ? { name: expression.text } : { name: expression.getText(), computed: true }; default: - return isLiteralExpression(name) ? { name: (name as ts.StringLiteral).text } : undefined; + return utils.isLiteralExpression(name) ? { name: (name as ts.StringLiteral).text } : undefined; } } diff --git a/src/rules/commentFormatRule.ts b/src/rules/commentFormatRule.ts index 995ec565203..12604bd1d3d 100644 --- a/src/rules/commentFormatRule.ts +++ b/src/rules/commentFormatRule.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import * as utils from "tsutils"; import * as ts from "typescript"; import * as Lint from "../index"; @@ -117,10 +118,10 @@ class CommentWalker extends Lint.RuleWalker { } public visitSourceFile(node: ts.SourceFile) { - Lint.forEachComment(node, (fullText, kind, pos) => { - if (kind === ts.SyntaxKind.SingleLineCommentTrivia) { - const commentText = fullText.substring(pos.tokenStart, pos.end); - const startPosition = pos.tokenStart + 2; + utils.forEachComment(node, (fullText, comment) => { + if (comment.kind === ts.SyntaxKind.SingleLineCommentTrivia) { + const commentText = fullText.substring(comment.pos, comment.end); + const startPosition = comment.pos + 2; const width = commentText.length - 2; if (this.hasOption(OPTION_SPACE)) { if (!startsWithSpace(commentText)) { diff --git a/src/rules/importBlacklistRule.ts b/src/rules/importBlacklistRule.ts index 73350c779cf..fd8d97c17e6 100644 --- a/src/rules/importBlacklistRule.ts +++ b/src/rules/importBlacklistRule.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import * as utils from "tsutils"; import * as ts from "typescript"; import * as Lint from "../index"; @@ -66,7 +67,7 @@ class ImportBlacklistWalker extends Lint.RuleWalker { } public visitImportEqualsDeclaration(node: ts.ImportEqualsDeclaration) { - if (isExternalModuleReference(node.moduleReference) && + if (utils.isExternalModuleReference(node.moduleReference) && node.moduleReference.expression !== undefined) { // If it's an import require and not an import alias this.checkForBannedImport(node.moduleReference.expression); @@ -80,7 +81,7 @@ class ImportBlacklistWalker extends Lint.RuleWalker { } private checkForBannedImport(expression: ts.Expression) { - if (isStringLiteral(expression) && this.hasOption(expression.text)) { + if (utils.isTextualLiteral(expression) && this.hasOption(expression.text)) { this.addFailureFromStartToEnd( expression.getStart(this.getSourceFile()) + 1, expression.getEnd() - 1, @@ -89,12 +90,3 @@ class ImportBlacklistWalker extends Lint.RuleWalker { } } } - -function isStringLiteral(node: ts.Node): node is ts.LiteralExpression { - return node.kind === ts.SyntaxKind.StringLiteral || - node.kind === ts.SyntaxKind.NoSubstitutionTemplateLiteral; -} - -function isExternalModuleReference(node: ts.ModuleReference): node is ts.ExternalModuleReference { - return node.kind === ts.SyntaxKind.ExternalModuleReference; -} diff --git a/src/rules/jsdocFormatRule.ts b/src/rules/jsdocFormatRule.ts index f6c9b0ebe24..9645c1db607 100644 --- a/src/rules/jsdocFormatRule.ts +++ b/src/rules/jsdocFormatRule.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import * as utils from "tsutils"; import * as ts from "typescript"; import * as Lint from "../index"; @@ -50,9 +51,9 @@ export class Rule extends Lint.Rules.AbstractRule { class JsdocWalker extends Lint.RuleWalker { public visitSourceFile(node: ts.SourceFile) { - Lint.forEachComment(node, (fullText, kind, pos) => { - if (kind === ts.SyntaxKind.MultiLineCommentTrivia) { - this.findFailuresForJsdocComment(fullText.substring(pos.tokenStart, pos.end), pos.tokenStart); + utils.forEachComment(node, (fullText, comment) => { + if (comment.kind === ts.SyntaxKind.MultiLineCommentTrivia) { + this.findFailuresForJsdocComment(fullText.substring(comment.pos, comment.end), comment.pos); } }); } diff --git a/src/rules/newlineBeforeReturnRule.ts b/src/rules/newlineBeforeReturnRule.ts index 3465652c6bf..f94c99103be 100644 --- a/src/rules/newlineBeforeReturnRule.ts +++ b/src/rules/newlineBeforeReturnRule.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import * as utils from "tsutils"; import * as ts from "typescript"; import * as Lint from "../index"; @@ -47,7 +48,7 @@ class NewlineBeforeReturnWalker extends Lint.RuleWalker { super.visitReturnStatement(node); const parent = node.parent!; - if (!isBlockLike(parent)) { + if (!utils.isBlockLike(parent)) { // `node` is the only statement within this "block scope". No need to do any further validation. return; } @@ -75,7 +76,3 @@ class NewlineBeforeReturnWalker extends Lint.RuleWalker { } } } - -function isBlockLike(node: ts.Node): node is ts.BlockLike { - return "statements" in node; -} diff --git a/src/rules/noTrailingWhitespaceRule.ts b/src/rules/noTrailingWhitespaceRule.ts index a6b80204bcf..a1193ec9c5a 100644 --- a/src/rules/noTrailingWhitespaceRule.ts +++ b/src/rules/noTrailingWhitespaceRule.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import * as utils from "tsutils"; import * as ts from "typescript"; import * as Lint from "../index"; @@ -73,29 +74,29 @@ export class Rule extends Lint.Rules.AbstractRule { function walk(ctx: Lint.WalkContext) { let lastSeenWasWhitespace = false; let lastSeenWhitespacePosition = 0; - Lint.forEachToken(ctx.sourceFile, false, (fullText, kind, pos) => { + utils.forEachTokenWithTrivia(ctx.sourceFile, (fullText, kind, range) => { if (kind === ts.SyntaxKind.NewLineTrivia || kind === ts.SyntaxKind.EndOfFileToken) { if (lastSeenWasWhitespace) { - reportFailure(ctx, lastSeenWhitespacePosition, pos.tokenStart); + reportFailure(ctx, lastSeenWhitespacePosition, range.pos); } lastSeenWasWhitespace = false; } else if (kind === ts.SyntaxKind.WhitespaceTrivia) { lastSeenWasWhitespace = true; - lastSeenWhitespacePosition = pos.tokenStart; + lastSeenWhitespacePosition = range.pos; } else { if (ctx.options !== IgnoreOption.Comments) { if (kind === ts.SyntaxKind.SingleLineCommentTrivia) { - const commentText = fullText.substring(pos.tokenStart + 2, pos.end); + const commentText = fullText.substring(range.pos + 2, range.end); const match = /\s+$/.exec(commentText); if (match !== null) { - reportFailure(ctx, pos.end - match[0].length, pos.end); + reportFailure(ctx, range.end - match[0].length, range.end); } } else if (kind === ts.SyntaxKind.MultiLineCommentTrivia && (ctx.options !== IgnoreOption.JsDoc || - fullText[pos.tokenStart + 2] !== "*" || - fullText[pos.tokenStart + 3] === "*")) { - let startPos = pos.tokenStart + 2; - const commentText = fullText.substring(startPos, pos.end - 2); + fullText[range.pos + 2] !== "*" || + fullText[range.pos + 3] === "*")) { + let startPos = range.pos + 2; + const commentText = fullText.substring(startPos, range.end - 2); const lines = commentText.split("\n"); // we don't want to check the content of the last comment line, as it is always followed by */ const len = lines.length - 1; diff --git a/src/rules/noUnnecessaryInitializerRule.ts b/src/rules/noUnnecessaryInitializerRule.ts index f722b3f1ed3..c3fda70d632 100644 --- a/src/rules/noUnnecessaryInitializerRule.ts +++ b/src/rules/noUnnecessaryInitializerRule.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import * as utils from "tsutils"; import * as ts from "typescript"; import * as Lint from "../index"; @@ -45,7 +46,7 @@ export class Rule extends Lint.Rules.AbstractRule { class Walker extends Lint.RuleWalker { public visitVariableDeclaration(node: ts.VariableDeclaration) { - if (isBindingPattern(node.name)) { + if (utils.isBindingPattern(node.name)) { for (const elem of node.name.elements) { if (elem.kind === ts.SyntaxKind.BindingElement) { this.checkInitializer(elem); @@ -115,7 +116,3 @@ function isUndefined(node: ts.Node | undefined): boolean { node.kind === ts.SyntaxKind.Identifier && (node as ts.Identifier).originalKeywordKind === ts.SyntaxKind.UndefinedKeyword; } - -function isBindingPattern(node: ts.Node): node is ts.ArrayBindingPattern | ts.ObjectBindingPattern { - return node.kind === ts.SyntaxKind.ArrayBindingPattern || node.kind === ts.SyntaxKind.ObjectBindingPattern; -} diff --git a/src/rules/noUnnecessaryQualifierRule.ts b/src/rules/noUnnecessaryQualifierRule.ts index d2c1e5c7bcb..8d4e4232c7a 100644 --- a/src/rules/noUnnecessaryQualifierRule.ts +++ b/src/rules/noUnnecessaryQualifierRule.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import * as utils from "tsutils"; import * as ts from "typescript"; import * as Lint from "../index"; @@ -67,7 +68,7 @@ class Walker extends Lint.ProgramAwareRuleWalker { break; case ts.SyntaxKind.PropertyAccessExpression: const { expression, name } = node as ts.PropertyAccessExpression; - if (isEntityNameExpression(expression)) { + if (utils.isEntityNameExpression(expression)) { this.visitNamespaceAccess(node, expression, name); break; } @@ -126,11 +127,6 @@ class Walker extends Lint.ProgramAwareRuleWalker { } } -function isEntityNameExpression(expr: ts.Expression): expr is ts.EntityNameExpression { - return expr.kind === ts.SyntaxKind.Identifier || - expr.kind === ts.SyntaxKind.PropertyAccessExpression && isEntityNameExpression((expr as ts.PropertyAccessExpression).expression); -} - // TODO: Should just be `===`. See https://github.com/palantir/tslint/issues/1969 function nodesAreEqual(a: ts.Node, b: ts.Node) { return a.pos === b.pos; diff --git a/src/rules/noUnsafeAnyRule.ts b/src/rules/noUnsafeAnyRule.ts index 90e7fe16f87..14851adf82e 100644 --- a/src/rules/noUnsafeAnyRule.ts +++ b/src/rules/noUnsafeAnyRule.ts @@ -42,9 +42,12 @@ export class Rule extends Lint.Rules.TypedRule { } } +// This is marked @internal, but we need it! +const isExpression: (node: ts.Node) => node is ts.Expression = (ts as any).isExpression; + class Walker extends Lint.ProgramAwareRuleWalker { public visitNode(node: ts.Node) { - if (ts.isExpression(node) && isAny(this.getType(node)) && !this.isAllowedLocation(node)) { + if (isExpression(node) && isAny(this.getType(node)) && !this.isAllowedLocation(node)) { this.addFailureAtNode(node, Rule.FAILURE_STRING); } else { super.visitNode(node); @@ -111,8 +114,3 @@ class Walker extends Lint.ProgramAwareRuleWalker { function isAny(type: ts.Type | undefined): boolean { return type !== undefined && Lint.isTypeFlagSet(type, ts.TypeFlags.Any); } - -// This is marked @internal, but we need it! -declare module "typescript" { - export function isExpression(node: ts.Node): node is ts.Expression; -} diff --git a/src/rules/noUnsafeFinallyRule.ts b/src/rules/noUnsafeFinallyRule.ts index f6044afff0a..6c84729b15b 100644 --- a/src/rules/noUnsafeFinallyRule.ts +++ b/src/rules/noUnsafeFinallyRule.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import * as utils from "tsutils"; import * as ts from "typescript"; import * as Lint from "../index"; @@ -192,14 +193,10 @@ function isFinallyBlock(node: ts.Node): boolean { return parent !== undefined && node.kind === ts.SyntaxKind.Block && - isTryStatement(parent) && + utils.isTryStatement(parent) && parent.finallyBlock === node; } -function isTryStatement(node: ts.Node): node is ts.TryStatement { - return node.kind === ts.SyntaxKind.TryStatement; -} - function isReturnsOrThrowsBoundary(scope: IFinallyScope) { return scope.isReturnsThrowsBoundary; } diff --git a/src/rules/typedefRule.ts b/src/rules/typedefRule.ts index 0e5326faf17..81a41e133b7 100644 --- a/src/rules/typedefRule.ts +++ b/src/rules/typedefRule.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import * as utils from "tsutils"; import * as ts from "typescript"; import * as Lint from "../index"; @@ -139,7 +140,7 @@ class TypedefWalker extends Lint.RuleWalker { let optionName: string | null = null; if (isArrowFunction && isTypedPropertyDeclaration(node.parent.parent)) { // leave optionName as null and don't perform check - } else if (isArrowFunction && isPropertyDeclaration(node.parent.parent)) { + } else if (isArrowFunction && utils.isPropertyDeclaration(node.parent.parent)) { optionName = "member-variable-declaration"; } else if (isArrowFunction) { optionName = "arrow-parameter"; @@ -259,10 +260,6 @@ function getName(name?: ts.Node, prefix?: string, suffix?: string): string { return ns ? `${prefix || ""}${ns}${suffix || ""}` : ""; } -function isPropertyDeclaration(node: ts.Node): node is ts.PropertyDeclaration { - return node.kind === ts.SyntaxKind.PropertyDeclaration; -} - function isTypedPropertyDeclaration(node: ts.Node): boolean { - return isPropertyDeclaration(node) && node.type != null; + return utils.isPropertyDeclaration(node) && node.type != null; } diff --git a/src/rules/unifiedSignaturesRule.ts b/src/rules/unifiedSignaturesRule.ts index 8d844604ddf..b9dec69a282 100644 --- a/src/rules/unifiedSignaturesRule.ts +++ b/src/rules/unifiedSignaturesRule.ts @@ -15,12 +15,13 @@ * limitations under the License. */ +import * as utils from "tsutils"; import * as ts from "typescript"; import * as Lint from "../index"; import { arraysAreEqual, Equal } from "../utils"; -import { getOverloadKey, isSignatureDeclaration } from "./adjacentOverloadSignaturesRule"; +import { getOverloadKey } from "./adjacentOverloadSignaturesRule"; export class Rule extends Lint.Rules.AbstractRule { /* tslint:disable:object-literal-sort-keys */ @@ -94,7 +95,7 @@ class Walker extends Lint.RuleWalker { private checkMembers(members: Array, typeParameters?: ts.TypeParameterDeclaration[]) { this.checkOverloads(members, getOverloadName, typeParameters); function getOverloadName(member: ts.TypeElement | ts.ClassElement) { - if (!isSignatureDeclaration(member) || (member as ts.MethodDeclaration).body) { + if (!utils.isSignatureDeclaration(member) || (member as ts.MethodDeclaration).body) { return undefined; } const key = getOverloadKey(member); diff --git a/src/rules/whitespaceRule.ts b/src/rules/whitespaceRule.ts index 711693cf97f..0df3e3ee239 100644 --- a/src/rules/whitespaceRule.ts +++ b/src/rules/whitespaceRule.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import * as utils from "tsutils"; import * as ts from "typescript"; import * as Lint from "../index"; @@ -80,7 +81,7 @@ class WhitespaceWalker extends Lint.RuleWalker { super.visitSourceFile(node); let prevTokenShouldBeFollowedByWhitespace = false; - Lint.forEachToken(node, false, (_text, tokenKind, pos, parent) => { + utils.forEachTokenWithTrivia(node, (_text, tokenKind, range, parent) => { if (tokenKind === ts.SyntaxKind.WhitespaceTrivia || tokenKind === ts.SyntaxKind.NewLineTrivia || tokenKind === ts.SyntaxKind.EndOfFileToken) { @@ -88,7 +89,7 @@ class WhitespaceWalker extends Lint.RuleWalker { prevTokenShouldBeFollowedByWhitespace = false; return; } else if (prevTokenShouldBeFollowedByWhitespace) { - this.addMissingWhitespaceErrorAt(pos.tokenStart); + this.addMissingWhitespaceErrorAt(range.pos); prevTokenShouldBeFollowedByWhitespace = false; } // check for trailing space after the given tokens diff --git a/yarn.lock b/yarn.lock index 05c81b8ff31..56c622efc10 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1184,6 +1184,10 @@ tslint@latest: resolve "^1.1.7" update-notifier "^1.0.2" +tsutils@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-1.0.0.tgz#a1f87d1092179987d13a1a8406deaec28ac3a0ad" + type-detect@0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-0.1.1.tgz#0ba5ec2a885640e470ea4e8505971900dac58822" @@ -1192,9 +1196,9 @@ type-detect@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-1.0.0.tgz#762217cc06db258ec48908a1298e8b95121e8ea2" -typescript@2.1.4: - version "2.1.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.1.4.tgz#b53b69fb841126acb1dd4b397d21daba87572251" +typescript@^2.1.6: + version "2.1.6" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.1.6.tgz#40c7e6e9e5da7961b7718b55505f9cac9487a607" unique-string@^1.0.0: version "1.0.0" From 5a17c46ab9d86db4d5c418147635e9b80334294f Mon Sep 17 00:00:00 2001 From: Yuichi Nukiyama Date: Sat, 25 Feb 2017 05:26:40 +0900 Subject: [PATCH 115/131] add fix function to noConsecutiveBlankLines (#2201) --- src/rules/noConsecutiveBlankLinesRule.ts | 7 ++++- .../default/test.ts.fix | 27 +++++++++++++++++++ .../multiple/test.ts.fix | 27 +++++++++++++++++++ 3 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 test/rules/no-consecutive-blank-lines/default/test.ts.fix create mode 100644 test/rules/no-consecutive-blank-lines/multiple/test.ts.fix diff --git a/src/rules/noConsecutiveBlankLinesRule.ts b/src/rules/noConsecutiveBlankLinesRule.ts index c763d0ba267..1203f4c8115 100644 --- a/src/rules/noConsecutiveBlankLinesRule.ts +++ b/src/rules/noConsecutiveBlankLinesRule.ts @@ -27,6 +27,7 @@ export class Rule extends Lint.Rules.AbstractRule { public static metadata: Lint.IRuleMetadata = { ruleName: "no-consecutive-blank-lines", description: "Disallows one or more blank lines in a row.", + hasFix: true, rationale: "Helps maintain a readable style in your codebase.", optionsDescription: Lint.Utils.dedent` An optional number of maximum allowed sequential blanks can be specified. If no value @@ -97,10 +98,14 @@ class NoConsecutiveBlankLinesWalker extends Lint.RuleWalker { } const startLineNum = arr[0]; + const endLineNum = arr[arr.length - allowedBlanks]; const pos = lineStarts[startLineNum + 1]; + const end = lineStarts[endLineNum]; const isInTemplate = templateIntervals.some((interval) => pos >= interval.startPosition && pos < interval.endPosition); + if (!isInTemplate) { - this.addFailureAt(pos, 1, failureMessage); + const fix = this.createFix(this.deleteFromTo(pos, end)); + this.addFailureAt(pos, 1, failureMessage, fix); } } } diff --git a/test/rules/no-consecutive-blank-lines/default/test.ts.fix b/test/rules/no-consecutive-blank-lines/default/test.ts.fix new file mode 100644 index 00000000000..2f010efa7ed --- /dev/null +++ b/test/rules/no-consecutive-blank-lines/default/test.ts.fix @@ -0,0 +1,27 @@ +// the markup for this test is a little bit weird +// tslint, for the first error below, says it goes from +// [5, 1] to [6, 1], and thus the markup appears to be off (but it's not) + +class Clazz { // comment + + public funcxion() { + + // also comment + + console.log("test"); + + } + +} + +//Begin whitespace +// The next two lines of "code" contain only tabs or spaces, they are also considered "blank" lines + +let foo = ` + + +`; + +let bar = `${bar + +}`; diff --git a/test/rules/no-consecutive-blank-lines/multiple/test.ts.fix b/test/rules/no-consecutive-blank-lines/multiple/test.ts.fix new file mode 100644 index 00000000000..545c0d4c1c2 --- /dev/null +++ b/test/rules/no-consecutive-blank-lines/multiple/test.ts.fix @@ -0,0 +1,27 @@ +// the markup for this test is a little bit weird +// tslint, for the first error below, says it goes from +// [5, 1] to [6, 1], and thus the markup appears to be off (but it's not) + +class Clazz { // comment + + + public funcxion() { + + // also comment + + + // still allowed since 2 lines only + + + // this one won't be allowed anymore + + console.log("test"); + + } + + +} + +//Begin whitespace +// The next lines contain only tabs or spaces, they are also considered "blank" lines + From 894d0e0429882ad37df441547befddc633067907 Mon Sep 17 00:00:00 2001 From: Klaus Meinhardt Date: Fri, 24 Feb 2017 21:32:44 +0100 Subject: [PATCH 116/131] Rewrite no-switch-case-fallthrough (#2218) [bugfix] handle break, throw, continue and return nested in block, if-else and switch [bugfix] allow empty case clauses before default clause [enhancement] allow single line comment `// falls through` --- src/rules/noSwitchCaseFallThroughRule.ts | 87 +++++++++---------- .../no-switch-case-fall-through/test.js.lint | 3 - .../no-switch-case-fall-through/test.ts.lint | 52 ++++++++++- 3 files changed, 91 insertions(+), 51 deletions(-) diff --git a/src/rules/noSwitchCaseFallThroughRule.ts b/src/rules/noSwitchCaseFallThroughRule.ts index 562db2e5c5e..b93ff9f605a 100644 --- a/src/rules/noSwitchCaseFallThroughRule.ts +++ b/src/rules/noSwitchCaseFallThroughRule.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import * as utils from "tsutils"; import * as ts from "typescript"; import * as Lint from "../index"; @@ -61,59 +62,57 @@ export class Rule extends Lint.Rules.AbstractRule { public static FAILURE_STRING_PART = "expected a 'break' before "; public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { - return this.applyWithWalker(new NoSwitchCaseFallThroughWalker(sourceFile, this.getOptions())); + return this.applyWithWalker(new NoSwitchCaseFallThroughWalker(sourceFile, this.ruleName, undefined)); } } -export class NoSwitchCaseFallThroughWalker extends Lint.RuleWalker { - public visitSwitchStatement(node: ts.SwitchStatement) { - let isFallingThrough = false; - - // get the position for the first case statement - const switchClauses = node.caseBlock.clauses; - switchClauses.forEach((child, i) => { - const kind = child.kind; - if (kind === ts.SyntaxKind.CaseClause) { - const switchClause = child as ts.CaseClause; - isFallingThrough = fallsThrough(switchClause.statements); - // no break statements and no statements means the fallthrough is expected. - // last item doesn't need a break - if (isFallingThrough && switchClause.statements.length > 0 && ((switchClauses.length - 1) > i)) { - if (!isFallThroughAllowed(this.getSourceFile(), switchClauses[i + 1])) { - this.addFailureAt(switchClauses[i + 1].getStart(), "case".length, `${Rule.FAILURE_STRING_PART}'case'`); - } - } - } else { - // case statement falling through a default - if (isFallingThrough && !isFallThroughAllowed(this.getSourceFile(), child)) { - this.addFailureAt(switchClauses[i].getStart(), "default".length, Rule.FAILURE_STRING_PART + "'default'"); - } +export class NoSwitchCaseFallThroughWalker extends Lint.AbstractWalker { + public walk(sourceFile: ts.SourceFile) { + const cb = (node: ts.Node): void => { + if (node.kind === ts.SyntaxKind.SwitchStatement) { + this.visitSwitchStatement(node as ts.SwitchStatement); } - }); - super.visitSwitchStatement(node); + return ts.forEachChild(node, cb); + }; + return ts.forEachChild(sourceFile, cb); } -} -function fallsThrough(statements: ts.NodeArray) { - return !statements.some((statement) => { - return statement.kind === ts.SyntaxKind.BreakStatement - || statement.kind === ts.SyntaxKind.ThrowStatement - || statement.kind === ts.SyntaxKind.ReturnStatement - || statement.kind === ts.SyntaxKind.ContinueStatement; - }); -} + private visitSwitchStatement(node: ts.SwitchStatement) { + const clauses = node.caseBlock.clauses; + const len = clauses.length - 1; // last clause doesn't need to be checked + for (let i = 0; i < len; ++i) { + if (clauses[i].statements.length !== 0 && + // TODO type assertion can be removed with typescript 2.2 + !utils.endsControlFlow(clauses[i] as ts.CaseClause) && + !this.isFallThroughAllowed(clauses[i])) { -function isFallThroughAllowed(sourceFile: ts.SourceFile, nextCaseOrDefaultStatement: ts.Node) { - const sourceFileText = sourceFile.text; - const firstChild = nextCaseOrDefaultStatement.getChildAt(0); - const commentRanges = ts.getLeadingCommentRanges(sourceFileText, firstChild.getFullStart()); - if (commentRanges != null) { - for (const commentRange of commentRanges) { - const commentText = sourceFileText.substring(commentRange.pos, commentRange.end); - if (commentText === "/* falls through */") { + this.reportError(clauses[i + 1]); + } + } + } + + private isFallThroughAllowed(clause: ts.CaseOrDefaultClause) { + const sourceFileText = this.sourceFile.text; + const comments = ts.getLeadingCommentRanges(sourceFileText, clause.end); + if (comments === undefined) { + return false; + } + for (const comment of comments) { + let commentText: string; + if (comment.kind === ts.SyntaxKind.MultiLineCommentTrivia) { + commentText = sourceFileText.substring(comment.pos + 2, comment.end - 2); + } else { + commentText = sourceFileText.substring(comment.pos + 2, comment.end); + } + if (commentText.trim() === "falls through") { return true; } } + return false; + } + + private reportError(clause: ts.CaseOrDefaultClause) { + const keyword = clause.kind === ts.SyntaxKind.CaseClause ? "case" : "default"; + this.addFailureAt(clause.getStart(this.sourceFile), keyword.length, `${Rule.FAILURE_STRING_PART}'${keyword}'`); } - return false; } diff --git a/test/rules/no-switch-case-fall-through/test.js.lint b/test/rules/no-switch-case-fall-through/test.js.lint index 1dc40e8627b..0c53e2a05b0 100644 --- a/test/rules/no-switch-case-fall-through/test.js.lint +++ b/test/rules/no-switch-case-fall-through/test.js.lint @@ -9,7 +9,6 @@ switch (foo) { ~~~~ [expected a 'break' before 'case'] case 4: default: - ~~~~~~~ [expected a 'break' before 'default'] break; } @@ -27,7 +26,6 @@ switch (foo) { case 1: case 2: default: - ~~~~~~~ [expected a 'break' before 'default'] bar(); } @@ -36,7 +34,6 @@ switch (foo) { switch (bar) { case "": default: - ~~~~~~~ [expected a 'break' before 'default'] break; } case 2: diff --git a/test/rules/no-switch-case-fall-through/test.ts.lint b/test/rules/no-switch-case-fall-through/test.ts.lint index 1dc40e8627b..9a37931a68e 100644 --- a/test/rules/no-switch-case-fall-through/test.ts.lint +++ b/test/rules/no-switch-case-fall-through/test.ts.lint @@ -9,7 +9,6 @@ switch (foo) { ~~~~ [expected a 'break' before 'case'] case 4: default: - ~~~~~~~ [expected a 'break' before 'default'] break; } @@ -27,7 +26,6 @@ switch (foo) { case 1: case 2: default: - ~~~~~~~ [expected a 'break' before 'default'] bar(); } @@ -36,7 +34,6 @@ switch (foo) { switch (bar) { case "": default: - ~~~~~~~ [expected a 'break' before 'default'] break; } case 2: @@ -72,8 +69,11 @@ switch (foo) { bar(); /* Testing */ /* falls through */ + default: + bar(); + // falls through case 3: - break; + break; } // valid @@ -85,3 +85,47 @@ switch (foo) { break; default: } + +// recognise blocks +switch (foo) { + case 1: { + break; + } + case 2: +} + +// recognise if-else +switch (foo) { + case 1: + if (bar) { + break; + } else { + return; + } + case 2: + if (bar) { + break; + } + case 3: + ~~~~ [expected a 'break' before 'case'] +} + +// allow throw +switch (foo) { + case 1: + throw bar; + case 2: +} + +// handle nested switch statements +switch (foo) { + case 1: + switch (bar) { + case 1: + foobar(); // this case clause falls through and returns in the default clause + default: + ~~~~~~~ [expected a 'break' before 'default'] + return; + } + case 2: +} \ No newline at end of file From c61d641f85f92238f8299b6e6d25a219e7ac6ca8 Mon Sep 17 00:00:00 2001 From: Noah Chen Date: Fri, 24 Feb 2017 15:34:36 -0500 Subject: [PATCH 117/131] fix lint error --- src/rules/noConsecutiveBlankLinesRule.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rules/noConsecutiveBlankLinesRule.ts b/src/rules/noConsecutiveBlankLinesRule.ts index 1203f4c8115..e2a76efb4b7 100644 --- a/src/rules/noConsecutiveBlankLinesRule.ts +++ b/src/rules/noConsecutiveBlankLinesRule.ts @@ -102,7 +102,7 @@ class NoConsecutiveBlankLinesWalker extends Lint.RuleWalker { const pos = lineStarts[startLineNum + 1]; const end = lineStarts[endLineNum]; const isInTemplate = templateIntervals.some((interval) => pos >= interval.startPosition && pos < interval.endPosition); - + if (!isInTemplate) { const fix = this.createFix(this.deleteFromTo(pos, end)); this.addFailureAt(pos, 1, failureMessage, fix); From 3dbd010c6dc575f23efa532c9fdedc74050cc2ef Mon Sep 17 00:00:00 2001 From: Klaus Meinhardt Date: Sun, 26 Feb 2017 06:11:29 +0100 Subject: [PATCH 118/131] Fix no-consecutive-blank-lines (#2239) Correctly apply fixes at EOF. Fix tests. Simplify collecting consecutive blank lines. --- package.json | 2 +- src/rules/noConsecutiveBlankLinesRule.ts | 123 +++++++++--------- .../{ => default}/test.js.lint | 0 .../default/test.ts.fix | 4 +- .../multiple/test.ts.fix | 7 +- .../multiple/test.ts.lint | 10 +- yarn.lock | 6 +- 7 files changed, 75 insertions(+), 77 deletions(-) rename test/rules/no-consecutive-blank-lines/{ => default}/test.js.lint (100%) diff --git a/package.json b/package.json index f6aa25fa63c..5c52a5d0e28 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "glob": "^7.1.1", "optimist": "~0.6.0", "resolve": "^1.1.7", - "tsutils": "^1.0.0", + "tsutils": "^1.1.0", "update-notifier": "^2.0.0" }, "peerDependencies": { diff --git a/src/rules/noConsecutiveBlankLinesRule.ts b/src/rules/noConsecutiveBlankLinesRule.ts index e2a76efb4b7..2d1321065d8 100644 --- a/src/rules/noConsecutiveBlankLinesRule.ts +++ b/src/rules/noConsecutiveBlankLinesRule.ts @@ -15,13 +15,13 @@ * limitations under the License. */ +import * as utils from "tsutils"; import * as ts from "typescript"; import * as Lint from "../index"; export class Rule extends Lint.Rules.AbstractRule { public static DEFAULT_ALLOWED_BLANKS = 1; - public static MINIMUM_ALLOWED_BLANKS = 1; /* tslint:disable:object-literal-sort-keys */ public static metadata: Lint.IRuleMetadata = { @@ -46,84 +46,81 @@ export class Rule extends Lint.Rules.AbstractRule { return allowed === 1 ? "Consecutive blank lines are forbidden" : `Exceeds the ${allowed} allowed consecutive blank lines`; - }; + } /** * Disable the rule if the option is provided but non-numeric or less than the minimum. */ public isEnabled(): boolean { - if (!super.isEnabled()) { - return false; - } - const options = this.getOptions(); - const allowedBlanks = options.ruleArguments && options.ruleArguments[0] || Rule.DEFAULT_ALLOWED_BLANKS; - return typeof allowedBlanks === "number" && allowedBlanks >= Rule.MINIMUM_ALLOWED_BLANKS; + return super.isEnabled() && + (!this.ruleArguments[0] || + typeof this.ruleArguments[0] === "number" && this.ruleArguments[0] > 0); } public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { - return this.applyWithWalker(new NoConsecutiveBlankLinesWalker(sourceFile, this.getOptions())); + const limit: number = this.ruleArguments[0] || Rule.DEFAULT_ALLOWED_BLANKS; + return this.applyWithFunction(sourceFile, walk, limit); } } -class NoConsecutiveBlankLinesWalker extends Lint.RuleWalker { - public walk(node: ts.SourceFile) { - const templateIntervals = this.getTemplateIntervals(node); - const lineStarts = node.getLineStarts(); +function walk(ctx: Lint.WalkContext) { + const sourceText = ctx.sourceFile.text; + const threshold = ctx.options + 1; + const possibleFailures: ts.TextRange[] = []; + let consecutiveBlankLines = 0; - const options = this.getOptions(); - const allowedBlanks = options && options[0] || Rule.DEFAULT_ALLOWED_BLANKS; - const failureMessage = Rule.FAILURE_STRING_FACTORY(allowedBlanks); - const sourceFileText = node.getFullText(); - const soureFileLines = sourceFileText.split(/\n/); - - // find all the lines that are blank or only contain whitespace - const blankLineIndexes: number[] = []; - soureFileLines.forEach((txt, i) => { - if (txt.trim() === "") { - blankLineIndexes.push(i); + for (const line of utils.getLineRanges(ctx.sourceFile)) { + if (sourceText.substring(line.pos, line.end).search(/\S/) === -1) { + ++consecutiveBlankLines; + if (consecutiveBlankLines === threshold) { + possibleFailures.push({ + end: line.end, + pos: line.pos, + }); + } else if (consecutiveBlankLines > threshold) { + possibleFailures[possibleFailures.length - 1].end = line.end; } - }); - - // now only throw failures for the fisrt number from groups of consecutive blank line indexes - const sequences: number[][] = []; - let lastVal = -2; - for (const line of blankLineIndexes) { - line > lastVal + 1 ? sequences.push([line]) : sequences[sequences.length - 1].push(line); - lastVal = line; + } else { + consecutiveBlankLines = 0; } + } - for (const arr of sequences) { - if (arr.length <= allowedBlanks) { - continue; - } - - const startLineNum = arr[0]; - const endLineNum = arr[arr.length - allowedBlanks]; - const pos = lineStarts[startLineNum + 1]; - const end = lineStarts[endLineNum]; - const isInTemplate = templateIntervals.some((interval) => pos >= interval.startPosition && pos < interval.endPosition); - - if (!isInTemplate) { - const fix = this.createFix(this.deleteFromTo(pos, end)); - this.addFailureAt(pos, 1, failureMessage, fix); - } + if (possibleFailures.length === 0) { + return; + } + const failureString = Rule.FAILURE_STRING_FACTORY(ctx.options); + const templateRanges = getTemplateRanges(ctx.sourceFile); + for (const possibleFailure of possibleFailures) { + if (!templateRanges.some((template) => template.pos < possibleFailure.pos && possibleFailure.pos < template.end)) { + ctx.addFailureAt(possibleFailure.pos, 1, failureString, ctx.createFix( + Lint.Replacement.deleteFromTo( + // special handling for fixing blank lines at the end of the file + // to fix this we need to cut off the line break of the last allowed blank line, too + possibleFailure.end === sourceText.length ? getStartOfLineBreak(sourceText, possibleFailure.pos) : possibleFailure.pos, + possibleFailure.end, + ), + )); } } +} - private getTemplateIntervals(sourceFile: ts.SourceFile): Lint.IDisabledInterval[] { - const intervals: Lint.IDisabledInterval[] = []; - const cb = (node: ts.Node) => { - if (node.kind >= ts.SyntaxKind.FirstTemplateToken && - node.kind <= ts.SyntaxKind.LastTemplateToken) { - intervals.push({ - endPosition: node.getEnd(), - startPosition: node.getStart(sourceFile), - }); - } else { - ts.forEachChild(node, cb); - } - }; - ts.forEachChild(sourceFile, cb); - return intervals; - } +function getStartOfLineBreak(sourceText: string, pos: number) { + return sourceText[pos - 2] === "\r" ? pos - 1 : pos - 1; +} + +function getTemplateRanges(sourceFile: ts.SourceFile): ts.TextRange[] { + const intervals: ts.TextRange[] = []; + const cb = (node: ts.Node): void => { + if (node.kind >= ts.SyntaxKind.FirstTemplateToken && + node.kind <= ts.SyntaxKind.LastTemplateToken) { + intervals.push({ + end: node.end, + pos: node.getStart(sourceFile), + }); + } else { + return ts.forEachChild(node, cb); + } + }; + ts.forEachChild(sourceFile, cb); + return intervals; } diff --git a/test/rules/no-consecutive-blank-lines/test.js.lint b/test/rules/no-consecutive-blank-lines/default/test.js.lint similarity index 100% rename from test/rules/no-consecutive-blank-lines/test.js.lint rename to test/rules/no-consecutive-blank-lines/default/test.js.lint diff --git a/test/rules/no-consecutive-blank-lines/default/test.ts.fix b/test/rules/no-consecutive-blank-lines/default/test.ts.fix index 2f010efa7ed..119e8e98967 100644 --- a/test/rules/no-consecutive-blank-lines/default/test.ts.fix +++ b/test/rules/no-consecutive-blank-lines/default/test.ts.fix @@ -16,12 +16,12 @@ class Clazz { // comment //Begin whitespace // The next two lines of "code" contain only tabs or spaces, they are also considered "blank" lines - + let foo = ` `; let bar = `${bar - + }`; diff --git a/test/rules/no-consecutive-blank-lines/multiple/test.ts.fix b/test/rules/no-consecutive-blank-lines/multiple/test.ts.fix index 545c0d4c1c2..c1ba96dee2e 100644 --- a/test/rules/no-consecutive-blank-lines/multiple/test.ts.fix +++ b/test/rules/no-consecutive-blank-lines/multiple/test.ts.fix @@ -11,8 +11,8 @@ class Clazz { // comment // still allowed since 2 lines only - - + + // this one won't be allowed anymore console.log("test"); @@ -24,4 +24,5 @@ class Clazz { // comment //Begin whitespace // The next lines contain only tabs or spaces, they are also considered "blank" lines - + + \ No newline at end of file diff --git a/test/rules/no-consecutive-blank-lines/multiple/test.ts.lint b/test/rules/no-consecutive-blank-lines/multiple/test.ts.lint index b81bc3c41d7..5f05126c895 100644 --- a/test/rules/no-consecutive-blank-lines/multiple/test.ts.lint +++ b/test/rules/no-consecutive-blank-lines/multiple/test.ts.lint @@ -5,22 +5,22 @@ class Clazz { // comment + ~nil ~nil [Exceeds the 2 allowed consecutive blank lines] - public funcxion() { // also comment // still allowed since 2 lines only - + + ~nil - -~nil [Exceeds the 2 allowed consecutive blank lines] +~nil [Exceeds the 2 allowed consecutive blank lines] // this one won't be allowed anymore @@ -35,6 +35,6 @@ class Clazz { // comment // The next lines contain only tabs or spaces, they are also considered "blank" lines -~ [Exceeds the 2 allowed consecutive blank lines] +~ [Exceeds the 2 allowed consecutive blank lines] diff --git a/yarn.lock b/yarn.lock index 56c622efc10..8beda8980be 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1184,9 +1184,9 @@ tslint@latest: resolve "^1.1.7" update-notifier "^1.0.2" -tsutils@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-1.0.0.tgz#a1f87d1092179987d13a1a8406deaec28ac3a0ad" +tsutils@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-1.1.0.tgz#94e0c267624eeb1b63561ba8ec0bcff71b4e2872" type-detect@0.1.1: version "0.1.1" From 991d586133a6c548f7a18ba4e0d928cade1180a2 Mon Sep 17 00:00:00 2001 From: Adi Dahiya Date: Sun, 26 Feb 2017 22:49:56 -0500 Subject: [PATCH 119/131] Upgrade to typescript 2.2.1 (#2254) --- package.json | 2 +- .../promises/test.ts.lint | 10 ++++----- yarn.lock | 21 ++++++++++--------- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index 5c52a5d0e28..3987176ea6c 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "rimraf": "^2.5.4", "tslint": "latest", "tslint-test-config-non-relative": "file:test/external/tslint-test-config-non-relative", - "typescript": "^2.1.6" + "typescript": "^2.2.1" }, "license": "Apache-2.0", "engines": { diff --git a/test/rules/no-floating-promises/promises/test.ts.lint b/test/rules/no-floating-promises/promises/test.ts.lint index baeb2b5c3dd..115fc066f27 100644 --- a/test/rules/no-floating-promises/promises/test.ts.lint +++ b/test/rules/no-floating-promises/promises/test.ts.lint @@ -1,16 +1,16 @@ -class Promise { - then(): Promise; +class Promise { + then(): Promise; } function returnsPromiseFunction() { - return new Promise(); + return new Promise(); } -const returnsPromiseVariable = () => new Promise(); +const returnsPromiseVariable = () => new Promise(); class ReturnsPromiseClass { returnsPromiseMemberFunction() { - return new Promise(); + return new Promise(); } returnsPromiseMemberVariable = () => new Promise(); diff --git a/yarn.lock b/yarn.lock index 8beda8980be..f1996647356 100644 --- a/yarn.lock +++ b/yarn.lock @@ -374,9 +374,6 @@ event-stream@~3.3.0: stream-combiner "~0.0.4" through "~2.3.1" -extend@3, extend@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.0.tgz#5a474353b9f3353ddd8176dfd37b91c83a46f1d4" execa@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/execa/-/execa-0.4.0.tgz#4eb6467a36a095fabb2970ff9d5e3fb7bce6ebc3" @@ -388,6 +385,10 @@ execa@^0.4.0: path-key "^1.0.0" strip-eof "^1.0.0" +extend@3, extend@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.0.tgz#5a474353b9f3353ddd8176dfd37b91c83a46f1d4" + filled-array@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/filled-array/-/filled-array-1.1.0.tgz#c3c4f6c663b923459a9aa29912d2d031f1507f84" @@ -428,6 +429,10 @@ function-bind@^1.0.2, function-bind@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.0.tgz#16176714c801798e4e8f2cf7f7529467bb4a5771" +get-stream@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" + github@^8.1.1: version "8.1.1" resolved "https://registry.yarnpkg.com/github/-/github-8.1.1.tgz#a078c61669b4d4b588bf1b2e2a591eb7c49feb36" @@ -436,10 +441,6 @@ github@^8.1.1: https-proxy-agent "^1.0.0" mime "^1.2.11" netrc "^0.1.4" - -get-stream@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" glob@7.0.5: version "7.0.5" @@ -1196,9 +1197,9 @@ type-detect@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-1.0.0.tgz#762217cc06db258ec48908a1298e8b95121e8ea2" -typescript@^2.1.6: - version "2.1.6" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.1.6.tgz#40c7e6e9e5da7961b7718b55505f9cac9487a607" +typescript@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.2.1.tgz#4862b662b988a4c8ff691cc7969622d24db76ae9" unique-string@^1.0.0: version "1.0.0" From b683d83f2a1e153d3f2dd020f9cfb48edaad12eb Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Sun, 26 Feb 2017 19:51:01 -0800 Subject: [PATCH 120/131] Remove Eclipse settings file (#2246) --- .settings/com.palantir.typescript.prefs | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 .settings/com.palantir.typescript.prefs diff --git a/.settings/com.palantir.typescript.prefs b/.settings/com.palantir.typescript.prefs deleted file mode 100644 index 6c6e8bfd460..00000000000 --- a/.settings/com.palantir.typescript.prefs +++ /dev/null @@ -1,13 +0,0 @@ -build.path.exportedFolder= -build.path.sourceFolder=src;test;typings -compiler.codeGenTarget=ECMASCRIPT5 -compiler.compileOnSave=false -compiler.generateDeclarationFiles=false -compiler.mapSourceFiles=false -compiler.moduleGenTarget=SYNCHRONOUS -compiler.noImplicitAny=false -compiler.noLib=false -compiler.outputDirOption= -compiler.outputFileOption= -compiler.removeComments=false -eclipse.preferences.version=1 From 39ec195031b8e51865e71fbdfd431fdfeda15bd9 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Sun, 26 Feb 2017 19:51:43 -0800 Subject: [PATCH 121/131] Update vscode settings (#2250) --- .vscode/settings.json | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index aa07ee373b5..5e603e317dc 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,18 +6,19 @@ "editor.insertSpaces": true, // Controls after how many characters the editor will wrap to the next line. Setting this to 0 turns on viewport width wrapping - "editor.wrappingColumn": 140, + "editor.wordWrap": "bounded", + "editor.wordWrapColumn": 140, // The folders to exclude when doing a full text search in the workspace. - "search.excludeFolders": [ - ".git", - ".tscache", - "bower_components", - "bin", - "build", - "lib", - "node_modules" - ], + "files.exclude": { + ".git": true, + ".tscache": true, + "bower_components": true, + "bin": true, + "build": true, + "lib": true, + "node_modules": true + }, // Always use project's provided typescript compiler version "typescript.tsdk": "node_modules/typescript/lib" From ba3cfa06b12810076b74ccf88b798b883a04d4b3 Mon Sep 17 00:00:00 2001 From: Adi Dahiya Date: Sun, 26 Feb 2017 22:56:56 -0500 Subject: [PATCH 122/131] Use 4-space indentation in vscode settings.json --- .vscode/settings.json | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 5e603e317dc..662622c43ec 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,25 +1,25 @@ { - // Controls the rendering size of tabs in characters. If set to auto, the value will be guessed based on the opened file. - "editor.tabSize": 4, + // Controls the rendering size of tabs in characters. If set to auto, the value will be guessed based on the opened file. + "editor.tabSize": 4, - // Controls if the editor will insert spaces for tabs. If set to auto, the value will be guessed based on the opened file. - "editor.insertSpaces": true, + // Controls if the editor will insert spaces for tabs. If set to auto, the value will be guessed based on the opened file. + "editor.insertSpaces": true, - // Controls after how many characters the editor will wrap to the next line. Setting this to 0 turns on viewport width wrapping - "editor.wordWrap": "bounded", - "editor.wordWrapColumn": 140, + // Controls after how many characters the editor will wrap to the next line. Setting this to 0 turns on viewport width wrapping + "editor.wordWrap": "bounded", + "editor.wordWrapColumn": 140, - // The folders to exclude when doing a full text search in the workspace. - "files.exclude": { - ".git": true, - ".tscache": true, - "bower_components": true, - "bin": true, - "build": true, - "lib": true, - "node_modules": true - }, + // The folders to exclude when doing a full text search in the workspace. + "files.exclude": { + ".git": true, + ".tscache": true, + "bower_components": true, + "bin": true, + "build": true, + "lib": true, + "node_modules": true + }, - // Always use project's provided typescript compiler version - "typescript.tsdk": "node_modules/typescript/lib" + // Always use project's provided typescript compiler version + "typescript.tsdk": "node_modules/typescript/lib" } From 12435bfb265af17fe195ece3161e225860a33d29 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Sun, 26 Feb 2017 20:09:28 -0800 Subject: [PATCH 123/131] completed-docs: Handle undefined symbol (#2230) --- src/rules/completedDocsRule.ts | 7 ++++++- test/rules/completed-docs/defaults/test.ts.lint | 4 ++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/rules/completedDocsRule.ts b/src/rules/completedDocsRule.ts index 02e75d15652..17dc0a44584 100644 --- a/src/rules/completedDocsRule.ts +++ b/src/rules/completedDocsRule.ts @@ -392,7 +392,12 @@ class CompletedDocsWalker extends Lint.ProgramAwareRuleWalker { return; } - const comments = this.getTypeChecker().getSymbolAtLocation(node.name).getDocumentationComment(); + const symbol = this.getTypeChecker().getSymbolAtLocation(node.name); + if (!symbol) { + return; + } + + const comments = symbol.getDocumentationComment(); this.checkComments(node, nodeType, comments); } diff --git a/test/rules/completed-docs/defaults/test.ts.lint b/test/rules/completed-docs/defaults/test.ts.lint index d77d0af495b..e712dd95807 100644 --- a/test/rules/completed-docs/defaults/test.ts.lint +++ b/test/rules/completed-docs/defaults/test.ts.lint @@ -5,6 +5,10 @@ export class Aaa { public ccc() { } ~~~~~~~~~~~~~~~~ [Documentation must exist for public methods.] + + // TODO: TypeScript API doesn't give us a symbol for this, so we must ignore it. + // https://github.com/Microsoft/TypeScript/issues/14257 + [Symbol.iterator]() {} } export enum Ddd { } From 4385b2dc54e6f86410433471067ef80993ede76c Mon Sep 17 00:00:00 2001 From: Martin Probst Date: Mon, 27 Feb 2017 17:50:38 +0100 Subject: [PATCH 124/131] Un-deprecate no-unused-variable rule (#2256) --- src/rules/noUnusedVariableRule.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/rules/noUnusedVariableRule.ts b/src/rules/noUnusedVariableRule.ts index 437cfd1e53a..95b88c0c47f 100644 --- a/src/rules/noUnusedVariableRule.ts +++ b/src/rules/noUnusedVariableRule.ts @@ -31,8 +31,9 @@ export class Rule extends Lint.Rules.AbstractRule { /* tslint:disable:object-literal-sort-keys */ public static metadata: Lint.IRuleMetadata = { ruleName: "no-unused-variable", - deprecationMessage: "Use the tsc compiler options --noUnusedParameters and --noUnusedLocals instead.", - description: "Disallows unused imports, variables, functions and private class members.", + description: Lint.Utils.dedent`Disallows unused imports, variables, functions and + private class members. Similar to tsc's --noUnusedParameters and --noUnusedLocals + options, but does not interrupt code compilation.`, hasFix: true, optionsDescription: Lint.Utils.dedent` Three optional arguments may be optionally provided: From 187b67e6ae3192662f1837c293753449df5aa013 Mon Sep 17 00:00:00 2001 From: Noah Chen Date: Mon, 27 Feb 2017 13:59:19 -0500 Subject: [PATCH 125/131] Prepare for v4.5.0 (#2258) --- CHANGELOG.md | 128 +++++++++++++++++++++++++++++++++++--------------- package.json | 2 +- src/linter.ts | 2 +- 3 files changed, 91 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ddccb9fbabb..69532cfc2b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,56 @@ Change Log === +v4.5.0 +--- + +- [new-rule] `no-import-side-effect` (#2155) +- [new-rule] `match-default-export-name` (#2117) +- [new-rule] `no-non-null-assertion` (#2221) +- [new-rule] `ban-types` (#2175) +- [new-rule] `no-duplicate-super` (#2038) +- [new-rule] `newline-before-return` (#2173) +- [new-rule-option] `completed-docs` adds options for location, type, and privacy. Also adds interfaces, enums, types (#2095) +- [new-rule-option] `no-inferrable-types` adds option `ignore-properties` (#2178) +- [new-rule-option] `typedef` adds options `object-destructuring` and `array-destructuring` options (#2146) +- [new-rule-option] `member-ordering` adds option `alphabetize` (#2101) +- [new-rule-option] `no-trailing-whitespace` adds option `ignore-jsdoc` (#2177) +- [new-rule-option] `no-trailing-whitespace` adds option `ignore-comments` option (#2153) +- [new-fixer] `no-inferrable-types` automatically remove inferrable type annotations (#2178) +- [new-fixer] `no-any` (#2165) +- [new-fixer] `noConsecutiveBlankLines` (#2201) +- [new-fixer] `object-literal-shorthand` (#2165) +- [bugfix] `no-switch-case-fallthrough` handle break, throw, continue and return nested in block, if-else and switch (#2218) +- [bugfix] `no-switch-case-fallthrough` allow empty case clauses before default clause (#2218) +- [bugfix] `no-mergeable-namespace` ignore property types that can't be merged (#2105) +- [bugfix] `object-literal-key-quotes` no need to quote a float if its .toString() is the same. (#2144) +- [bugfix] `no-consecutive-blank-lines` Correctly apply fixes at EOF (#2239) +- [bugfix]: Fixes installation issue with node 7.5 (#2212) +- [bugfix]: `quotemark` now handles escaped chars (#2224) +- [enhancement] Don't exit when a rule requires type checking but type checking is not enabled (#2188) +- [enhancement] `no-switch-case-fallthrough` allow single line comment `// falls through` (#2218) +- [enhancement] `no-unbound-method` allows property access and binary expressions (#2143) + +Thanks to our contributors! + +- Andy Hanson +- Stefan Reichel +- Shlomi Assaf +- Josh Goldberg +- Minko Gechev +- Irfan Hudda +- Klaus Meinhardt +- Martin Probst +- Naoto Usuyama +- Caleb Eggensperger +- Arturs Vonda +- Joscha Feth +- Moritz +- Alexander Rusakov +- Alex Ryan +- Andy +- Yuichi Nukiyama + v4.4.2 --- @@ -37,7 +87,7 @@ v4.4.0 * [bugfix] `no-empty-interface` allow empty interface with 2 or more parents (#2070) * [bugfix] `no-trailing-whitespace` fixed for comments and EOF (#2060) * [bugfix] `no-empty` no longer fails for private or protected constructor (#1976) -* [bugfix] `tslint:disable`/`tslint-enable` now handles multiple rules and fixes what code is enabled/disabled (#2061) +* [bugfix] `tslint:disable`/`tslint-enable` now handles multiple rules and fixes what code is enabled/disabled (#2061) * [bugfix] `no-inferrable-types` now validates property declarations (#2081) * [bugfix] `unified-signatures` false positive (#2016) * [bugfix] `whitespace` finds all whitespace errors in JsxExpressions and TemplateExpressions (#2036) @@ -48,7 +98,7 @@ Thanks to our contributors! * Alexander Rusakov * Andrii Dieiev -* @andy-ms +* @andy-ms * Andy Hanson * Josh Goldberg * Kei Son @@ -92,7 +142,7 @@ v4.3.0 Thanks to our contributors! * Andrii Dieiev -* @andy-ms +* @andy-ms * Andy Hanson * Josh Goldberg * Klaus Meinhardt @@ -120,7 +170,7 @@ v4.2.0 Thanks to our contributors! * Andrew Scott -* @andy-ms +* @andy-ms * Andy Hanson * James Booth * Klaus Meinhardt @@ -234,8 +284,8 @@ v4.0.0-dev.1 --- * **BREAKING CHANGES** - * [enhancement] The `semicolon` rule now disallows semicolons in multi-line bound class methods - (to get the v3 behavior, use the `ignore-bound-class-methods` option) (#1643) + * [enhancement] The `semicolon` rule now disallows semicolons in multi-line bound class methods + (to get the v3 behavior, use the `ignore-bound-class-methods` option) (#1643) * [removed-rule] Removed `use-strict` rule (#678) * [removed-rule] Removed `label-undefined` rule; covered by compiler (#1614) * [enhancement] Renamed `no-constructor-vars` to `no-parameter-properties` (#1296) @@ -255,21 +305,21 @@ v4.0.0-dev.1 Thanks to our contributors! -* Alex Eagle -* Andrii Dieiev -* Ben Coveney -* Boris Aranovich -* Chris Barr -* Cyril Gandon -* Evgeniy Zhukovskiy -* Jay Anslow -* Kunal Marwaha -* Martin Probst -* Mingye Wang -* Raghav Katyal -* Sean Dawson -* Yuichi Nukiyama -* jakpaw +* Alex Eagle +* Andrii Dieiev +* Ben Coveney +* Boris Aranovich +* Chris Barr +* Cyril Gandon +* Evgeniy Zhukovskiy +* Jay Anslow +* Kunal Marwaha +* Martin Probst +* Mingye Wang +* Raghav Katyal +* Sean Dawson +* Yuichi Nukiyama +* jakpaw v4.0.0-dev.0 --- @@ -297,24 +347,24 @@ v4.0.0-dev.0 Thanks to our contributors! -* Alex Eagle -* Andrii Dieiev -* Andy Hanson -* Ben Coveney -* Boris Aranovich -* Chris Barr -* Christian Dreher -* Claas Augner -* Josh Goldberg -* Martin Probst -* Mike Deverell -* Nina Hartmann -* Satoshi Amemiya -* Scott Wu -* Steve Van Opstal -* Umar Bolatov -* Vladimir Matveev -* Yui +* Alex Eagle +* Andrii Dieiev +* Andy Hanson +* Ben Coveney +* Boris Aranovich +* Chris Barr +* Christian Dreher +* Claas Augner +* Josh Goldberg +* Martin Probst +* Mike Deverell +* Nina Hartmann +* Satoshi Amemiya +* Scott Wu +* Steve Van Opstal +* Umar Bolatov +* Vladimir Matveev +* Yui v3.15.1 --- diff --git a/package.json b/package.json index 3987176ea6c..e301095a471 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tslint", - "version": "4.4.2", + "version": "4.5.0", "description": "An extensible static analysis linter for the TypeScript language", "bin": { "tslint": "./bin/tslint" diff --git a/src/linter.ts b/src/linter.ts index d4b9cbb2294..c1bfd83c9eb 100644 --- a/src/linter.ts +++ b/src/linter.ts @@ -44,7 +44,7 @@ import { arrayify, dedent } from "./utils"; * Linter that can lint multiple files in consecutive runs. */ class Linter { - public static VERSION = "4.4.2"; + public static VERSION = "4.5.0"; public static findConfiguration = findConfiguration; public static findConfigurationPath = findConfigurationPath; From 9d8c4ed6b9167ca397e8f146b7431809a6d9886c Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Mon, 27 Feb 2017 18:08:10 -0800 Subject: [PATCH 126/131] Fix typo in custom rule dev docs (#2261) --- docs/develop/custom-rules/performance.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/develop/custom-rules/performance.md b/docs/develop/custom-rules/performance.md index 6b17c5cab45..51c4d6aba1a 100644 --- a/docs/develop/custom-rules/performance.md +++ b/docs/develop/custom-rules/performance.md @@ -97,7 +97,7 @@ Some examples: `node.getStart()` scans the source to skip all the leading trivia. Although barely noticeable, this operation is not for free. If you need the start position of a node more than once per function, consider caching it. -`node.getWidth()` is most of the time used together with `node.getStart()` to get the node's span. Internally it uses `node.getStart() - node.getEnd()` which effectively doubles the calls to `node.getStart()`. Consider using `node.getEnd()` instead and calculate the width yourself if necessary. +`node.getWidth()` is most of the time used together with `node.getStart()` to get the node's span. Internally it uses `node.getEnd() - node.getStart()` which effectively doubles the calls to `node.getStart()`. Consider using `node.getEnd()` instead and calculate the width yourself if necessary. `node.getText()` calculates the start of the node and returns a substring until the end of the token. Most of the time this not needed, because this substring is already contained in the node. From 85e04000665db3140bd4b76fa7d71eb15360ea59 Mon Sep 17 00:00:00 2001 From: Noah Chen Date: Mon, 27 Feb 2017 21:37:15 -0500 Subject: [PATCH 127/131] Update docs (#2259) - sync with changes made to gh-pages branch - Update release notes to note AbstractWalker --- .gitignore | 2 +- CHANGELOG.md | 2 ++ docs/_data/develop_sidebar.json | 8 ++++++++ docs/_sass/_cayman.scss | 6 +++++- docs/css/main.scss | 7 ++++--- docs/rules/index.html | 22 +++++++++++++++++++++ docs/rules/no-unused-this/index.html | 28 --------------------------- docs/usage/third-party-tools/index.md | 6 ++++++ scripts/generate-changelog.ts | 1 + 9 files changed, 49 insertions(+), 33 deletions(-) create mode 100644 docs/rules/index.html delete mode 100644 docs/rules/no-unused-this/index.html diff --git a/.gitignore b/.gitignore index ec83b7c5002..8a0d1147a6e 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,4 @@ typings/.basedir.ts *.swp docs/_data/rules.json -docs/rules/ +docs/rules/*/index.html diff --git a/CHANGELOG.md b/CHANGELOG.md index 69532cfc2b0..2ce247f59ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,8 @@ v4.5.0 - [enhancement] Don't exit when a rule requires type checking but type checking is not enabled (#2188) - [enhancement] `no-switch-case-fallthrough` allow single line comment `// falls through` (#2218) - [enhancement] `no-unbound-method` allows property access and binary expressions (#2143) +- [api] Introduce `AbstractWalker` for performance (#2093) + - see [performance] (https://palantir.github.io/tslint/develop/custom-rules/performance.html) and [migration] (https://palantir.github.io/tslint/develop/custom-rules/migration.html) docs Thanks to our contributors! diff --git a/docs/_data/develop_sidebar.json b/docs/_data/develop_sidebar.json index 1af6e2f92a8..d083ccfa97b 100644 --- a/docs/_data/develop_sidebar.json +++ b/docs/_data/develop_sidebar.json @@ -6,6 +6,14 @@ "title": "Custom Rules", "url": "/custom-rules/" }, + { + "title": "Custom Rules - Performance", + "url": "/custom-rules/performance.html" + }, + { + "title": "Custom Rules - Migrating to AbstractWalker", + "url": "/custom-rules/performance/migration.html" + }, { "title": "Custom Formatters", "url": "/custom-formatters/" diff --git a/docs/_sass/_cayman.scss b/docs/_sass/_cayman.scss index 41bbb75b37e..265338c51bc 100644 --- a/docs/_sass/_cayman.scss +++ b/docs/_sass/_cayman.scss @@ -39,6 +39,10 @@ a { &:hover { text-decoration: underline; } + + &:visited { + color: $link-visited-color; + } } h1 { font-size: 2.5rem; } @@ -493,4 +497,4 @@ float: left; @include small { float: left; } -} \ No newline at end of file +} diff --git a/docs/css/main.scss b/docs/css/main.scss index 75f9efad894..50119f13693 100755 --- a/docs/css/main.scss +++ b/docs/css/main.scss @@ -12,9 +12,10 @@ $base-font-size: 16px; $small-font-size: $base-font-size * 0.875; $base-line-height: 1.5; -$text-color: #606c71; -$link-color: #1e6bb8; -$heading-color: #159957; +$heading-color: #159957; +$text-color: #606c71; +$link-color: #1e6bb8; +$link-visited-color: #7d0ce8; // Import partials from `sass_dir` (defaults to `_sass`) @import diff --git a/docs/rules/index.html b/docs/rules/index.html new file mode 100644 index 00000000000..537271b7ba2 --- /dev/null +++ b/docs/rules/index.html @@ -0,0 +1,22 @@ +--- +layout: page +title: Rules +permalink: /rules/ +menu: main +order: 2 +--- +

TypeScript Specific

+

These rules find errors related to TypeScript features:

+{% include rule_list.html ruleType="typescript" %} + +

Functionality

+

These rules catch common errors in JS programming or otherwise confusing constructs that are prone to producing bugs:

+{% include rule_list.html ruleType="functionality" %} + +

Maintainability

+

These rules make code maintenance easier:

+{% include rule_list.html ruleType="maintainability" %} + +

Style

+

These rules enforce consistent style across your codebase:

+{% include rule_list.html ruleType="style" %} diff --git a/docs/rules/no-unused-this/index.html b/docs/rules/no-unused-this/index.html deleted file mode 100644 index 85b84209125..00000000000 --- a/docs/rules/no-unused-this/index.html +++ /dev/null @@ -1,28 +0,0 @@ ---- -ruleName: no-unused-this -description: Warns for methods that do not use 'this'. -optionsDescription: |- - - "allow-public" excludes public methods. - "allow-protected" excludes protected methods. -options: - type: string - enum: - - allow-public - - allow-protected -optionExamples: - - 'true' - - '[true, "allow-public", "allow-protected"]' -type: style -typescriptOnly: false -layout: rule -title: 'Rule: no-unused-this' -optionsJSON: |- - { - "type": "string", - "enum": [ - "allow-public", - "allow-protected" - ] - } ---- \ No newline at end of file diff --git a/docs/usage/third-party-tools/index.md b/docs/usage/third-party-tools/index.md index 661a1bc2c74..e11e3125de4 100644 --- a/docs/usage/third-party-tools/index.md +++ b/docs/usage/third-party-tools/index.md @@ -16,6 +16,8 @@ _Note: Most of these tools are not maintained by TSLint._ * [syntastic][10] ([VIM][11]) * [Web Analyzer][12] ([Visual Studio][13]) * [Webstorm][14] +* [mocha-tslint][15] ([Mocha][16]) +* [tslint.tmbundle][17] ([TextMate][18]) [0]: https://github.com/palantir/grunt-tslint @@ -33,3 +35,7 @@ _Note: Most of these tools are not maintained by TSLint._ [12]: https://visualstudiogallery.msdn.microsoft.com/6edc26d4-47d8-4987-82ee-7c820d79be1d [13]: https://www.visualstudio.com/ [14]: https://www.jetbrains.com/help/webstorm/2016.1/tslint.html +[15]: https://github.com/t-sauer/mocha-tslint +[16]: https://mochajs.org/ +[17]: https://github.com/natesilva/tslint.tmbundle +[18]: https://macromates.com diff --git a/scripts/generate-changelog.ts b/scripts/generate-changelog.ts index 049f3ef9d24..8fe98387ff9 100644 --- a/scripts/generate-changelog.ts +++ b/scripts/generate-changelog.ts @@ -48,6 +48,7 @@ const auth: GitHubApi.Auth = { }; console.log("Using OAuth token " + auth.token + "\n"); +// process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; // ignores TLS certificate error github.authenticate(auth); const commits: ICommit[] = []; From b37e5d8130b84b34659e7ea407151f9ee4c1efbe Mon Sep 17 00:00:00 2001 From: Noah Chen Date: Tue, 28 Feb 2017 11:17:47 -0500 Subject: [PATCH 128/131] `object-literal-key-quotes` flags negative number as not needing quotes (#2272) --- src/rules/objectLiteralKeyQuotesRule.ts | 2 +- test/rules/object-literal-key-quotes/as-needed/test.ts.fix | 1 + test/rules/object-literal-key-quotes/as-needed/test.ts.lint | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/rules/objectLiteralKeyQuotesRule.ts b/src/rules/objectLiteralKeyQuotesRule.ts index 9be27c2a570..d2609713481 100644 --- a/src/rules/objectLiteralKeyQuotesRule.ts +++ b/src/rules/objectLiteralKeyQuotesRule.ts @@ -155,7 +155,7 @@ function quotesAreInconsistent(properties: ts.ObjectLiteralElementLike[]): boole } function propertyNeedsQuotes(property: string): boolean { - return !IDENTIFIER_NAME_REGEX.test(property) && Number(property).toString() !== property; + return !IDENTIFIER_NAME_REGEX.test(property) && (Number(property).toString() !== property || property.startsWith("-")); } // This is simplistic. See https://mothereff.in/js-properties for the gorey details. diff --git a/test/rules/object-literal-key-quotes/as-needed/test.ts.fix b/test/rules/object-literal-key-quotes/as-needed/test.ts.fix index 9dd05ac5201..d4e6146b179 100644 --- a/test/rules/object-literal-key-quotes/as-needed/test.ts.fix +++ b/test/rules/object-literal-key-quotes/as-needed/test.ts.fix @@ -12,6 +12,7 @@ const o = { 1.23: null, '010': 'but this one does.', '.123': 'as does this one', + '-1': 'and this one', fn() { return }, true: 0, "0x0": 0, diff --git a/test/rules/object-literal-key-quotes/as-needed/test.ts.lint b/test/rules/object-literal-key-quotes/as-needed/test.ts.lint index 4a7facdccfe..3e4e2cefd43 100644 --- a/test/rules/object-literal-key-quotes/as-needed/test.ts.lint +++ b/test/rules/object-literal-key-quotes/as-needed/test.ts.lint @@ -15,6 +15,7 @@ const o = { 1.23: null, '010': 'but this one does.', '.123': 'as does this one', + '-1': 'and this one', fn() { return }, true: 0, "0x0": 0, From 9eae4d73ea20a645b8fbd18eb6c2f25b6729a18a Mon Sep 17 00:00:00 2001 From: Andy Date: Tue, 28 Feb 2017 09:10:08 -0800 Subject: [PATCH 129/131] Add "ban-types" to `tslint:latest`. (#2271) --- src/configs/latest.ts | 11 +++++++++++ src/formatterLoader.ts | 15 ++++----------- src/index.ts | 6 ++++-- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/configs/latest.ts b/src/configs/latest.ts index 0a10c824dec..9e8e743f687 100644 --- a/src/configs/latest.ts +++ b/src/configs/latest.ts @@ -52,6 +52,17 @@ export const rules = { "arrow-return-shorthand": true, "no-unnecessary-initializer": true, "no-misused-new": true, + + // added in v4.5 + "ban-types": [ + true, + ["Object", "Avoid using the `Object` type. Did you mean `object`?"], + ["Function", "Avoid using the `Function` type. Prefer a specific function type, like `() => void`."], + ["Boolean", "Avoid using the `Boolean` type. Did you mean `boolean`?"], + ["Number", "Avoid using the `Number` type. Did you mean `number`?"], + ["String", "Avoid using the `String` type. Did you mean `string`?"], + ["Symbol", "Avoid using the `Symbol` type. Did you mean `symbol`?"], + ], }; // tslint:enable object-literal-sort-keys diff --git a/src/formatterLoader.ts b/src/formatterLoader.ts index 942f40f8db9..5da7a24d56e 100644 --- a/src/formatterLoader.ts +++ b/src/formatterLoader.ts @@ -17,23 +17,16 @@ import * as fs from "fs"; import * as path from "path"; +import {FormatterFunction} from "./index"; import {camelize} from "./utils"; const moduleDirectory = path.dirname(module.filename); const CORE_FORMATTERS_DIRECTORY = path.resolve(moduleDirectory, ".", "formatters"); -function isFunction(variable: any): variable is Function { - return typeof variable === "function"; -} - -function isString(variable: any): variable is string { - return typeof variable === "string"; -} - -export function findFormatter(name: string | Function, formattersDirectory?: string) { - if (isFunction(name)) { +export function findFormatter(name: string | FormatterFunction, formattersDirectory?: string) { + if (typeof name === "function") { return name; - } else if (isString(name)) { + } else if (typeof name === "string") { name = name.trim(); const camelizedName = camelize(`${name}Formatter`); diff --git a/src/index.ts b/src/index.ts index faf6ede9279..835ac72fc15 100644 --- a/src/index.ts +++ b/src/index.ts @@ -38,13 +38,15 @@ export interface LintResult { failureCount: number; failures: RuleFailure[]; fixes?: RuleFailure[]; - format: string | Function; + format: string | FormatterFunction; output: string; } +export type FormatterFunction = (failures: RuleFailure[]) => string; + export interface ILinterOptions { fix: boolean; - formatter?: string | Function; + formatter?: string | FormatterFunction; formattersDirectory?: string; rulesDirectory?: string | string[]; } From 127d0fdf747c32e2cf10fca69d314e50128036a1 Mon Sep 17 00:00:00 2001 From: Noah Chen Date: Tue, 28 Feb 2017 13:52:35 -0500 Subject: [PATCH 130/131] Prepare release v4.5.1 (#2274) --- CHANGELOG.md | 6 ++++++ package.json | 2 +- src/configs/latest.ts | 1 + src/linter.ts | 2 +- 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ce247f59ea..f0e1947c46b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ Change Log === +v4.5.1 +--- + +- [enhancement] Updated recommended rules to include `ban-types` and `no-duplicate-super` (#2271) +- [bugfix] `object-literal-key-quotes` handle negative number property name (#2273) + v4.5.0 --- diff --git a/package.json b/package.json index e301095a471..57fba87e655 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tslint", - "version": "4.5.0", + "version": "4.5.1", "description": "An extensible static analysis linter for the TypeScript language", "bin": { "tslint": "./bin/tslint" diff --git a/src/configs/latest.ts b/src/configs/latest.ts index 9e8e743f687..928abec07e0 100644 --- a/src/configs/latest.ts +++ b/src/configs/latest.ts @@ -63,6 +63,7 @@ export const rules = { ["String", "Avoid using the `String` type. Did you mean `string`?"], ["Symbol", "Avoid using the `Symbol` type. Did you mean `symbol`?"], ], + "no-duplicate-super": true, }; // tslint:enable object-literal-sort-keys diff --git a/src/linter.ts b/src/linter.ts index c1bfd83c9eb..4e947e8f734 100644 --- a/src/linter.ts +++ b/src/linter.ts @@ -44,7 +44,7 @@ import { arrayify, dedent } from "./utils"; * Linter that can lint multiple files in consecutive runs. */ class Linter { - public static VERSION = "4.5.0"; + public static VERSION = "4.5.1"; public static findConfiguration = findConfiguration; public static findConfigurationPath = findConfigurationPath; From 55427368a5dd72a11edf6524216dd7d06c61f016 Mon Sep 17 00:00:00 2001 From: Adi Dahiya Date: Fri, 3 Mar 2017 19:21:44 -0500 Subject: [PATCH 131/131] Add typedef for stricter compiler check --- src/language/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/language/utils.ts b/src/language/utils.ts index 8baf0d1ce24..58c43f1d885 100644 --- a/src/language/utils.ts +++ b/src/language/utils.ts @@ -107,7 +107,7 @@ export function isBlockScopedBindingElement(node: ts.BindingElement): boolean { } export function getBindingElementVariableDeclaration(node: ts.BindingElement): ts.VariableDeclaration | null { - let currentParent = node.parent!; + let currentParent: ts.Node = node.parent!; while (currentParent.kind !== ts.SyntaxKind.VariableDeclaration) { if (currentParent.parent == null) { return null; // function parameter, no variable declaration