From 887c2c41f170bcfc7218c4a752326754aa625ed9 Mon Sep 17 00:00:00 2001 From: Konstantin Azizov Date: Thu, 6 Dec 2018 15:27:51 +0100 Subject: [PATCH] [feat] add no-negated-condition-rule --- src/rules/terNoNegatedConditionRule.ts | 70 +++++++++++++++++++ .../rules/terNoNegatedConditionRuleTests.ts | 56 +++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 src/rules/terNoNegatedConditionRule.ts create mode 100644 src/test/rules/terNoNegatedConditionRuleTests.ts diff --git a/src/rules/terNoNegatedConditionRule.ts b/src/rules/terNoNegatedConditionRule.ts new file mode 100644 index 0000000..c014b1e --- /dev/null +++ b/src/rules/terNoNegatedConditionRule.ts @@ -0,0 +1,70 @@ +import * as ts from 'typescript'; +import * as Lint from 'tslint'; + +const RULE_NAME = 'ter-no-negated-condition'; +export class Rule extends Lint.Rules.AbstractRule { + public static FAILURE_STRING = 'Unexpected negated condition.'; + public static metadata: Lint.IRuleMetadata = { + ruleName: RULE_NAME, + hasFix: false, + description: 'Disallows negated conditions for if statements with else branch and ternary expressions', + rationale: Lint.Utils.dedent` + Negated conditions are more difficult to understand. + Code can be made more readable by inverting the condition instead.`, + optionsDescription: '', + options: {}, + optionExamples: [ + Lint.Utils.dedent` + "${RULE_NAME}": true + ` + ], + typescriptOnly: false, + type: 'style' + }; + + public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { + return this.applyWithWalker(new Walk(sourceFile, this.getOptions())); + } +} + +class Walk extends Lint.RuleWalker { + protected visitIfStatement(node: ts.IfStatement) { + if (this.hasElseWithoutCondition(node) && ( + this.isSingleOperatorNegatedCondition(node.expression) ? this.isConditionNegated(node.expression) : this.isConditionNotEqual(node.expression as ts.BinaryExpression) + )) { + this.addFailureAtNode( + node, + Rule.FAILURE_STRING + ); + } + super.visitIfStatement(node); + } + + protected visitConditionalExpression(node: ts.ConditionalExpression) { + if ((this.isSingleOperatorNegatedCondition(node.condition) && this.isConditionNegated(node.condition)) || (this.isConditionNotEqual(node.condition as ts.BinaryExpression))) { + this.addFailureAtNode( + node, + Rule.FAILURE_STRING + ); + } + super.visitConditionalExpression(node); + } + + private hasElseWithoutCondition(node: ts.IfStatement) { + return node.elseStatement && node.elseStatement.getFirstToken() && node.elseStatement.getFirstToken()!.kind !== ts.SyntaxKind.IfKeyword; + } + + private isSingleOperatorNegatedCondition(condition: ts.ConditionalExpression['condition']) { + // Child count should be 2 so we can ensure that whole condition is negated(e.g. !(true && true) instead of !true && false) + return condition.getChildCount() === 2; + } + + private isConditionNegated(condition: ts.ConditionalExpression['condition']) { + return condition.getFirstToken() && condition.getFirstToken()!.kind === ts.SyntaxKind.ExclamationToken; + } + + private isConditionNotEqual(condition: ts.BinaryExpression) { + return condition.operatorToken && (condition.operatorToken.kind === ts.SyntaxKind.ExclamationEqualsToken || + condition.operatorToken.kind === ts.SyntaxKind.ExclamationEqualsEqualsToken); + } +} diff --git a/src/test/rules/terNoNegatedConditionRuleTests.ts b/src/test/rules/terNoNegatedConditionRuleTests.ts new file mode 100644 index 0000000..be83014 --- /dev/null +++ b/src/test/rules/terNoNegatedConditionRuleTests.ts @@ -0,0 +1,56 @@ +import { RuleTester, Position } from './ruleTester'; +// ESLint Tests: https://github.com/eslint/eslint/blob/master/lib/rules/no-negated-condition.js + +const ruleTester = new RuleTester('ter-no-negated-condition'); + +const noNegatedConditionError = { + failure: 'Unexpected negated condition.', + startPosition: new Position(0), + endPosition: new Position(0) +}; + +ruleTester.addTestGroup('valid', 'should pass when condition is not negated or cannot be converted', [ + 'if (a) {}', + 'if (a) {} else {}', + 'if (!a) {}', + 'if (!a) {} else if (b) {}', + 'if (!a) {} else if (b) {} else {}', + 'if (a == b) {}', + 'if (a == b) {} else {}', + 'if (a != b) {}', + 'if (a != b) {} else if (b) {}', + 'if (a != b) {} else if (b) {} else {}', + 'if (a !== b) {}', + 'if (a === b) {} else {}', + 'a ? b : c', + '!a && b ? c : d' +]); + +ruleTester.addTestGroup('invalid', 'should fail condition is negated and second branch is available', [ + { + code: 'if (!a) {;} else {;}', + errors: [noNegatedConditionError] + }, + { + code: 'if (a != b) {;} else {;}', + errors: [noNegatedConditionError] + }, + { + code: 'if (a !== b) {;} else {;}', + errors: [noNegatedConditionError] + }, + { + code: '!a ? b : c', + errors: [noNegatedConditionError] + }, + { + code: 'a != b ? c : d', + errors: [noNegatedConditionError] + }, + { + code: 'a !== b ? c : d', + errors: [noNegatedConditionError] + } +]); + +ruleTester.runTests();