|
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'; |
3 | 4 | import { NgWalker } from './angular/ngWalker';
|
4 |
| -import * as ast from '@angular/compiler'; |
5 | 5 | import { BasicTemplateAstVisitor } from './angular/templates/basicTemplateAstVisitor';
|
6 | 6 |
|
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 = { |
11 | 9 | description: 'Ensures a trackBy function is used.',
|
12 |
| - rationale: "The use of 'trackBy' is considered a good practice.", |
13 | 10 | options: null,
|
14 | 11 | optionsDescription: 'Not configurable.',
|
| 12 | + rationale: "The use of 'trackBy' is considered a good practice.", |
| 13 | + ruleName: 'trackBy-function', |
| 14 | + type: 'functionality', |
15 | 15 | typescriptOnly: true
|
16 | 16 | };
|
17 | 17 |
|
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 })); |
24 | 22 | }
|
25 | 23 | }
|
26 | 24 |
|
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 | + } |
29 | 34 |
|
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; |
32 | 37 |
|
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 | + } |
36 | 41 |
|
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*['"].*['"]/; |
40 | 43 |
|
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; |
48 | 46 | }
|
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()); |
50 | 55 | }
|
51 | 56 | }
|
52 | 57 |
|
53 | 58 | 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)); |
57 | 65 |
|
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)); |
63 | 66 | super.visitDirectiveProperty(prop, context);
|
64 | 67 | }
|
65 | 68 | }
|
0 commit comments