/
templateConditionalComplexityRule.ts
104 lines (87 loc) · 3.28 KB
/
templateConditionalComplexityRule.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
import { AST, ASTWithSource, Binary, BoundDirectivePropertyAst, Lexer, Parser } from '@angular/compiler';
import { sprintf } from 'sprintf-js';
import { IRuleMetadata, RuleFailure, Rules } from 'tslint/lib';
import { SourceFile } from 'typescript/lib/typescript';
import { NgWalker } from './angular/ngWalker';
import { BasicTemplateAstVisitor } from './angular/templates/basicTemplateAstVisitor';
export class Rule extends Rules.AbstractRule {
static readonly metadata: IRuleMetadata = {
description: "The condition complexity shouldn't exceed a rational limit in a template.",
optionExamples: ['true', '[true, 4]'],
options: {
items: {
type: 'string'
},
maxLength: 2,
minLength: 0,
type: 'array'
},
optionsDescription: 'Determine the maximum number of Boolean operators allowed.',
rationale: 'An important complexity complicates the tests and the maintenance.',
ruleName: 'template-conditional-complexity',
type: 'maintainability',
typescriptOnly: true
};
static readonly FAILURE_STRING = "The condition complexity (cost '%s') exceeded the defined limit (cost '%s'). The conditional expression should be moved into the component.";
static readonly DEFAULT_MAX_COMPLEXITY = 3;
apply(sourceFile: SourceFile): RuleFailure[] {
return this.applyWithWalker(
new NgWalker(sourceFile, this.getOptions(), {
templateVisitorCtrl: TemplateConditionalComplexityVisitor
})
);
}
}
export const getFailureMessage = (totalComplexity: number, maxComplexity = Rule.DEFAULT_MAX_COMPLEXITY): string => {
return sprintf(Rule.FAILURE_STRING, totalComplexity, maxComplexity);
};
const getTotalComplexity = (ast: AST): number => {
const expr = (ast as ASTWithSource).source.replace(/\s/g, '');
const expressionParser = new Parser(new Lexer());
const astWithSource = expressionParser.parseAction(expr, null);
const conditions: Binary[] = [];
let totalComplexity = 0;
let condition = astWithSource.ast as Binary;
if (condition.operation) {
totalComplexity++;
conditions.push(condition);
}
while (conditions.length > 0) {
condition = conditions.pop();
if (!condition.operation) {
continue;
}
if (condition.left instanceof Binary) {
totalComplexity++;
conditions.push(condition.left);
}
if (condition.right instanceof Binary) {
conditions.push(condition.right);
}
}
return totalComplexity;
};
class TemplateConditionalComplexityVisitor extends BasicTemplateAstVisitor {
visitDirectiveProperty(prop: BoundDirectivePropertyAst, context: BasicTemplateAstVisitor): any {
this.validateDirective(prop);
super.visitDirectiveProperty(prop, context);
}
private validateDirective(prop: BoundDirectivePropertyAst): void {
const { templateName, value } = prop;
if (templateName !== 'ngIf') {
return;
}
const maxComplexity: number = this.getOptions()[0] || Rule.DEFAULT_MAX_COMPLEXITY;
const totalComplexity = getTotalComplexity(value);
if (totalComplexity <= maxComplexity) {
return;
}
const {
sourceSpan: {
end: { offset: endOffset },
start: { offset: startOffset }
}
} = prop;
this.addFailureFromStartToEnd(startOffset, endOffset, getFailureMessage(totalComplexity, maxComplexity));
}
}