|
| 1 | +/** |
| 2 | + * @license |
| 3 | + * Copyright Google LLC All Rights Reserved. |
| 4 | + * |
| 5 | + * Use of this source code is governed by an MIT-style license that can be |
| 6 | + * found in the LICENSE file at https://angular.io/license |
| 7 | + */ |
| 8 | + |
| 9 | +import {Replacement, RuleFailure, WalkContext} from 'tslint/lib'; |
| 10 | +import {TypedRule} from 'tslint/lib/rules'; |
| 11 | +import * as ts from 'typescript'; |
| 12 | + |
| 13 | +const FAILURE_MESSAGE = 'Missing override modifier. Members implemented as part of ' + |
| 14 | + 'abstract classes should explicitly set the "override" modifier. ' + |
| 15 | + 'More details: https://github.com/microsoft/TypeScript/issues/44457#issuecomment-856202843.'; |
| 16 | + |
| 17 | +/** |
| 18 | + * Rule which enforces that class members implementing abstract members |
| 19 | + * from base classes explicitly specify the `override` modifier. |
| 20 | + * |
| 21 | + * This ensures we follow the best-practice of applying `override` for abstract-implemented |
| 22 | + * members so that TypeScript creates diagnostics in both scenarios where either the abstract |
| 23 | + * class member is removed, or renamed. |
| 24 | + * |
| 25 | + * More details can be found here: https://github.com/microsoft/TypeScript/issues/44457. |
| 26 | + */ |
| 27 | +export class Rule extends TypedRule { |
| 28 | + override applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] { |
| 29 | + return this.applyWithFunction(sourceFile, ctx => visitNode(sourceFile, ctx, program)); |
| 30 | + } |
| 31 | +} |
| 32 | + |
| 33 | +function visitNode(node: ts.Node, ctx: WalkContext, program: ts.Program) { |
| 34 | + // If a class element implements an abstract member but does not have the |
| 35 | + // `override` keyword, create a lint failure. |
| 36 | + if (ts.isClassElement(node) && !hasOverrideModifier(node) && |
| 37 | + matchesParentAbstractElement(node, program)) { |
| 38 | + ctx.addFailureAtNode( |
| 39 | + node, FAILURE_MESSAGE, Replacement.appendText(node.getStart(), `override `)); |
| 40 | + } |
| 41 | + |
| 42 | + ts.forEachChild(node, node => visitNode(node, ctx, program)); |
| 43 | +} |
| 44 | + |
| 45 | +/** |
| 46 | + * Checks if the specified class element matches a parent abstract class element. i.e. |
| 47 | + * whether the specified member "implements" an abstract member from a base class. |
| 48 | + */ |
| 49 | +function matchesParentAbstractElement(node: ts.ClassElement, program: ts.Program): boolean { |
| 50 | + const containingClass = node.parent as ts.ClassDeclaration; |
| 51 | + |
| 52 | + // If the property we check does not have a property name, we cannot look for similarly-named |
| 53 | + // members in parent classes and therefore return early. |
| 54 | + if (node.name === undefined) { |
| 55 | + return false; |
| 56 | + } |
| 57 | + |
| 58 | + const propertyName = getPropertyNameText(node.name); |
| 59 | + const typeChecker = program.getTypeChecker(); |
| 60 | + |
| 61 | + // If the property we check does not have a statically-analyzable property name, |
| 62 | + // we cannot look for similarly-named members in parent classes and return early. |
| 63 | + if (propertyName === null) { |
| 64 | + return false; |
| 65 | + } |
| 66 | + |
| 67 | + return checkClassForInheritedMatchingAbstractMember(containingClass, typeChecker, propertyName); |
| 68 | +} |
| 69 | + |
| 70 | +/** Checks if the given class inherits an abstract member with the specified name. */ |
| 71 | +function checkClassForInheritedMatchingAbstractMember( |
| 72 | + clazz: ts.ClassDeclaration, typeChecker: ts.TypeChecker, searchMemberName: string): boolean { |
| 73 | + const baseClass = getBaseClass(clazz, typeChecker); |
| 74 | + |
| 75 | + // If the class is not `abstract`, then all parent abstract methods would need to |
| 76 | + // be implemented, and there is never an abstract member within the class. |
| 77 | + if (baseClass === null || !hasAbstractModifier(baseClass)) { |
| 78 | + return false; |
| 79 | + } |
| 80 | + |
| 81 | + const matchingMember = baseClass.members.find( |
| 82 | + m => m.name !== undefined && getPropertyNameText(m.name) === searchMemberName); |
| 83 | + |
| 84 | + if (matchingMember !== undefined) { |
| 85 | + return hasAbstractModifier(matchingMember); |
| 86 | + } |
| 87 | + |
| 88 | + return checkClassForInheritedMatchingAbstractMember(baseClass, typeChecker, searchMemberName); |
| 89 | +} |
| 90 | + |
| 91 | +/** Gets the base class for the given class declaration. */ |
| 92 | +function getBaseClass(node: ts.ClassDeclaration, typeChecker: ts.TypeChecker): ts.ClassDeclaration| |
| 93 | + null { |
| 94 | + const baseTypes = getExtendsHeritageExpressions(node); |
| 95 | + |
| 96 | + if (baseTypes.length > 1) { |
| 97 | + throw Error('Class unexpectedly extends from multiple types.'); |
| 98 | + } |
| 99 | + |
| 100 | + const baseClass = typeChecker.getTypeAtLocation(baseTypes[0]).getSymbol(); |
| 101 | + const baseClassDecl = baseClass?.valueDeclaration ?? baseClass?.declarations?.[0]; |
| 102 | + |
| 103 | + if (baseClassDecl !== undefined && ts.isClassDeclaration(baseClassDecl)) { |
| 104 | + return baseClassDecl; |
| 105 | + } |
| 106 | + |
| 107 | + return null; |
| 108 | +} |
| 109 | + |
| 110 | +/** Gets the `extends` base type expressions of the specified class. */ |
| 111 | +function getExtendsHeritageExpressions(classDecl: ts.ClassDeclaration): |
| 112 | + ts.ExpressionWithTypeArguments[] { |
| 113 | + if (classDecl.heritageClauses === undefined) { |
| 114 | + return []; |
| 115 | + } |
| 116 | + const result: ts.ExpressionWithTypeArguments[] = []; |
| 117 | + for (const clause of classDecl.heritageClauses) { |
| 118 | + if (clause.token === ts.SyntaxKind.ExtendsKeyword) { |
| 119 | + result.push(...clause.types); |
| 120 | + } |
| 121 | + } |
| 122 | + return result; |
| 123 | +} |
| 124 | + |
| 125 | +/** Gets whether the specified node has the `abstract` modifier applied. */ |
| 126 | +function hasAbstractModifier(node: ts.Node): boolean { |
| 127 | + return !!node.modifiers?.some(s => s.kind === ts.SyntaxKind.AbstractKeyword); |
| 128 | +} |
| 129 | + |
| 130 | +/** Gets whether the specified node has the `override` modifier applied. */ |
| 131 | +function hasOverrideModifier(node: ts.Node): boolean { |
| 132 | + return !!node.modifiers?.some(s => s.kind === ts.SyntaxKind.OverrideKeyword); |
| 133 | +} |
| 134 | + |
| 135 | +/** Gets the property name text of the specified property name. */ |
| 136 | +function getPropertyNameText(name: ts.PropertyName): string|null { |
| 137 | + if (ts.isComputedPropertyName(name)) { |
| 138 | + return null; |
| 139 | + } |
| 140 | + return name.text; |
| 141 | +} |
0 commit comments