Skip to content

Commit f67f3fd

Browse files
committed
build: add rule to warn for undecorated base classes
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 03a9a39 commit f67f3fd

File tree

2 files changed

+94
-0
lines changed

2 files changed

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

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": true,
101102
"setters-after-getters": true,
102103
"ng-on-changes-property-access": true,
103104
"rxjs-imports": true,

0 commit comments

Comments
 (0)