Skip to content

Commit fb15e89

Browse files
devversionjelbourn
authored andcommitted
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
1 parent 3968918 commit fb15e89

File tree

2 files changed

+97
-0
lines changed

2 files changed

+97
-0
lines changed
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import * as Lint from 'tslint';
2+
import * as ts from 'typescript';
3+
4+
const RULE_FAILURE = `Class inherits constructor using dependency injection from ` +
5+
`undecorated base class. This breaks dependency injection with Ivy and can be fixed ` +
6+
`by creating an explicit pass-through constructor.`;
7+
8+
/**
9+
* Rule that doesn't allow inheriting a constructor using dependency injection from an
10+
* undecorated base class. With Ivy, undecorated base classes cannot use dependency
11+
* injection. Classes that inherit the constructor from the base class can specify
12+
* an explicit pass-through constructor to make DI work.
13+
*/
14+
export class Rule extends Lint.Rules.TypedRule {
15+
applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] {
16+
return this.applyWithWalker(
17+
new Walker(sourceFile, this.getOptions(), program.getTypeChecker()));
18+
}
19+
}
20+
21+
class Walker extends Lint.RuleWalker {
22+
constructor(
23+
sourceFile: ts.SourceFile, options: Lint.IOptions, private typeChecker: ts.TypeChecker) {
24+
super(sourceFile, options);
25+
}
26+
27+
visitClassDeclaration(node: ts.ClassDeclaration) {
28+
if (!this.hasDirectiveDecorator(node)) {
29+
return;
30+
}
31+
32+
// If the class already has an explicit constructor, it's not required
33+
// for base classes to be decorated.
34+
if (this.hasExplicitConstructor(node)) {
35+
return;
36+
}
37+
38+
const baseClass = this.getConstructorBaseClass(node);
39+
if (baseClass && !this.hasDirectiveDecorator(baseClass)) {
40+
this.addFailureAtNode(node, RULE_FAILURE);
41+
}
42+
}
43+
44+
/** Checks whether the given class declaration has an explicit constructor. */
45+
hasExplicitConstructor(node: ts.ClassDeclaration): boolean {
46+
return node.members.some(ts.isConstructorDeclaration);
47+
}
48+
49+
/** Checks if the specified node has a "@Directive" or "@Component" decorator. */
50+
hasDirectiveDecorator(node: ts.ClassDeclaration): boolean {
51+
return !!node.decorators && node.decorators.some(d => {
52+
if (!ts.isCallExpression(d.expression)) {
53+
return false;
54+
}
55+
56+
const decoratorText = d.expression.expression.getText();
57+
return decoratorText === 'Directive' || decoratorText === 'Component';
58+
});
59+
}
60+
61+
/**
62+
* Gets the first inherited class of the specified class that has an
63+
* explicit constructor.
64+
*/
65+
getConstructorBaseClass(node: ts.ClassDeclaration): ts.ClassDeclaration|null {
66+
let currentClass = node;
67+
while (currentClass) {
68+
const baseTypes = this.getBaseTypeIdentifiers(currentClass);
69+
if (!baseTypes || baseTypes.length !== 1) {
70+
return null;
71+
}
72+
const symbol = this.typeChecker.getTypeAtLocation(baseTypes[0]).getSymbol();
73+
if (!symbol || !ts.isClassDeclaration(symbol.valueDeclaration)) {
74+
return null;
75+
}
76+
if (this.hasExplicitConstructor(symbol.valueDeclaration)) {
77+
return symbol.valueDeclaration;
78+
}
79+
currentClass = symbol.valueDeclaration;
80+
}
81+
return null;
82+
}
83+
84+
/** Determines the base type identifiers of a specified class declaration. */
85+
getBaseTypeIdentifiers(node: ts.ClassDeclaration): ts.Identifier[]|null {
86+
if (!node.heritageClauses) {
87+
return null;
88+
}
89+
90+
return node.heritageClauses.filter(clause => clause.token === ts.SyntaxKind.ExtendsKeyword)
91+
.reduce(
92+
(types, clause) => types.concat(clause.types), [] as ts.ExpressionWithTypeArguments[])
93+
.map(typeExpression => typeExpression.expression)
94+
.filter(ts.isIdentifier);
95+
}
96+
}

tslint.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@
9898
"no-exposed-todo": true,
9999
"no-import-spacing": true,
100100
"no-private-getters": true,
101+
"no-undecorated-base-class-di": true,
101102
"setters-after-getters": true,
102103
"ng-on-changes-property-access": true,
103104
"rxjs-imports": true,

0 commit comments

Comments
 (0)