|
1 |
| -import * as Lint from 'tslint'; |
2 |
| -import * as ts from 'typescript'; |
| 1 | +import { IRuleMetadata, RuleFailure, Rules } from 'tslint'; |
| 2 | +import { SourceFile } from 'typescript'; |
3 | 3 | import { NgWalker } from './angular/ngWalker';
|
4 | 4 | import { RecursiveAngularExpressionVisitor } from './angular/templates/recursiveAngularExpressionVisitor';
|
5 |
| -import * as e from '@angular/compiler/src/expression_parser/ast'; |
6 |
| -import * as ast from '@angular/compiler'; |
| 5 | +import { Binary, PrefixNot } from '@angular/compiler/src/expression_parser/ast'; |
| 6 | +import { AST, BindingPipe, LiteralPrimitive } from '@angular/compiler'; |
7 | 7 |
|
8 | 8 | const unstrictEqualityOperator = '==';
|
9 | 9 |
|
10 |
| -class TemplateToNgTemplateVisitor extends RecursiveAngularExpressionVisitor { |
11 |
| - visitBinary(expr: e.Binary, context: any): any { |
12 |
| - if (!this.isAsyncBinding(expr.left)) { |
13 |
| - return super.visitBinary(expr, context); |
14 |
| - } |
15 |
| - if (!(expr.right instanceof ast.LiteralPrimitive) || expr.right.value !== false || expr.operation !== unstrictEqualityOperator) { |
16 |
| - return super.visitBinary(expr, context); |
17 |
| - } |
| 10 | +const isAsyncBinding = (ast: AST): boolean => { |
| 11 | + return ast instanceof BindingPipe && ast.name === 'async'; |
| 12 | +}; |
18 | 13 |
|
19 |
| - const { |
20 |
| - left: { |
21 |
| - span: { end: endLeftSpan } |
22 |
| - }, |
23 |
| - right: { |
24 |
| - span: { start: startRightSpan } |
25 |
| - }, |
26 |
| - span: { end: spanEnd, start: spanStart } |
27 |
| - } = expr; |
28 |
| - const operator = this.codeWithMap.code.slice(endLeftSpan, startRightSpan); |
29 |
| - const operatorStart = /^.*==/.exec(operator)![0].length - unstrictEqualityOperator.length; |
| 14 | +class TemplateToNgTemplateVisitor extends RecursiveAngularExpressionVisitor { |
| 15 | + visitBinary(ast: Binary, context: any): any { |
| 16 | + this.validateBinary(ast); |
| 17 | + super.visitBinary(ast, context); |
| 18 | + } |
30 | 19 |
|
31 |
| - this.addFailureFromStartToEnd(spanStart, spanEnd, 'Async pipes must use strict equality `===` when comparing with `false`', [ |
32 |
| - new Lint.Replacement(this.getSourcePosition(endLeftSpan) + operatorStart, unstrictEqualityOperator.length, '===') |
33 |
| - ]); |
34 |
| - super.visitBinary(expr, context); |
| 20 | + visitPrefixNot(ast: PrefixNot, context: any): any { |
| 21 | + this.validatePrefixNot(ast); |
| 22 | + super.visitPrefixNot(ast, context); |
35 | 23 | }
|
36 | 24 |
|
37 |
| - visitPrefixNot(expr: e.PrefixNot, context: any): any { |
38 |
| - if (!this.isAsyncBinding(expr.expression)) { |
39 |
| - return super.visitPrefixNot(expr, context); |
| 25 | + private validateBinary(ast: Binary): void { |
| 26 | + const { left, operation, right } = ast; |
| 27 | + |
| 28 | + if (!isAsyncBinding(left) || !(right instanceof LiteralPrimitive) || right.value !== false || operation !== unstrictEqualityOperator) { |
| 29 | + return; |
40 | 30 | }
|
41 |
| - const { |
42 |
| - span: { end: spanEnd, start: spanStart } |
43 |
| - } = expr; |
44 |
| - const absoluteStart = this.getSourcePosition(spanStart); |
45 | 31 |
|
46 |
| - // Angular includes the whitespace after an expression, we want to trim that |
47 |
| - const expressionSource = this.codeWithMap.code.slice(spanStart, spanEnd); |
48 |
| - const concreteWidth = spanEnd - spanStart - / *$/.exec(expressionSource)![0].length; |
| 32 | + this.generateFailure(ast, Rule.FAILURE_STRING_UNSTRICT_EQUALITY); |
| 33 | + } |
| 34 | + |
| 35 | + private validatePrefixNot(ast: PrefixNot): void { |
| 36 | + const { expression } = ast; |
| 37 | + |
| 38 | + if (!isAsyncBinding(expression)) { |
| 39 | + return; |
| 40 | + } |
49 | 41 |
|
50 |
| - this.addFailureFromStartToEnd(spanStart, spanEnd, 'Async pipes can not be negated, use (observable | async) === false instead', [ |
51 |
| - new Lint.Replacement(absoluteStart + concreteWidth, 1, ' === false '), |
52 |
| - new Lint.Replacement(absoluteStart, 1, '') |
53 |
| - ]); |
| 42 | + this.generateFailure(ast, Rule.FAILURE_STRING_NEGATED_PIPE); |
54 | 43 | }
|
55 | 44 |
|
56 |
| - protected isAsyncBinding(expr: ast.AST) { |
57 |
| - return expr instanceof ast.BindingPipe && expr.name === 'async'; |
| 45 | + private generateFailure(ast: Binary | PrefixNot, errorMessage: string): void { |
| 46 | + const { |
| 47 | + span: { end: spanEnd, start: spanStart } |
| 48 | + } = ast; |
| 49 | + |
| 50 | + this.addFailureFromStartToEnd(spanStart, spanEnd, errorMessage); |
58 | 51 | }
|
59 | 52 | }
|
60 | 53 |
|
61 |
| -export class Rule extends Lint.Rules.AbstractRule { |
62 |
| - public static metadata: Lint.IRuleMetadata = { |
63 |
| - ruleName: 'templates-no-negated-async', |
64 |
| - type: 'functionality', |
| 54 | +export class Rule extends Rules.AbstractRule { |
| 55 | + static readonly metadata: IRuleMetadata = { |
65 | 56 | description: 'Ensures that strict equality is used when evaluating negations on async pipe output.',
|
| 57 | + options: null, |
| 58 | + optionsDescription: 'Not configurable.', |
66 | 59 | rationale:
|
67 | 60 | 'Async pipe evaluate to `null` before the observable or promise emits, which can lead to layout thrashing as' +
|
68 | 61 | ' components load. Prefer strict `=== false` checks instead.',
|
69 |
| - options: null, |
70 |
| - optionsDescription: 'Not configurable.', |
71 |
| - typescriptOnly: true, |
72 |
| - hasFix: true |
| 62 | + ruleName: 'templates-no-negated-async', |
| 63 | + type: 'functionality', |
| 64 | + typescriptOnly: true |
73 | 65 | };
|
74 | 66 |
|
75 |
| - public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { |
| 67 | + static readonly FAILURE_STRING_NEGATED_PIPE = 'Async pipes can not be negated, use (observable | async) === false instead'; |
| 68 | + static readonly FAILURE_STRING_UNSTRICT_EQUALITY = 'Async pipes must use strict equality `===` when comparing with `false`'; |
| 69 | + |
| 70 | + apply(sourceFile: SourceFile): RuleFailure[] { |
76 | 71 | return this.applyWithWalker(
|
77 | 72 | new NgWalker(sourceFile, this.getOptions(), {
|
78 | 73 | expressionVisitorCtrl: TemplateToNgTemplateVisitor
|
|
0 commit comments