diff --git a/src/configs/all.ts b/src/configs/all.ts index 5c6c78bb41d..628fcce31bd 100644 --- a/src/configs/all.ts +++ b/src/configs/all.ts @@ -133,6 +133,7 @@ export const rules = { "no-string-throw": true, "no-sparse-arrays": true, "no-submodule-imports": true, + "no-tautology-expression": true, "no-unbound-method": true, "no-unnecessary-class": { options: ["allow-empty-class"] }, "no-unsafe-any": true, diff --git a/src/rules/noTautologyExpressionRule.ts b/src/rules/noTautologyExpressionRule.ts new file mode 100644 index 00000000000..8ec352836ae --- /dev/null +++ b/src/rules/noTautologyExpressionRule.ts @@ -0,0 +1,101 @@ +/** + * @license + * Copyright 2019 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"; + +const TAUTOLOGY_DISCOVERED_ERROR_STRING = + "Both sides of this equality comparison are the same, so the expression is either a tautology or a contradiction."; +export class Rule extends Lint.Rules.AbstractRule { + public static metadata: Lint.IRuleMetadata = { + description: Lint.Utils.dedent` + Enforces that relational/equality binary operators does not take two equal variables/literals as operands. + Expression like 3 === 3, someVar === someVar, "1" > "1" are either a tautology or contradiction, and will produce an error. + `, + optionExamples: [true], + options: null, + optionsDescription: "Not configurable.", + rationale: `Clean redundant code and unnecessary comparison of objects and literals.`, + ruleName: "no-tautology-expression", + type: "functionality", + typescriptOnly: false, + }; + + public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { + return this.applyWithFunction(sourceFile, walk); + } +} + +function walk(context: Lint.WalkContext) { + const cb = (node: ts.Node): void => { + if (tsutils.isBinaryExpression(node) && isRelationalOrLogicalOperator(node.operatorToken)) { + if ( + (tsutils.isStringLiteral(node.left) && tsutils.isStringLiteral(node.right)) || + (tsutils.isNumericLiteral(node.left) && tsutils.isNumericLiteral(node.right)) + ) { + if (node.left.text === node.right.text) { + context.addFailureAtNode(node, TAUTOLOGY_DISCOVERED_ERROR_STRING); + } + } else if (tsutils.isIdentifier(node.left) && tsutils.isIdentifier(node.right)) { + if (node.left.text === node.right.text) { + context.addFailureAtNode(node, TAUTOLOGY_DISCOVERED_ERROR_STRING); + } + } else if ( + tsutils.isPropertyAccessExpression(node.left) && + tsutils.isPropertyAccessExpression(node.right) + ) { + if (node.left.expression.getText() === node.right.expression.getText()) { + if (node.left.name.text === node.right.name.text) { + context.addFailureAtNode(node, TAUTOLOGY_DISCOVERED_ERROR_STRING); + } + } + } else if ( + (isBooleanLiteral(node.left) && isBooleanLiteral(node.right)) || + (isNullLiteral(node.left) && isNullLiteral(node.right)) + ) { + context.addFailureAtNode(node, TAUTOLOGY_DISCOVERED_ERROR_STRING); + } + } + return ts.forEachChild(node, cb); + }; + return ts.forEachChild(context.sourceFile, cb); +} + +function isNullLiteral(node: ts.Node): node is ts.NullLiteral { + return node.kind === ts.SyntaxKind.NullKeyword; +} + +function isBooleanLiteral(node: ts.Node): node is ts.BooleanLiteral { + return node.kind === ts.SyntaxKind.TrueKeyword || node.kind === ts.SyntaxKind.FalseKeyword; +} + +function isRelationalOrLogicalOperator(operator: ts.BinaryOperatorToken): boolean { + return new Set([ + ts.SyntaxKind.LessThanToken, + ts.SyntaxKind.GreaterThanToken, + ts.SyntaxKind.LessThanEqualsToken, + ts.SyntaxKind.GreaterThanEqualsToken, + ts.SyntaxKind.EqualsEqualsToken, + ts.SyntaxKind.EqualsEqualsEqualsToken, + ts.SyntaxKind.ExclamationEqualsToken, + ts.SyntaxKind.ExclamationEqualsEqualsToken, + ts.SyntaxKind.AmpersandAmpersandToken, + ts.SyntaxKind.BarBarToken, + ]).has(operator.kind); +} diff --git a/test/rules/no-tautology-expression/test.ts.lint b/test/rules/no-tautology-expression/test.ts.lint new file mode 100644 index 00000000000..09a5057a7cf --- /dev/null +++ b/test/rules/no-tautology-expression/test.ts.lint @@ -0,0 +1,159 @@ + +const expr = "someStr" === "someStr"; + ~~~~~~~~~~~~~~~~~~~~~~~ [Both sides of this equality comparison are the same, so the expression is either a tautology or a contradiction.] +const expr = 123 === 123; + ~~~~~~~~~~~ [Both sides of this equality comparison are the same, so the expression is either a tautology or a contradiction.] +const someVar = 100; +const expr = someVar === someVar; + ~~~~~~~~~~~~~~~~~~~ [Both sides of this equality comparison are the same, so the expression is either a tautology or a contradiction.] + +const someFunc = () => { + if ("SomeStr" === "SomeStr") { + ~~~~~~~~~~~~~~~~~~~~~~~ [Both sides of this equality comparison are the same, so the expression is either a tautology or a contradiction.] + return true; + } +} + +const someFunc = () => { + if (100 === 100) { + ~~~~~~~~~~~ [Both sides of this equality comparison are the same, so the expression is either a tautology or a contradiction.] + return true; + } +} + +const someFunc = () => { + let someVar = 100; + if (someVar === someVar) { + ~~~~~~~~~~~~~~~~~~~ [Both sides of this equality comparison are the same, so the expression is either a tautology or a contradiction.] + return true; + } +} + +const someFunc = () => { + if (1 !== 1) { + ~~~~~~~ [Both sides of this equality comparison are the same, so the expression is either a tautology or a contradiction.] + return true; + } +} + +const someFunc = () => { + if (1 > 1) { + ~~~~~ [Both sides of this equality comparison are the same, so the expression is either a tautology or a contradiction.] + return true; + } +} + +const someFunc = () => { + const someVar = 123; + if (someVar < someVar) { + ~~~~~~~~~~~~~~~~~ [Both sides of this equality comparison are the same, so the expression is either a tautology or a contradiction.] + return true; + } +} + +const someFunc = () => { + if ("str" == "str") { + ~~~~~~~~~~~~~~ [Both sides of this equality comparison are the same, so the expression is either a tautology or a contradiction.] + return true; + } +} + +const someFunc = () => { + if (123 != 123) { + ~~~~~~~~~~ [Both sides of this equality comparison are the same, so the expression is either a tautology or a contradiction.] + return true; + } +} + +const someFunc = () => { + if ("str" <= "str") { + ~~~~~~~~~~~~~~ [Both sides of this equality comparison are the same, so the expression is either a tautology or a contradiction.] + return true; + } +} + +const someFunc = () => { + if ("str" >= "str") { + ~~~~~~~~~~~~~~ [Both sides of this equality comparison are the same, so the expression is either a tautology or a contradiction.] + return true; + } +} + +const someFunc = () => { + const someVar = true; + if (someVar || someVar) { + ~~~~~~~~~~~~~~~~~~ [Both sides of this equality comparison are the same, so the expression is either a tautology or a contradiction.] + return true; + } +} + +const someFunc = () => { + const someVar = true; + if (someVar && someVar) { + ~~~~~~~~~~~~~~~~~~ [Both sides of this equality comparison are the same, so the expression is either a tautology or a contradiction.] + return true; + } +} + +const someFunc = () => { + const someObj = { "name" : "moses the great" }; + if (someObj.name === someObj.name) { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Both sides of this equality comparison are the same, so the expression is either a tautology or a contradiction.] + return true; + } +} + +const someFunc = () => { + const someObj = { "name" : "moses the great", "address" : "king's road 10" }; + if (someObj.name === someObj.address) { + return true; + } +} + +const someFunc = () => { + const objOne = { "name" : "moses the great" }; + const objTwo = { "name" : "moses the great" }; + if (objOne.name === objTwo.name) { + return true; + } +} + +const someFunc = () => { + if (true === true) { + ~~~~~~~~~~~~~ [Both sides of this equality comparison are the same, so the expression is either a tautology or a contradiction.] + return true; + } +} + +const someFunc = () => { + if (false === false) { + ~~~~~~~~~~~~~~~ [Both sides of this equality comparison are the same, so the expression is either a tautology or a contradiction.] + return true; + } +} + +const someFunc = () => { + if (true === false) { + ~~~~~~~~~~~~~~ [Both sides of this equality comparison are the same, so the expression is either a tautology or a contradiction.] + return true; + } +} + +const someFunc = () => { + if (null === null) { + ~~~~~~~~~~~~~ [Both sides of this equality comparison are the same, so the expression is either a tautology or a contradiction.] + return true; + } +} + +const someFunc = () => { + if (null === false) { + return true; + } +} + +const someVar1 = 3 + 3; +const someVar2 = 3 - 3; +const someVar3 = 3 * 3; +const someVar4 = 3 % 3; +const someVar5 = 3 / 3; diff --git a/test/rules/no-tautology-expression/tslint.json b/test/rules/no-tautology-expression/tslint.json new file mode 100644 index 00000000000..82111395df2 --- /dev/null +++ b/test/rules/no-tautology-expression/tslint.json @@ -0,0 +1,5 @@ +{ + "rules": { + "no-tautology-expression": true + } +}