1 | import * as ts from 'typescript';
|
2 | import * as Lint from 'tslint';
|
3 |
|
4 | import { ErrorTolerantWalker } from './utils/ErrorTolerantWalker';
|
5 | import { ExtendedMetadata } from './utils/ExtendedMetadata';
|
6 |
|
7 |
|
8 |
|
9 |
|
10 | export class Rule extends Lint.Rules.AbstractRule {
|
11 | public static metadata: ExtendedMetadata = {
|
12 | ruleName: 'prefer-dry-conditionals',
|
13 | type: 'maintainability',
|
14 | description: "Don't-Repeat-Yourself in if statement conditionals, instead use Switch statements.",
|
15 | options: null,
|
16 | optionsDescription: '',
|
17 | optionExamples: [],
|
18 | typescriptOnly: false,
|
19 | issueClass: 'Non-SDL',
|
20 | issueType: 'Warning',
|
21 | severity: 'Low',
|
22 | level: 'Opportunity for Excellence',
|
23 | group: 'Clarity',
|
24 | commonWeaknessEnumeration: '',
|
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 |
|
40 | class 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 |
|
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 | }
|