|
| 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 | +} |
0 commit comments