-
Notifications
You must be signed in to change notification settings - Fork 235
/
templatesNoNegatedAsyncRule.ts
78 lines (63 loc) · 2.79 KB
/
templatesNoNegatedAsyncRule.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
import { AST, BindingPipe, LiteralPrimitive } from '@angular/compiler';
import { Binary, PrefixNot } from '@angular/compiler/src/expression_parser/ast';
import { IRuleMetadata, RuleFailure, Rules, Utils } from 'tslint/lib';
import { SourceFile } from 'typescript';
import { NgWalker } from './angular/ngWalker';
import { RecursiveAngularExpressionVisitor } from './angular/templates/recursiveAngularExpressionVisitor';
const unstrictEqualityOperator = '==';
const isAsyncBinding = (ast: AST): boolean => {
return ast instanceof BindingPipe && ast.name === 'async';
};
class TemplateToNgTemplateVisitor extends RecursiveAngularExpressionVisitor {
visitBinary(ast: Binary, context: any): any {
this.validateBinary(ast);
super.visitBinary(ast, context);
}
visitPrefixNot(ast: PrefixNot, context: any): any {
this.validatePrefixNot(ast);
super.visitPrefixNot(ast, context);
}
private validateBinary(ast: Binary): void {
const { left, operation, right } = ast;
if (!isAsyncBinding(left) || !(right instanceof LiteralPrimitive) || right.value !== false || operation !== unstrictEqualityOperator) {
return;
}
this.generateFailure(ast, Rule.FAILURE_STRING_UNSTRICT_EQUALITY);
}
private validatePrefixNot(ast: PrefixNot): void {
const { expression } = ast;
if (!isAsyncBinding(expression)) {
return;
}
this.generateFailure(ast, Rule.FAILURE_STRING_NEGATED_PIPE);
}
private generateFailure(ast: Binary | PrefixNot, errorMessage: string): void {
const {
span: { end: spanEnd, start: spanStart }
} = ast;
this.addFailureFromStartToEnd(spanStart, spanEnd, errorMessage);
}
}
export class Rule extends Rules.AbstractRule {
static readonly metadata: IRuleMetadata = {
description: 'Ensures that strict equality is used when evaluating negations on async pipe output.',
options: null,
optionsDescription: 'Not configurable.',
rationale: Utils.dedent`
Angular's async pipes emit null initially, prior to the observable emitting any values, or the promise resolving. This can cause negations, like
*ngIf="!(myConditional | async)" to thrash the layout and cause expensive side-effects like firing off XHR requests for a component which should not be shown.
`,
ruleName: 'templates-no-negated-async',
type: 'functionality',
typescriptOnly: true
};
static readonly FAILURE_STRING_NEGATED_PIPE = 'Async pipes can not be negated, use (observable | async) === false instead';
static readonly FAILURE_STRING_UNSTRICT_EQUALITY = 'Async pipes must use strict equality `===` when comparing with `false`';
apply(sourceFile: SourceFile): RuleFailure[] {
return this.applyWithWalker(
new NgWalker(sourceFile, this.getOptions(), {
expressionVisitorCtrl: TemplateToNgTemplateVisitor
})
);
}
}