Skip to content

Commit 9f632cd

Browse files
committed
build: add lint rule to ensure classes with angular features are decorated
Read more about this here: https://hackmd.io/vuQfavzfRG6KUCtU7oK_EA.
1 parent 361d1cf commit 9f632cd

File tree

2 files changed

+68
-0
lines changed

2 files changed

+68
-0
lines changed
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import * as Lint from 'tslint';
2+
import * as ts from 'typescript';
3+
4+
const RULE_FAILURE = `Undecorated class defines fields with Angular decorators. In Ivy, such ` +
5+
`undecorated classes with Angular fields cannot be extended since no definition is generated. ` +
6+
`Add a "@Directive" decorator to fix this.`;
7+
8+
/**
9+
* Rule that doesn't allow undecorated class declarations with fields using Angular
10+
* decorators.
11+
*/
12+
export class Rule extends Lint.Rules.TypedRule {
13+
applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] {
14+
return this.applyWithWalker(
15+
new Walker(sourceFile, this.getOptions(), program.getTypeChecker()));
16+
}
17+
}
18+
19+
class Walker extends Lint.RuleWalker {
20+
constructor(
21+
sourceFile: ts.SourceFile, options: Lint.IOptions, private _typeChecker: ts.TypeChecker) {
22+
super(sourceFile, options);
23+
}
24+
25+
visitClassDeclaration(node: ts.ClassDeclaration) {
26+
if (this.hasAngularDecorator(node)) {
27+
return;
28+
}
29+
30+
for (let member of node.members) {
31+
if (member.decorators && this.hasAngularDecorator(member)) {
32+
this.addFailureAtNode(node, RULE_FAILURE);
33+
return;
34+
}
35+
}
36+
}
37+
38+
/** Checks if the specified node has an Angular decorator. */
39+
hasAngularDecorator(node: ts.Node): boolean {
40+
return !!node.decorators && node.decorators.some(d => {
41+
if (!ts.isCallExpression(d.expression) ||
42+
!ts.isIdentifier(d.expression.expression)) {
43+
return false;
44+
}
45+
46+
const moduleImport = this.getModuleImportOfIdentifier(d.expression.expression);
47+
return moduleImport ? moduleImport.startsWith('@angular/core') : false;
48+
});
49+
}
50+
51+
/** Gets the module import of the given identifier if imported. */
52+
getModuleImportOfIdentifier(node: ts.Identifier): string|null {
53+
const symbol = this._typeChecker.getSymbolAtLocation(node);
54+
if (!symbol || !symbol.declarations.length) {
55+
return null;
56+
}
57+
const decl = symbol.declarations[0];
58+
if (!ts.isImportSpecifier(decl)) {
59+
return null;
60+
}
61+
const importDecl = decl.parent.parent.parent;
62+
if (!ts.isStringLiteral(importDecl.moduleSpecifier)) {
63+
return null;
64+
}
65+
return importDecl.moduleSpecifier.text;
66+
}
67+
}

tslint.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@
101101
"no-import-spacing": true,
102102
"no-private-getters": true,
103103
"no-undecorated-base-class-di": true,
104+
"no-undecorated-class-with-ng-fields": true,
104105
"setters-after-getters": true,
105106
"ng-on-changes-property-access": true,
106107
"rxjs-imports": true,

0 commit comments

Comments
 (0)