|
| 1 | +import * as Lint from "tslint"; |
| 2 | +import * as ts from "typescript"; |
| 3 | +import * as tsutils from "tsutils"; |
| 4 | +import { tsquery } from "@phenomnomnominal/tsquery"; |
| 5 | +import { dedent } from "tslint/lib/utils"; |
| 6 | + |
| 7 | +export class Rule extends Lint.Rules.TypedRule { |
| 8 | + |
| 9 | + public static FAILURE_STRING = 'call to super.{methodName}() is missing'; |
| 10 | + |
| 11 | + public static metadata: Lint.IRuleMetadata = { |
| 12 | + description: dedent`Enforces the application to call parent lifecycle function e.g. super.ngOnDestroy() |
| 13 | + when using inheritance within an Angular component or directive.`, |
| 14 | + options: null, |
| 15 | + optionsDescription: "", |
| 16 | + requiresTypeInfo: true, |
| 17 | + ruleName: "angular-call-super-lifecycle-method-in-extended-class", |
| 18 | + type: "functionality", |
| 19 | + typescriptOnly: true |
| 20 | + }; |
| 21 | + |
| 22 | + private componentLifeCycleMethods: string[] = [ |
| 23 | + "ngOnDestroy", |
| 24 | + "ngOnInit", |
| 25 | + "ngAfterViewInit", |
| 26 | + "ngAfterContentInit", |
| 27 | + "ngOnChanges", |
| 28 | + "ngDoCheck", |
| 29 | + "ngAfterContentChecked", |
| 30 | + "ngAfterViewChecked" |
| 31 | + ]; |
| 32 | + private directiveLifeCycleMethods: string[] = [ |
| 33 | + "ngOnDestroy", |
| 34 | + "ngOnInit" |
| 35 | + ]; |
| 36 | + |
| 37 | + public applyWithProgram( |
| 38 | + sourceFile: ts.SourceFile, |
| 39 | + program: ts.Program |
| 40 | + ): Lint.RuleFailure[] { |
| 41 | + const failures: Lint.RuleFailure[] = []; |
| 42 | + |
| 43 | + // find all classes with an @Component() decorator and heritage clause |
| 44 | + const componentClassDeclarations = tsquery( |
| 45 | + sourceFile, |
| 46 | + `ClassDeclaration:has(Decorator[expression.expression.name='Component']):has(HeritageClause:has(ExpressionWithTypeArguments))` |
| 47 | + ) as ts.ClassDeclaration[]; |
| 48 | + |
| 49 | + // find all classes with an @Directive() decorator and heritage clause |
| 50 | + const directiveClassDeclarations = tsquery( |
| 51 | + sourceFile, |
| 52 | + `ClassDeclaration:has(Decorator[expression.expression.name='Directive']):has(HeritageClause:has(ExpressionWithTypeArguments))` |
| 53 | + ) as ts.ClassDeclaration[]; |
| 54 | + |
| 55 | + // check all components |
| 56 | + [ |
| 57 | + ...componentClassDeclarations, |
| 58 | + ...componentClassDeclarations |
| 59 | + .map(classDeclaration => this.findParentClasses(program, classDeclaration)) |
| 60 | + .reduce((allParentClasses, parentClasses) => [...allParentClasses, ...parentClasses], []) |
| 61 | + ].forEach(classDeclaration => { |
| 62 | + failures.push( |
| 63 | + ...this.checkLifeCycleMethodSuperInvokation( |
| 64 | + classDeclaration.getSourceFile(), |
| 65 | + program, |
| 66 | + classDeclaration as ts.ClassDeclaration, |
| 67 | + false |
| 68 | + ) |
| 69 | + ); |
| 70 | + }); |
| 71 | + |
| 72 | + // check all directives |
| 73 | + [ |
| 74 | + ...directiveClassDeclarations, |
| 75 | + ...directiveClassDeclarations |
| 76 | + .map(classDeclaration => this.findParentClasses(program, classDeclaration)) |
| 77 | + .reduce((allParentClasses, parentClasses) => [...allParentClasses, ...parentClasses], []) |
| 78 | + ].forEach(classDeclaration => { |
| 79 | + failures.push( |
| 80 | + ...this.checkLifeCycleMethodSuperInvokation( |
| 81 | + classDeclaration.getSourceFile(), |
| 82 | + program, |
| 83 | + classDeclaration as ts.ClassDeclaration, |
| 84 | + true |
| 85 | + ) |
| 86 | + ); |
| 87 | + }); |
| 88 | + |
| 89 | + return failures; |
| 90 | + } |
| 91 | + |
| 92 | + /** |
| 93 | + * Verify that a class implementing a lifecycle method calls super.lifeCycleMethod() if it overrides any parent implementation of it |
| 94 | + */ |
| 95 | + private checkLifeCycleMethodSuperInvokation(sourceFile: ts.SourceFile, |
| 96 | + program: ts.Program, |
| 97 | + classDeclaration: ts.ClassDeclaration, |
| 98 | + isDirective: boolean): Lint.RuleFailure[] { |
| 99 | + const lintFailures: Lint.RuleFailure[] = []; |
| 100 | + const lifeCycleMethodsToCheck = isDirective ? this.directiveLifeCycleMethods : this.componentLifeCycleMethods; |
| 101 | + |
| 102 | + // check all life cycle methods |
| 103 | + lifeCycleMethodsToCheck.forEach(lifeCycleMethodName => { |
| 104 | + // find an implementation of the lifecycle method |
| 105 | + const lifeCycleMethod = this.findLifeCycleMethod(classDeclaration, lifeCycleMethodName); |
| 106 | + |
| 107 | + // if implementation found, check parent implementations are called when overriding |
| 108 | + if (lifeCycleMethod) { |
| 109 | + const parentClasses = this.findParentClasses(program, classDeclaration); |
| 110 | + if (parentClasses.some(parentClass => !!this.findLifeCycleMethod(parentClass, lifeCycleMethodName))) { |
| 111 | + // some parent has life cycle method implementation, ensure super.lifeCycleMethod() is called |
| 112 | + const superLifeCycleMethodCall = this.findSuperLifeCycleMethodInvocation(lifeCycleMethod, lifeCycleMethodName); |
| 113 | + if (!superLifeCycleMethodCall) { |
| 114 | + lintFailures.push( |
| 115 | + new Lint.RuleFailure( |
| 116 | + sourceFile, |
| 117 | + lifeCycleMethod.name ? lifeCycleMethod.name.getStart() : sourceFile.getStart(), |
| 118 | + lifeCycleMethod.name ? lifeCycleMethod.name.getStart() + |
| 119 | + lifeCycleMethod.name.getWidth() : sourceFile.getStart() + sourceFile.getWidth(), |
| 120 | + Rule.FAILURE_STRING.replace("{methodName}", lifeCycleMethod.name.getText()), |
| 121 | + this.ruleName |
| 122 | + ) |
| 123 | + ) |
| 124 | + } |
| 125 | + } |
| 126 | + } |
| 127 | + }); |
| 128 | + return lintFailures; |
| 129 | + } |
| 130 | + |
| 131 | + /** |
| 132 | + * Returns the method declaration of the life cycle method implemenation in the class given |
| 133 | + */ |
| 134 | + private findLifeCycleMethod( |
| 135 | + classDeclaration: ts.ClassDeclaration, |
| 136 | + lifeCycleMethodName: string |
| 137 | + ): ts.MethodDeclaration { |
| 138 | + return classDeclaration.members.find( |
| 139 | + member => member.name && member.name.getText() === lifeCycleMethodName |
| 140 | + ) as ts.MethodDeclaration; |
| 141 | + } |
| 142 | + |
| 143 | + /** |
| 144 | + * Returns the property access expression of the invocation of super.lifeCycleMethod(), if any |
| 145 | + */ |
| 146 | + private findSuperLifeCycleMethodInvocation( |
| 147 | + methodDeclaration: ts.MethodDeclaration, |
| 148 | + lifeCycleMethodName: string |
| 149 | + ): ts.PropertyAccessExpression | undefined { |
| 150 | + |
| 151 | + if (!methodDeclaration.body) { |
| 152 | + return undefined; |
| 153 | + } |
| 154 | + |
| 155 | + const propertyAccessExpressions = tsquery( |
| 156 | + methodDeclaration, |
| 157 | + `CallExpression > PropertyAccessExpression[expression.kind=${ts.SyntaxKind.SuperKeyword}][name.text="${lifeCycleMethodName}"]` |
| 158 | + ) as ts.PropertyAccessExpression[]; |
| 159 | + if (propertyAccessExpressions && propertyAccessExpressions.length > 0) { |
| 160 | + return propertyAccessExpressions[0]; |
| 161 | + } |
| 162 | + } |
| 163 | + |
| 164 | + /** |
| 165 | + * recursively find all parent classes of the class given |
| 166 | + */ |
| 167 | + private findParentClasses( |
| 168 | + program: ts.Program, |
| 169 | + classDeclarationToBeChecked: ts.ClassDeclaration |
| 170 | + ): ts.ClassDeclaration[] { |
| 171 | + const classDeclarationsFound: ts.ClassDeclaration[] = []; |
| 172 | + const typeChecker = program.getTypeChecker(); |
| 173 | + |
| 174 | + const heritageClauses = classDeclarationToBeChecked.heritageClauses; |
| 175 | + |
| 176 | + if (!heritageClauses) { |
| 177 | + return []; |
| 178 | + } |
| 179 | + heritageClauses.forEach(heritageClause => { |
| 180 | + if (heritageClause.token === ts.SyntaxKind.ExtendsKeyword) { |
| 181 | + heritageClause.types.forEach(heritageClauseType => { |
| 182 | + if (!tsutils.isIdentifier(heritageClauseType.expression)) { |
| 183 | + return; |
| 184 | + } |
| 185 | + const extendType = typeChecker.getTypeAtLocation(heritageClauseType.expression); |
| 186 | + if (extendType && extendType.symbol |
| 187 | + && extendType.symbol.declarations |
| 188 | + && extendType.symbol.declarations.length > 0 |
| 189 | + && tsutils.isClassDeclaration(extendType.symbol.declarations[0]) |
| 190 | + ) { |
| 191 | + const parentClassDeclaration = extendType.symbol.declarations[0] as ts.ClassDeclaration; |
| 192 | + classDeclarationsFound.push(parentClassDeclaration); |
| 193 | + classDeclarationsFound.push(...this.findParentClasses(program, parentClassDeclaration)) |
| 194 | + } |
| 195 | + }) |
| 196 | + } |
| 197 | + }); |
| 198 | + return classDeclarationsFound; |
| 199 | + }; |
| 200 | +} |
0 commit comments