UNPKG

5.65 kBPlain TextView Raw
1import * as ts from 'typescript';
2import * as Lint from 'tslint';
3
4import { ErrorTolerantWalker } from './utils/ErrorTolerantWalker';
5import { ExtendedMetadata } from './utils/ExtendedMetadata';
6
7/**
8 * Implementation of the prefer-dry-conditionals rule.
9 */
10export class Rule extends Lint.Rules.AbstractRule {
11 public static metadata: ExtendedMetadata = {
12 ruleName: 'prefer-dry-conditionals',
13 type: 'maintainability', // one of: 'functionality' | 'maintainability' | 'style' | 'typescript'
14 description: "Don't-Repeat-Yourself in if statement conditionals, instead use Switch statements.",
15 options: null,
16 optionsDescription: '',
17 optionExamples: [], //Remove this property if the rule has no options
18 typescriptOnly: false,
19 issueClass: 'Non-SDL', // one of: 'SDL' | 'Non-SDL' | 'Ignored'
20 issueType: 'Warning', // one of: 'Error' | 'Warning'
21 severity: 'Low', // one of: 'Critical' | 'Important' | 'Moderate' | 'Low'
22 level: 'Opportunity for Excellence', // one of 'Mandatory' | 'Opportunity for Excellence'
23 group: 'Clarity', // one of 'Ignored' | 'Security' | 'Correctness' | 'Clarity' | 'Whitespace' | 'Configurable' | 'Deprecated'
24 commonWeaknessEnumeration: '', // if possible, please map your rule to a CWE (see cwe_descriptions.json and https://cwe.mitre.org)
25 };
26
27 public static FAILURE_STRING(switchExpression: string, caseExpressions: string[]): string {
28 const cases: string[] = caseExpressions.map(text => ` case ${text}:\n // ...\n break;`);
29 return (
30 "Don't Repeat Yourself in If statements. Try using a Switch statement instead:\n" +
31 ` switch (${switchExpression}) {\n${cases.join('\n')}\n default:\n // ...\n}`
32 );
33 }
34
35 public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
36 return this.applyWithWalker(new PreferDryConditionalsRuleWalker(sourceFile, this.getOptions()));
37 }
38}
39
40class PreferDryConditionalsRuleWalker extends ErrorTolerantWalker {
41 private threshold: number = 1;
42
43 constructor(sourceFile: ts.SourceFile, options: Lint.IOptions) {
44 super(sourceFile, options);
45 this.parseOptions();
46 }
47
48 protected visitIfStatement(node: ts.IfStatement): void {
49 this.checkAndReport(node);
50 super.visitIfStatement(node);
51 }
52
53 private checkAndReport(node: ts.IfStatement): void {
54 const { expression, parent } = node;
55 if (!ts.isIfStatement(parent) && ts.isBinaryExpression(expression)) {
56 const ifStatements: ts.IfStatement[] = this.allNestedIfStatements(node);
57 const exceedsThreshold: boolean = ifStatements.length > this.threshold;
58 if (!exceedsThreshold) {
59 return;
60 }
61 const areAllBinaryExpressions: boolean = ifStatements.every(statement => ts.isBinaryExpression(statement.expression));
62 if (areAllBinaryExpressions) {
63 const binaryExpressions: ts.BinaryExpression[] = ifStatements.map(statement => <ts.BinaryExpression>statement.expression);
64 this.checkBinaryExpressions(binaryExpressions);
65 }
66 }
67 }
68
69 private allNestedIfStatements(node: ts.IfStatement): ts.IfStatement[] {
70 const ifStatements: ts.IfStatement[] = [node];
71 let curr: ts.IfStatement = node;
72 while (ts.isIfStatement(curr.elseStatement)) {
73 ifStatements.push(curr.elseStatement);
74 curr = curr.elseStatement;
75 }
76 return ifStatements;
77 }
78
79 private checkBinaryExpressions(expressions: ts.BinaryExpression[]): void {
80 // console.log('expressions', expressions);
81 if (expressions.length <= 1) {
82 return;
83 }
84 const firstExpression = expressions[0];
85 const expectedOperatorToken = firstExpression.operatorToken;
86 const isEqualityCheck =
87 expectedOperatorToken.kind === ts.SyntaxKind.EqualsEqualsToken ||
88 expectedOperatorToken.kind === ts.SyntaxKind.EqualsEqualsEqualsToken;
89 if (!isEqualityCheck) {
90 return;
91 }
92
93 const expectedLeft = firstExpression.left;
94 const expectedRight = firstExpression.right;
95
96 const hasSameOperator = expressions.every(expression => expression.operatorToken.kind === expectedOperatorToken.kind);
97 if (!hasSameOperator) {
98 return;
99 }
100
101 const leftExpressions = expressions.map(expression => expression.left);
102 const rightExpressions = expressions.map(expression => expression.right);
103
104 const hasSameLeft = leftExpressions.every(expression => expression.getText() === expectedLeft.getText());
105 const hasSameRight = rightExpressions.every(expression => expression.getText() === expectedRight.getText());
106 if (hasSameLeft) {
107 this.addFailureAtNode(
108 firstExpression.parent,
109 Rule.FAILURE_STRING(expectedLeft.getText(), rightExpressions.map(expression => expression.getText()))
110 );
111 } else if (hasSameRight) {
112 this.addFailureAtNode(
113 firstExpression.parent,
114 Rule.FAILURE_STRING(expectedRight.getText(), leftExpressions.map(expression => expression.getText()))
115 );
116 }
117 }
118
119 private parseOptions(): void {
120 this.getOptions().forEach((opt: any) => {
121 if (typeof opt === 'boolean') {
122 return;
123 }
124 if (typeof opt === 'number') {
125 this.threshold = Math.max(1, opt);
126 return;
127 }
128 });
129 }
130}