Skip to content

Commit 76fb193

Browse files
crisbetommalerba
authored andcommitted
build: add lint rule to disallow invocation of lifecycle hooks (#18981)
Adds a lint rule that will flag any cases where a lifecycle hook is invoked directly. There's an exception for test files where we need to call `ngOnDestroy` manually on providers, as well as on `super`. Also fixes a memory leak in the calendar that prompted the rule to be written.
1 parent 174e4cd commit 76fb193

File tree

3 files changed

+71
-1
lines changed

3 files changed

+71
-1
lines changed

src/material/datepicker/calendar.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,7 @@ export class MatCalendar<D> implements AfterContentInit, AfterViewChecked, OnDes
364364
view = this.multiYearView;
365365
}
366366

367-
view.ngAfterContentInit();
367+
view._init();
368368
}
369369

370370
/** Handles date selection in the month view. */
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import * as path from 'path';
2+
import * as Lint from 'tslint';
3+
import * as ts from 'typescript';
4+
import * as minimatch from 'minimatch';
5+
6+
const hooks = new Set([
7+
'ngOnChanges',
8+
'ngOnInit',
9+
'ngDoCheck',
10+
'ngAfterContentInit',
11+
'ngAfterContentChecked',
12+
'ngAfterViewInit',
13+
'ngAfterViewChecked',
14+
'ngOnDestroy',
15+
'ngDoBootstrap'
16+
]);
17+
18+
/** Rule that prevents direct calls of the Angular lifecycle hooks */
19+
export class Rule extends Lint.Rules.AbstractRule {
20+
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
21+
return this.applyWithWalker(new Walker(sourceFile, this.getOptions()));
22+
}
23+
}
24+
25+
class Walker extends Lint.RuleWalker {
26+
/** Whether the walker should check the current source file. */
27+
private _enabled: boolean;
28+
29+
constructor(sourceFile: ts.SourceFile, options: Lint.IOptions) {
30+
super(sourceFile, options);
31+
const fileGlobs = options.ruleArguments;
32+
const relativeFilePath = path.relative(process.cwd(), sourceFile.fileName);
33+
this._enabled = fileGlobs.some(p => minimatch(relativeFilePath, p));
34+
}
35+
36+
visitPropertyAccessExpression(node: ts.PropertyAccessExpression) {
37+
// Flag any accesses of the lifecycle hooks that are
38+
// inside function call and don't match the allowed criteria.
39+
if (this._enabled && ts.isCallExpression(node.parent) && hooks.has(node.name.text) &&
40+
!this._isAllowedAccessor(node)) {
41+
this.addFailureAtNode(node, 'Manually invoking Angular lifecycle hooks is not allowed.');
42+
}
43+
44+
return super.visitPropertyAccessExpression(node);
45+
}
46+
47+
/** Checks whether the accessor of an Angular lifecycle hook expression is allowed. */
48+
private _isAllowedAccessor(node: ts.PropertyAccessExpression): boolean {
49+
// We only allow accessing the lifecycle hooks via super.
50+
if (node.expression.kind !== ts.SyntaxKind.SuperKeyword) {
51+
return false;
52+
}
53+
54+
let parent = node.parent;
55+
56+
// Even if the access is on a `super` expression, verify that the hook is being called
57+
// from inside a method with the same name (e.g. to avoid calling `ngAfterViewInit` from
58+
// inside `ngOnInit`).
59+
while (parent && !ts.isSourceFile(parent)) {
60+
if (ts.isMethodDeclaration(parent)) {
61+
return (parent.name as ts.Identifier).text === node.name.text;
62+
} else {
63+
parent = parent.parent;
64+
}
65+
}
66+
67+
return false;
68+
}
69+
}

tslint.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@
119119
"class-list-signatures": true,
120120
"no-nested-ternary": true,
121121
"prefer-const-enum": true,
122+
"no-lifecycle-invocation": [true, "**/!(*.spec).ts"],
122123
"coercion-types": [true,
123124
["coerceBooleanProperty", "coerceCssPixelValue", "coerceNumberProperty"],
124125
{

0 commit comments

Comments
 (0)