Skip to content

Commit af52912

Browse files
rafaelss95wKoza
authored andcommitted
fix(rule): trackBy-function not reporting failures for '[ngForOf]' (#610)
1 parent 91d1042 commit af52912

File tree

3 files changed

+239
-171
lines changed

3 files changed

+239
-171
lines changed

src/trackByFunctionRule.ts

+43-40
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,68 @@
1-
import * as Lint from 'tslint';
2-
import * as ts from 'typescript';
1+
import { BoundDirectivePropertyAst } from '@angular/compiler';
2+
import { IRuleMetadata, RuleFailure, Rules } from 'tslint/lib';
3+
import { SourceFile } from 'typescript/lib/typescript';
34
import { NgWalker } from './angular/ngWalker';
4-
import * as ast from '@angular/compiler';
55
import { BasicTemplateAstVisitor } from './angular/templates/basicTemplateAstVisitor';
66

7-
export class Rule extends Lint.Rules.AbstractRule {
8-
public static metadata: Lint.IRuleMetadata = {
9-
ruleName: 'trackBy-function',
10-
type: 'functionality',
7+
export class Rule extends Rules.AbstractRule {
8+
static readonly metadata: IRuleMetadata = {
119
description: 'Ensures a trackBy function is used.',
12-
rationale: "The use of 'trackBy' is considered a good practice.",
1310
options: null,
1411
optionsDescription: 'Not configurable.',
12+
rationale: "The use of 'trackBy' is considered a good practice.",
13+
ruleName: 'trackBy-function',
14+
type: 'functionality',
1515
typescriptOnly: true
1616
};
1717

18-
public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
19-
return this.applyWithWalker(
20-
new NgWalker(sourceFile, this.getOptions(), {
21-
templateVisitorCtrl: TrackByTemplateVisitor
22-
})
23-
);
18+
static readonly FAILURE_STRING = 'Missing trackBy function in ngFor directive';
19+
20+
apply(sourceFile: SourceFile): RuleFailure[] {
21+
return this.applyWithWalker(new NgWalker(sourceFile, this.getOptions(), { templateVisitorCtrl: TrackByTemplateVisitor }));
2422
}
2523
}
2624

27-
const ngForExpressionRe = new RegExp(/\*ngFor\s*=\s*(?:'|")(.+)(?:'|")/);
28-
const trackByRe = new RegExp(/trackBy\s*:/);
25+
export const getFailureMessage = (): string => {
26+
return Rule.FAILURE_STRING;
27+
};
28+
29+
class TrackByFunctionTemplateVisitor extends BasicTemplateAstVisitor {
30+
visitDirectiveProperty(prop: BoundDirectivePropertyAst, context: any): any {
31+
this.validateDirective(prop, context);
32+
super.visitDirectiveProperty(prop, context);
33+
}
2934

30-
class TrackByNgForTemplateVisitor extends BasicTemplateAstVisitor {
31-
static Error = 'Missing trackBy function in ngFor directive';
35+
private validateDirective(prop: BoundDirectivePropertyAst, context: any): any {
36+
const { templateName } = prop;
3237

33-
visitDirectiveProperty(prop: ast.BoundDirectivePropertyAst, context: BasicTemplateAstVisitor): any {
34-
if (prop.sourceSpan) {
35-
const directive = (<any>prop.sourceSpan).toString();
38+
if (templateName !== 'ngForOf') {
39+
return;
40+
}
3641

37-
if (directive.startsWith('*ngFor')) {
38-
const directiveMatch = directive.match(ngForExpressionRe);
39-
const expr = directiveMatch && directiveMatch[1];
42+
const pattern = /trackBy\s*:|\[ngForTrackBy\]\s*=\s*['"].*['"]/;
4043

41-
if (expr && !trackByRe.test(expr)) {
42-
const span = prop.sourceSpan;
43-
context.addFailure(
44-
context.createFailure(span.start.offset, span.end.offset - span.start.offset, TrackByNgForTemplateVisitor.Error)
45-
);
46-
}
47-
}
44+
if (pattern.test(context.codeWithMap.source)) {
45+
return;
4846
}
49-
super.visitDirectiveProperty(prop, context);
47+
48+
const {
49+
sourceSpan: {
50+
end: { offset: endOffset },
51+
start: { offset: startOffset }
52+
}
53+
} = prop;
54+
context.addFailureFromStartToEnd(startOffset, endOffset, getFailureMessage());
5055
}
5156
}
5257

5358
class TrackByTemplateVisitor extends BasicTemplateAstVisitor {
54-
private visitors: (BasicTemplateAstVisitor)[] = [
55-
new TrackByNgForTemplateVisitor(this.getSourceFile(), this.getOptions(), this.context, this.templateStart)
56-
];
59+
private readonly visitors: ReadonlySet<BasicTemplateAstVisitor> = new Set([
60+
new TrackByFunctionTemplateVisitor(this.getSourceFile(), this.getOptions(), this.context, this.templateStart)
61+
]);
62+
63+
visitDirectiveProperty(prop: BoundDirectivePropertyAst, context: any): any {
64+
this.visitors.forEach(visitor => visitor.visitDirectiveProperty(prop, this));
5765

58-
visitDirectiveProperty(prop: ast.BoundDirectivePropertyAst, context: any): any {
59-
this.visitors
60-
.map(v => v.visitDirectiveProperty(prop, this))
61-
.filter(f => !!f)
62-
.forEach(f => this.addFailure(f));
6366
super.visitDirectiveProperty(prop, context);
6467
}
6568
}

test/trackByFunctionRule.spec.ts

+196
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
import { getFailureMessage, Rule } from '../src/trackByFunctionRule';
2+
import { assertAnnotated, assertMultipleAnnotated, assertSuccess } from './testHelper';
3+
4+
const {
5+
metadata: { ruleName }
6+
} = Rule;
7+
8+
describe(ruleName, () => {
9+
describe('failure', () => {
10+
it('should fail when trackBy function is not present', () => {
11+
const source = `
12+
@Component({
13+
template: \`
14+
<ul>
15+
<li *ngFor="let item of [1, 2, 3];">
16+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17+
{{ item }}
18+
</li>
19+
</ul>
20+
\`
21+
})
22+
class Bar {}
23+
`;
24+
assertAnnotated({ message: getFailureMessage(), ruleName, source });
25+
});
26+
27+
it('should fail when trackBy is missing colon', () => {
28+
const source = `
29+
@Component({
30+
template: \`
31+
<div *ngFor="let item of [1, 2, 3]; trackBy trackByFn">
32+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
33+
{{ item }}
34+
</div>
35+
\`
36+
})
37+
class Bar {}
38+
`;
39+
assertAnnotated({ message: getFailureMessage(), ruleName, source });
40+
});
41+
42+
it('should fail when [ngForTrackBy] is missing in ng-template', () => {
43+
const source = `
44+
@Component({
45+
template: \`
46+
<ng-template ngFor let-item [ngForOf]="[1, 2, 3]" let-i="index">
47+
~~~~~~~~~~~~~~~~~~~~~
48+
{{ item }}
49+
</ng-template>
50+
\`
51+
})
52+
class Bar {}
53+
`;
54+
assertAnnotated({ message: getFailureMessage(), ruleName, source });
55+
});
56+
57+
it('should fail when trackBy function is missing in multiple *ngFor', () => {
58+
const source = `
59+
@Component({
60+
template: \`
61+
<div *ngFor="let item of [1, 2, 3];">
62+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
63+
{{ item }}
64+
</div>
65+
66+
<ng-template ngFor let-item [ngForOf]="[1, 2, 3]" let-i="index">
67+
^^^^^^^^^^^^^^^^^^^^^
68+
{{ item }}
69+
</ng-template>
70+
\`
71+
})
72+
class Bar {}
73+
`;
74+
assertMultipleAnnotated({
75+
failures: [
76+
{
77+
char: '~',
78+
msg: getFailureMessage()
79+
},
80+
{
81+
char: '^',
82+
msg: getFailureMessage()
83+
}
84+
],
85+
ruleName,
86+
source
87+
});
88+
});
89+
});
90+
91+
describe('success', () => {
92+
it('should succeed when trackBy function is present', () => {
93+
const source = `
94+
@Component({
95+
template: \`
96+
<div *ngFor="let item of [1, 2, 3]; trackBy: trackByFn">
97+
{{ item }}
98+
</div>
99+
\`
100+
})
101+
class Bar {}
102+
`;
103+
assertSuccess(ruleName, source);
104+
});
105+
106+
it('should succeed when trackBy function and exported index value are present', () => {
107+
const source = `
108+
@Component({
109+
template: \`
110+
<div *ngFor="let item of [1, 2, 3]; let i = index; trackBy: trackByFn">
111+
{{ item }}
112+
</div>
113+
\`
114+
})
115+
class Bar {}
116+
`;
117+
assertSuccess(ruleName, source);
118+
});
119+
120+
it('should succeed when trackBy function is present and has trailing spaces', () => {
121+
const source = `
122+
@Component({
123+
template: \`
124+
<div *ngFor="let item of [1, 2, 3]; trackBy : trackByFn">
125+
{{ item }}
126+
</div>
127+
\`
128+
})
129+
class Bar {}
130+
`;
131+
assertSuccess(ruleName, source);
132+
});
133+
134+
it('should succeed when trackBy function is present and *ngFor uses single quotes', () => {
135+
const source = `
136+
@Component({
137+
template: \`
138+
<div *ngFor='let item of [1, 2, 3]; let i = index; trackBy: trackByFn'>
139+
{{ item }}
140+
</div>
141+
\`
142+
})
143+
class Bar {}
144+
`;
145+
assertSuccess(ruleName, source);
146+
});
147+
148+
it('should succeed when *ngFor is surrounded by a lot of whitespaces', () => {
149+
const source = `
150+
@Component({
151+
template: \`
152+
<div *ngFor = "let item of [1, 2, 3]; let i = index; trackBy : trackByFn">
153+
{{ item }}
154+
</div>
155+
\`
156+
})
157+
class Bar {}
158+
`;
159+
assertSuccess(ruleName, source);
160+
});
161+
162+
it('should succeed when [ngForTrackBy] is present in ng-template', () => {
163+
const source = `
164+
@Component({
165+
template: \`
166+
<ng-template ngFor let-item [ngForOf]="[1, 2, 3]" let-i="index"
167+
[ngForTrackBy]="trackByFn">
168+
{{ item }}
169+
</ng-template>
170+
\`
171+
})
172+
class Bar {}
173+
`;
174+
assertSuccess(ruleName, source);
175+
});
176+
177+
it('should succeed when trackBy function is present in multiple *ngFor', () => {
178+
const source = `
179+
@Component({
180+
template: \`
181+
<div *ngFor="let item of ['a', 'b', 'c']; index as i; trackBy: trackByFn">
182+
{{ item }}
183+
</div>
184+
185+
<ng-template ngFor let-item [ngForOf]="[1, 2, 3]" let-i="index"
186+
[ngForTrackBy]="trackByFn">
187+
{{ item }}
188+
</ng-template>
189+
\`
190+
})
191+
class Bar {}
192+
`;
193+
assertSuccess(ruleName, source);
194+
});
195+
});
196+
});

0 commit comments

Comments
 (0)