Skip to content

Commit 7fc3b09

Browse files
rafaelss95wKoza
authored andcommitted
fix(rule): template-conditional-complexity not reporting failures for '[ngIf]' (#611)
1 parent fedd331 commit 7fc3b09

File tree

2 files changed

+142
-114
lines changed

2 files changed

+142
-114
lines changed
Lines changed: 77 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,33 @@
1-
import * as Lint from 'tslint';
2-
import * as ts from 'typescript';
3-
import * as ast from '@angular/compiler';
1+
import { AST, ASTWithSource, Binary, BoundDirectivePropertyAst, Lexer, Parser } from '@angular/compiler';
42
import { sprintf } from 'sprintf-js';
5-
import { BasicTemplateAstVisitor } from './angular/templates/basicTemplateAstVisitor';
3+
import { IRuleMetadata, RuleFailure, Rules } from 'tslint/lib';
4+
import { SourceFile } from 'typescript/lib/typescript';
65
import { NgWalker } from './angular/ngWalker';
7-
import * as compiler from '@angular/compiler';
8-
import { Binary } from '@angular/compiler';
6+
import { BasicTemplateAstVisitor } from './angular/templates/basicTemplateAstVisitor';
97

10-
export class Rule extends Lint.Rules.AbstractRule {
11-
public static metadata: Lint.IRuleMetadata = {
12-
ruleName: 'template-conditional-complexity',
13-
type: 'functionality',
8+
export class Rule extends Rules.AbstractRule {
9+
static readonly metadata: IRuleMetadata = {
1410
description: "The condition complexity shouldn't exceed a rational limit in a template.",
15-
rationale: 'An important complexity complicates the tests and the maintenance.',
11+
optionExamples: ['true', '[true, 4]'],
1612
options: {
17-
type: 'array',
1813
items: {
1914
type: 'string'
2015
},
16+
maxLength: 2,
2117
minLength: 0,
22-
maxLength: 2
18+
type: 'array'
2319
},
24-
optionExamples: ['true', '[true, 4]'],
2520
optionsDescription: 'Determine the maximum number of Boolean operators allowed.',
21+
rationale: 'An important complexity complicates the tests and the maintenance.',
22+
ruleName: 'template-conditional-complexity',
23+
type: 'maintainability',
2624
typescriptOnly: true
2725
};
2826

29-
static COMPLEXITY_FAILURE_STRING = "The condition complexity (cost '%s') exceeded the defined limit (cost '%s'). The conditional expression should be moved into the component.";
30-
31-
static COMPLEXITY_MAX = 3;
27+
static readonly FAILURE_STRING = "The condition complexity (cost '%s') exceeded the defined limit (cost '%s'). The conditional expression should be moved into the component.";
28+
static readonly DEFAULT_MAX_COMPLEXITY = 3;
3229

33-
public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
30+
apply(sourceFile: SourceFile): RuleFailure[] {
3431
return this.applyWithWalker(
3532
new NgWalker(sourceFile, this.getOptions(), {
3633
templateVisitorCtrl: TemplateConditionalComplexityVisitor
@@ -39,53 +36,69 @@ export class Rule extends Lint.Rules.AbstractRule {
3936
}
4037
}
4138

42-
class TemplateConditionalComplexityVisitor extends BasicTemplateAstVisitor {
43-
visitDirectiveProperty(prop: ast.BoundDirectivePropertyAst, context: BasicTemplateAstVisitor): any {
44-
if (prop.sourceSpan) {
45-
const directive = (<any>prop.sourceSpan).toString();
46-
47-
if (directive.startsWith('*ngIf')) {
48-
// extract expression and drop characters new line and quotes
49-
const expr = directive
50-
.split(/\*ngIf\s*=\s*/)[1]
51-
.slice(1, -1)
52-
.replace(/[\n\r]/g, '');
53-
54-
const expressionParser = new compiler.Parser(new compiler.Lexer());
55-
const ast = expressionParser.parseAction(expr, null);
56-
57-
let complexity = 0;
58-
let conditions: Array<Binary> = [];
59-
let condition = ast.ast as Binary;
60-
if (condition.operation) {
61-
complexity++;
62-
conditions.push(condition);
63-
}
64-
65-
while (conditions.length > 0) {
66-
condition = conditions.pop();
67-
if (condition.operation) {
68-
if (condition.left instanceof Binary) {
69-
complexity++;
70-
conditions.push(condition.left as Binary);
71-
}
72-
73-
if (condition.right instanceof Binary) {
74-
conditions.push(condition.right as Binary);
75-
}
76-
}
77-
}
78-
const options = this.getOptions();
79-
const complexityMax: number = options.length ? options[0] : Rule.COMPLEXITY_MAX;
80-
81-
if (complexity > complexityMax) {
82-
const span = prop.sourceSpan;
83-
let failureConfig: string[] = [String(complexity), String(complexityMax)];
84-
failureConfig.unshift(Rule.COMPLEXITY_FAILURE_STRING);
85-
this.addFailure(this.createFailure(span.start.offset, span.end.offset - span.start.offset, sprintf.apply(this, failureConfig)));
86-
}
87-
}
39+
export const getFailureMessage = (totalComplexity: number, maxComplexity = Rule.DEFAULT_MAX_COMPLEXITY): string => {
40+
return sprintf(Rule.FAILURE_STRING, totalComplexity, maxComplexity);
41+
};
42+
43+
const getTotalComplexity = (ast: AST): number => {
44+
const expr = (ast as ASTWithSource).source.replace(/\s/g, '');
45+
const expressionParser = new Parser(new Lexer());
46+
const astWithSource = expressionParser.parseAction(expr, null);
47+
const conditions: Binary[] = [];
48+
let totalComplexity = 0;
49+
let condition = astWithSource.ast as Binary;
50+
51+
if (condition.operation) {
52+
totalComplexity++;
53+
conditions.push(condition);
54+
}
55+
56+
while (conditions.length > 0) {
57+
condition = conditions.pop();
58+
59+
if (!condition.operation) {
60+
continue;
8861
}
62+
63+
if (condition.left instanceof Binary) {
64+
totalComplexity++;
65+
conditions.push(condition.left);
66+
}
67+
68+
if (condition.right instanceof Binary) {
69+
conditions.push(condition.right);
70+
}
71+
}
72+
73+
return totalComplexity;
74+
};
75+
76+
class TemplateConditionalComplexityVisitor extends BasicTemplateAstVisitor {
77+
visitDirectiveProperty(prop: BoundDirectivePropertyAst, context: BasicTemplateAstVisitor): any {
78+
this.validateDirective(prop);
8979
super.visitDirectiveProperty(prop, context);
9080
}
81+
82+
private validateDirective(prop: BoundDirectivePropertyAst): void {
83+
const { templateName, value } = prop;
84+
85+
if (templateName !== 'ngIf') {
86+
return;
87+
}
88+
89+
const maxComplexity: number = this.getOptions()[0] || Rule.DEFAULT_MAX_COMPLEXITY;
90+
const totalComplexity = getTotalComplexity(value);
91+
92+
if (totalComplexity <= maxComplexity) {
93+
return;
94+
}
95+
96+
const {
97+
sourceSpan: {
98+
end: { offset: endOffset },
99+
start: { offset: startOffset }
100+
}
101+
} = prop;
102+
this.addFailureFromStartToEnd(startOffset, endOffset, getFailureMessage(totalComplexity, maxComplexity));
103+
}
91104
}
Lines changed: 65 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,122 +1,137 @@
1-
import { assertSuccess, assertAnnotated } from './testHelper';
1+
import { getFailureMessage, Rule } from '../src/templateConditionalComplexityRule';
2+
import { assertAnnotated, assertSuccess } from './testHelper';
23

3-
describe('complexity', () => {
4-
describe('success', () => {
5-
it('should work with a lower level of complexity', () => {
6-
let source = `
4+
const {
5+
metadata: { ruleName }
6+
} = Rule;
7+
8+
describe(ruleName, () => {
9+
describe('failure', () => {
10+
it('should fail with a higher level of complexity', () => {
11+
const source = `
712
@Component({
813
template: \`
9-
<div *ngIf="a === '1' || (b === '2' && c.d !== e)">
14+
<div *ngIf="a === '3' || (b === '3' && c.d !== '1' && e.f !== '6' && q !== g)">
15+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1016
Enter your card details
1117
</div>
1218
\`
1319
})
1420
class Bar {}
1521
`;
16-
assertSuccess('template-conditional-complexity', source);
22+
assertAnnotated({ message: getFailureMessage(5, 4), options: [4], ruleName, source });
1723
});
1824

19-
it('should work with a lower level of complexity', () => {
20-
let source = `
25+
it('should fail with a higher level of complexity and a carrier return', () => {
26+
const source = `
2127
@Component({
2228
template: \`
23-
<div *ngIf="a === '1' || b === '2' && c.d !== e">
29+
<div *ngIf="a === '3' || (b === '3' && c.d !== '1'
30+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
31+
&& e.f !== '6' && q !== g)">
32+
~~~~~~~~~~~~~~~~~~~~~~~~~~~
2433
Enter your card details
2534
</div>
2635
\`
2736
})
2837
class Bar {}
2938
`;
30-
assertSuccess('template-conditional-complexity', source);
39+
assertAnnotated({ message: getFailureMessage(5, 3), ruleName, source });
3140
});
3241

33-
it('should work with a level of complexity customisable', () => {
34-
let source = `
42+
it('should fail with a higher level of complexity with ng-template', () => {
43+
const source = `
3544
@Component({
3645
template: \`
37-
<div *ngIf="a === '3' || (b === '3' && c.d !== '1' && e.f !== '6' && q !== g)">
46+
<ng-template [ngIf]="a === '3' || (b === '3' && c.d !== '1'
47+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
48+
&& e.f !== '6' && q !== g && x === '1')">
49+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
50+
Enter details
51+
</ng-template>
52+
\`
53+
})
54+
class Bar {}
55+
`;
56+
assertAnnotated({ message: getFailureMessage(6, 3), ruleName, source });
57+
});
58+
});
59+
60+
describe('success', () => {
61+
it('should succeed with a lower level of complexity', () => {
62+
const source = `
63+
@Component({
64+
template: \`
65+
<div *ngIf="a === '1' || b === '2' && c.d !== e">
3866
Enter your card details
3967
</div>
4068
\`
4169
})
4270
class Bar {}
4371
`;
44-
assertSuccess('template-conditional-complexity', source, [5]);
72+
assertSuccess(ruleName, source);
4573
});
4674

47-
it('should work with a level of complexity customisable', () => {
48-
let source = `
75+
it('should succeed with a lower level of complexity with separated statements', () => {
76+
const source = `
4977
@Component({
5078
template: \`
51-
<div *ngIf="(b === '3' && c.d !== '1' && e.f !== '6' && q !== g) || a === '3'">
79+
<div *ngIf="a === '1' || (b === '2' && c.d !== e)">
5280
Enter your card details
5381
</div>
5482
\`
5583
})
5684
class Bar {}
5785
`;
58-
assertSuccess('template-conditional-complexity', source, [5]);
86+
assertSuccess(ruleName, source);
5987
});
6088

61-
it('should work with something else', () => {
62-
let source = `
89+
it('should succeed with a level of complexity customizable', () => {
90+
const source = `
6391
@Component({
6492
template: \`
65-
<div *ngIf="isValid;then content else other_content">
93+
<div *ngIf="a === '3' || (b === '3' && c.d !== '1' && e.f !== '6' && q !== g)">
6694
Enter your card details
6795
</div>
6896
\`
6997
})
7098
class Bar {}
7199
`;
72-
assertSuccess('template-conditional-complexity', source, [5]);
100+
assertSuccess(ruleName, source, [5]);
73101
});
74-
});
75102

76-
describe('failure', () => {
77-
it('should fail with a higher level of complexity', () => {
78-
let source = `
103+
it('should succeed with a level of complexity customizable', () => {
104+
const source = `
79105
@Component({
80106
template: \`
81-
<div *ngIf="a === '3' || (b === '3' && c.d !== '1' && e.f !== '6' && q !== g)">
82-
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
107+
<div *ngIf="(b === '3' && c.d !== '1' && e.f !== '6' && q !== g) || a === '3'">
83108
Enter your card details
84109
</div>
85110
\`
86111
})
87112
class Bar {}
88113
`;
89-
assertAnnotated({
90-
ruleName: 'template-conditional-complexity',
91-
message:
92-
"The condition complexity (cost '5') exceeded the defined limit (cost '4'). The conditional expression should be moved into the component.",
93-
source,
94-
options: [4]
95-
});
114+
assertSuccess(ruleName, source, [5]);
96115
});
97-
});
98116

99-
describe('failure', () => {
100-
it('should fail with a higher level of complexity and a carrier return', () => {
101-
let source = `
117+
it('should succeed with non-inlined then template', () => {
118+
const source = `
102119
@Component({
103120
template: \`
104-
<div *ngIf="a === '3' || (b === '3' && c.d !== '1'
105-
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
106-
&& e.f !== '6' && q !== g)">
107-
~~~~~~~~~~~~~~~~~~~~~~~~~~~
121+
<div *ngIf="isValid; then thenBlock; else elseBlock">
108122
Enter your card details
109123
</div>
124+
<ng-template #thenBlock>
125+
thenBlock
126+
</ng-template>
127+
<ng-template #elseBlock>
128+
elseBlock
129+
</ng-template>
110130
\`
111131
})
112132
class Bar {}
113133
`;
114-
assertAnnotated({
115-
ruleName: 'template-conditional-complexity',
116-
message:
117-
"The condition complexity (cost '5') exceeded the defined limit (cost '3'). The conditional expression should be moved into the component.",
118-
source
119-
});
134+
assertSuccess(ruleName, source, [5]);
120135
});
121136
});
122137
});

0 commit comments

Comments
 (0)