build: add rule to warn for undecorated base classes (#15976)

With Ivy, directives or components no longer properly inherit
a constructor from a parent base class which is undecorated.

See more details here: FW-1238

In order to avoid dependency injection issues with Ivy, a new
custom TSLint rule reports a failure when a directive/component
uses dependency injection in a way that breaks with Ivy.

Related to #15975
This commit is contained in:
Paul Gschwendtner
2019-05-10 23:29:12 +02:00
committed by Jeremy Elbourn
parent 3968918e55
commit fb15e89426
2 changed files with 97 additions and 0 deletions
@@ -0,0 +1,96 @@
import * as Lint from 'tslint';
import * as ts from 'typescript';
const RULE_FAILURE = `Class inherits constructor using dependency injection from ` +
`undecorated base class. This breaks dependency injection with Ivy and can be fixed ` +
`by creating an explicit pass-through constructor.`;
/**
* Rule that doesn't allow inheriting a constructor using dependency injection from an
* undecorated base class. With Ivy, undecorated base classes cannot use dependency
* injection. Classes that inherit the constructor from the base class can specify
* an explicit pass-through constructor to make DI work.
*/
export class Rule extends Lint.Rules.TypedRule {
applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] {
return this.applyWithWalker(
new Walker(sourceFile, this.getOptions(), program.getTypeChecker()));
}
}
class Walker extends Lint.RuleWalker {
constructor(
sourceFile: ts.SourceFile, options: Lint.IOptions, private typeChecker: ts.TypeChecker) {
super(sourceFile, options);
}
visitClassDeclaration(node: ts.ClassDeclaration) {
if (!this.hasDirectiveDecorator(node)) {
return;
}
// If the class already has an explicit constructor, it's not required
// for base classes to be decorated.
if (this.hasExplicitConstructor(node)) {
return;
}
const baseClass = this.getConstructorBaseClass(node);
if (baseClass && !this.hasDirectiveDecorator(baseClass)) {
this.addFailureAtNode(node, RULE_FAILURE);
}
}
/** Checks whether the given class declaration has an explicit constructor. */
hasExplicitConstructor(node: ts.ClassDeclaration): boolean {
return node.members.some(ts.isConstructorDeclaration);
}
/** Checks if the specified node has a "@Directive" or "@Component" decorator. */
hasDirectiveDecorator(node: ts.ClassDeclaration): boolean {
return !!node.decorators && node.decorators.some(d => {
if (!ts.isCallExpression(d.expression)) {
return false;
}
const decoratorText = d.expression.expression.getText();
return decoratorText === 'Directive' || decoratorText === 'Component';
});
}
/**
* Gets the first inherited class of the specified class that has an
* explicit constructor.
*/
getConstructorBaseClass(node: ts.ClassDeclaration): ts.ClassDeclaration|null {
let currentClass = node;
while (currentClass) {
const baseTypes = this.getBaseTypeIdentifiers(currentClass);
if (!baseTypes || baseTypes.length !== 1) {
return null;
}
const symbol = this.typeChecker.getTypeAtLocation(baseTypes[0]).getSymbol();
if (!symbol || !ts.isClassDeclaration(symbol.valueDeclaration)) {
return null;
}
if (this.hasExplicitConstructor(symbol.valueDeclaration)) {
return symbol.valueDeclaration;
}
currentClass = symbol.valueDeclaration;
}
return null;
}
/** Determines the base type identifiers of a specified class declaration. */
getBaseTypeIdentifiers(node: ts.ClassDeclaration): ts.Identifier[]|null {
if (!node.heritageClauses) {
return null;
}
return node.heritageClauses.filter(clause => clause.token === ts.SyntaxKind.ExtendsKeyword)
.reduce(
(types, clause) => types.concat(clause.types), [] as ts.ExpressionWithTypeArguments[])
.map(typeExpression => typeExpression.expression)
.filter(ts.isIdentifier);
}
}
+1
View File
@@ -98,6 +98,7 @@
"no-exposed-todo": true,
"no-import-spacing": true,
"no-private-getters": true,
"no-undecorated-base-class-di": true,
"setters-after-getters": true,
"ng-on-changes-property-access": true,
"rxjs-imports": true,