diff --git a/src/configs/all.ts b/src/configs/all.ts index 822a2f9e4ae..f10234898c3 100644 --- a/src/configs/all.ts +++ b/src/configs/all.ts @@ -195,6 +195,7 @@ export const rules = { "encoding": true, "file-name-casing": [true, "camel-case"], "import-spacing": true, + "increment-decrement": true, "interface-name": true, "interface-over-type-literal": true, "jsdoc-format": [true, "check-multiline-start"], diff --git a/src/rules/incrementDecrementRule.ts b/src/rules/incrementDecrementRule.ts new file mode 100644 index 00000000000..1baed03122e --- /dev/null +++ b/src/rules/incrementDecrementRule.ts @@ -0,0 +1,117 @@ +/** + * @license + * Copyright 2013 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 tsutils from "tsutils"; +import * as ts from "typescript"; + +import * as Lint from "../index"; + +interface Options { + allowPost: boolean; +} + +const OPTION_ALLOW_POST = "allow-post"; + +export class Rule extends Lint.Rules.AbstractRule { + public static metadata: Lint.IRuleMetadata = { + description: "Enforces using explicit += 1 or -= 1 operators.", + optionExamples: [ + true, + [true, OPTION_ALLOW_POST], + ], + options: { + items: { + enum: [OPTION_ALLOW_POST], + type: "string", + }, + maxLength: 1, + minLength: 0, + type: "array", + }, + optionsDescription: Lint.Utils.dedent` + If no arguments are provided, both pre- and post-unary operators are banned. + If \`"${OPTION_ALLOW_POST}"\` is provided, post-unary operators will be allowed. + `, + rationale: Lint.Utils.dedent` + It's easy to type +i or -i instead of --i or ++i, and won't always result in invalid code. + Prefer standardizing small arithmetic operations with the explicit += and -= operators. + `, + ruleName: "increment-decrement", + type: "style", + typescriptOnly: false, + }; + + public static FAILURE_STRING_FACTORY = (newOperatorText: string) => + `Use an explicit ${newOperatorText} operator.` + + public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { + const options: Options = { + allowPost: this.ruleArguments.indexOf(OPTION_ALLOW_POST) !== -1, + }; + + return this.applyWithFunction(sourceFile, walk, options); + } +} + +function walk(context: Lint.WalkContext) { + function createReplacement(node: ts.PostfixUnaryExpression | ts.PrefixUnaryExpression, newOperatorText: string): Lint.Replacement { + let text = `${node.operand.getText(context.sourceFile)} ${newOperatorText}`; + + if (node.parent !== undefined && tsutils.isBinaryExpression(node.parent)) { + text = `(${text})`; + } + + return Lint.Replacement.replaceNode(node, text); + } + + function complainOnNode(node: ts.PostfixUnaryExpression | ts.PrefixUnaryExpression) { + const newOperatorText = node.operator === ts.SyntaxKind.PlusPlusToken + ? "+= 1" + : "-= 1"; + const replacement = createReplacement(node, newOperatorText); + + const failure = Rule.FAILURE_STRING_FACTORY(newOperatorText); + + context.addFailureAtNode(node, failure, replacement); + } + + function checkPostfixUnaryExpression(node: ts.PostfixUnaryExpression): void { + if (!context.options.allowPost && isIncrementOrDecrementOperator(node.operator)) { + complainOnNode(node); + } + } + + function checkPrefixUnaryExpression(node: ts.PrefixUnaryExpression): void { + if (isIncrementOrDecrementOperator(node.operator)) { + complainOnNode(node); + } + } + + return ts.forEachChild(context.sourceFile, function callback(node: ts.Node): void { + if (tsutils.isPostfixUnaryExpression(node)) { + checkPostfixUnaryExpression(node); + } else if (tsutils.isPrefixUnaryExpression(node)) { + checkPrefixUnaryExpression(node); + } + + return ts.forEachChild(node, callback); + }); +} + +function isIncrementOrDecrementOperator(operator: ts.PostfixUnaryOperator | ts.PrefixUnaryOperator) { + return operator === ts.SyntaxKind.PlusPlusToken || operator === ts.SyntaxKind.MinusMinusToken; +} diff --git a/test/rules/increment-decrement/allow-post/test.ts.fix b/test/rules/increment-decrement/allow-post/test.ts.fix new file mode 100644 index 00000000000..ebac59985d4 --- /dev/null +++ b/test/rules/increment-decrement/allow-post/test.ts.fix @@ -0,0 +1,20 @@ +let x = 7; + +x += 1; +x++; + +x -= 1; +x--; + ++x; +-x; +x + 1; +x - 1; +1 + x; +1 - x; + +x + (x += 1); +x + x++; + +x - (x -= 1); +x - x--; diff --git a/test/rules/increment-decrement/allow-post/test.ts.lint b/test/rules/increment-decrement/allow-post/test.ts.lint new file mode 100644 index 00000000000..c27629a2cdb --- /dev/null +++ b/test/rules/increment-decrement/allow-post/test.ts.lint @@ -0,0 +1,26 @@ +let x = 7; + +++x; +~~~ [plus] +x++; + +--x; +~~~ [minus] +x--; + ++x; +-x; +x + 1; +x - 1; +1 + x; +1 - x; + +x + ++x; + ~~~ [plus] +x + x++; + +x - --x; + ~~~ [minus] +x - x--; +[plus]: Use an explicit += 1 operator. +[minus]: Use an explicit -= 1 operator. diff --git a/test/rules/increment-decrement/allow-post/tslint.json b/test/rules/increment-decrement/allow-post/tslint.json new file mode 100644 index 00000000000..b0964670126 --- /dev/null +++ b/test/rules/increment-decrement/allow-post/tslint.json @@ -0,0 +1,5 @@ +{ + "rules": { + "increment-decrement": [true, "allow-post"] + } +} diff --git a/test/rules/increment-decrement/default/test.ts.fix b/test/rules/increment-decrement/default/test.ts.fix new file mode 100644 index 00000000000..f69f47c9056 --- /dev/null +++ b/test/rules/increment-decrement/default/test.ts.fix @@ -0,0 +1,26 @@ +let x = 7; + +x += 1; +x += 1; + +x -= 1; +x -= 1; + ++x; +-x; +x + 1; +x - 1; +1 + x; +1 - x; + +x + (x += 1); +x + (x += 1); + +x - (x -= 1); +x - (x -= 1); + +(x += 1) + x; +(x += 1) + x; + +(x -= 1) - x; +(x -= 1) - x; diff --git a/test/rules/increment-decrement/default/test.ts.lint b/test/rules/increment-decrement/default/test.ts.lint new file mode 100644 index 00000000000..203f22a705f --- /dev/null +++ b/test/rules/increment-decrement/default/test.ts.lint @@ -0,0 +1,40 @@ +let x = 7; + +++x; +~~~ [plus] +x++; +~~~ [plus] + +--x; +~~~ [minus] +x--; +~~~ [minus] + ++x; +-x; +x + 1; +x - 1; +1 + x; +1 - x; + +x + ++x; + ~~~ [plus] +x + x++; + ~~~ [plus] + +x - --x; + ~~~ [minus] +x - x--; + ~~~ [minus] + +++x + x; +~~~ [plus] +x++ + x; +~~~ [plus] + +--x - x; +~~~ [minus] +x-- - x; +~~~ [minus] +[plus]: Use an explicit += 1 operator. +[minus]: Use an explicit -= 1 operator. diff --git a/test/rules/increment-decrement/default/tslint.json b/test/rules/increment-decrement/default/tslint.json new file mode 100644 index 00000000000..42c70f0b7ab --- /dev/null +++ b/test/rules/increment-decrement/default/tslint.json @@ -0,0 +1,5 @@ +{ + "rules": { + "increment-decrement": true + } +} diff --git a/tslint.json b/tslint.json index 59d34f6574c..3b2ce4e7f04 100644 --- a/tslint.json +++ b/tslint.json @@ -6,6 +6,7 @@ "rules": { // Don't want these "cyclomatic-complexity": false, + "increment-decrement": false, "newline-before-return": false, "no-parameter-properties": false, "no-parameter-reassignment": false,