146 lines
5.4 KiB
TypeScript
146 lines
5.4 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright Google LLC All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
* found in the LICENSE file at https://angular.io/license
|
|
*/
|
|
|
|
import {Replacement, RuleFailure, WalkContext} from 'tslint/lib';
|
|
import {TypedRule} from 'tslint/lib/rules';
|
|
import * as ts from 'typescript';
|
|
|
|
const FAILURE_MESSAGE = 'Missing override modifier. Members implemented as part of ' +
|
|
'abstract classes must explicitly set the "override" modifier. ' +
|
|
'More details: https://github.com/microsoft/TypeScript/issues/44457#issuecomment-856202843.';
|
|
|
|
/**
|
|
* Rule which enforces that class members implementing abstract members
|
|
* from base classes explicitly specify the `override` modifier.
|
|
*
|
|
* This ensures we follow the best-practice of applying `override` for abstract-implemented
|
|
* members so that TypeScript creates diagnostics in both scenarios where either the abstract
|
|
* class member is removed, or renamed.
|
|
*
|
|
* More details can be found here: https://github.com/microsoft/TypeScript/issues/44457.
|
|
*/
|
|
export class Rule extends TypedRule {
|
|
override applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] {
|
|
return this.applyWithFunction(sourceFile, ctx => visitNode(sourceFile, ctx, program));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* For a TypeScript AST node and each of its child nodes, check whether the node is a class
|
|
* element which implements an abstract member but does not have the `override` keyword.
|
|
*/
|
|
function visitNode(node: ts.Node, ctx: WalkContext, program: ts.Program) {
|
|
// If a class element implements an abstract member but does not have the
|
|
// `override` keyword, create a lint failure.
|
|
if (ts.isClassElement(node) && !hasOverrideModifier(node) &&
|
|
matchesParentAbstractElement(node, program)) {
|
|
ctx.addFailureAtNode(
|
|
node, FAILURE_MESSAGE, Replacement.appendText(node.getStart(), `override `));
|
|
}
|
|
|
|
ts.forEachChild(node, node => visitNode(node, ctx, program));
|
|
}
|
|
|
|
/**
|
|
* Checks if the specified class element matches a parent abstract class element. i.e.
|
|
* whether the specified member "implements" an abstract member from a base class.
|
|
*/
|
|
function matchesParentAbstractElement(node: ts.ClassElement, program: ts.Program): boolean {
|
|
const containingClass = node.parent as ts.ClassDeclaration;
|
|
|
|
// If the property we check does not have a property name, we cannot look for similarly-named
|
|
// members in parent classes and therefore return early.
|
|
if (node.name === undefined) {
|
|
return false;
|
|
}
|
|
|
|
const propertyName = getPropertyNameText(node.name);
|
|
const typeChecker = program.getTypeChecker();
|
|
|
|
// If the property we check does not have a statically-analyzable property name,
|
|
// we cannot look for similarly-named members in parent classes and return early.
|
|
if (propertyName === null) {
|
|
return false;
|
|
}
|
|
|
|
return checkClassForInheritedMatchingAbstractMember(containingClass, typeChecker, propertyName);
|
|
}
|
|
|
|
/** Checks if the given class inherits an abstract member with the specified name. */
|
|
function checkClassForInheritedMatchingAbstractMember(
|
|
clazz: ts.ClassDeclaration, typeChecker: ts.TypeChecker, searchMemberName: string): boolean {
|
|
const baseClass = getBaseClass(clazz, typeChecker);
|
|
|
|
// If the class is not `abstract`, then all parent abstract methods would need to
|
|
// be implemented, and there is never an abstract member within the class.
|
|
if (baseClass === null || !hasAbstractModifier(baseClass)) {
|
|
return false;
|
|
}
|
|
|
|
const matchingMember = baseClass.members.find(
|
|
m => m.name !== undefined && getPropertyNameText(m.name) === searchMemberName);
|
|
|
|
if (matchingMember !== undefined) {
|
|
return hasAbstractModifier(matchingMember);
|
|
}
|
|
|
|
return checkClassForInheritedMatchingAbstractMember(baseClass, typeChecker, searchMemberName);
|
|
}
|
|
|
|
/** Gets the base class for the given class declaration. */
|
|
function getBaseClass(node: ts.ClassDeclaration, typeChecker: ts.TypeChecker): ts.ClassDeclaration|
|
|
null {
|
|
const baseTypes = getExtendsHeritageExpressions(node);
|
|
|
|
if (baseTypes.length > 1) {
|
|
throw Error('Class unexpectedly extends from multiple types.');
|
|
}
|
|
|
|
const baseClass = typeChecker.getTypeAtLocation(baseTypes[0]).getSymbol();
|
|
const baseClassDecl = baseClass?.valueDeclaration ?? baseClass?.declarations?.[0];
|
|
|
|
if (baseClassDecl !== undefined && ts.isClassDeclaration(baseClassDecl)) {
|
|
return baseClassDecl;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/** Gets the `extends` base type expressions of the specified class. */
|
|
function getExtendsHeritageExpressions(classDecl: ts.ClassDeclaration):
|
|
ts.ExpressionWithTypeArguments[] {
|
|
if (classDecl.heritageClauses === undefined) {
|
|
return [];
|
|
}
|
|
const result: ts.ExpressionWithTypeArguments[] = [];
|
|
for (const clause of classDecl.heritageClauses) {
|
|
if (clause.token === ts.SyntaxKind.ExtendsKeyword) {
|
|
result.push(...clause.types);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/** Gets whether the specified node has the `abstract` modifier applied. */
|
|
function hasAbstractModifier(node: ts.Node): boolean {
|
|
return !!node.modifiers?.some(s => s.kind === ts.SyntaxKind.AbstractKeyword);
|
|
}
|
|
|
|
/** Gets whether the specified node has the `override` modifier applied. */
|
|
function hasOverrideModifier(node: ts.Node): boolean {
|
|
return !!node.modifiers?.some(s => s.kind === ts.SyntaxKind.OverrideKeyword);
|
|
}
|
|
|
|
/** Gets the property name text of the specified property name. */
|
|
function getPropertyNameText(name: ts.PropertyName): string|null {
|
|
if (ts.isComputedPropertyName(name)) {
|
|
return null;
|
|
}
|
|
return name.text;
|
|
}
|