Skip to content

Commit

Permalink
build: add lint rule to ensure classes with angular features are deco…
Browse files Browse the repository at this point in the history
…rated

Read more about this here: https://hackmd.io/vuQfavzfRG6KUCtU7oK_EA.
  • Loading branch information
devversion committed Oct 3, 2019
1 parent 361d1cf commit 9f632cd
Show file tree
Hide file tree
Showing 2 changed files with 68 additions and 0 deletions.
67 changes: 67 additions & 0 deletions tools/tslint-rules/noUndecoratedClassWithNgFieldsRule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import * as Lint from 'tslint';
import * as ts from 'typescript';

const RULE_FAILURE = `Undecorated class defines fields with Angular decorators. In Ivy, such ` +
`undecorated classes with Angular fields cannot be extended since no definition is generated. ` +
`Add a "@Directive" decorator to fix this.`;

/**
* Rule that doesn't allow undecorated class declarations with fields using Angular
* decorators.
*/
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.hasAngularDecorator(node)) {
return;
}

for (let member of node.members) {
if (member.decorators && this.hasAngularDecorator(member)) {
this.addFailureAtNode(node, RULE_FAILURE);
return;
}
}
}

/** Checks if the specified node has an Angular decorator. */
hasAngularDecorator(node: ts.Node): boolean {
return !!node.decorators && node.decorators.some(d => {
if (!ts.isCallExpression(d.expression) ||
!ts.isIdentifier(d.expression.expression)) {
return false;
}

const moduleImport = this.getModuleImportOfIdentifier(d.expression.expression);
return moduleImport ? moduleImport.startsWith('@angular/core') : false;
});
}

/** Gets the module import of the given identifier if imported. */
getModuleImportOfIdentifier(node: ts.Identifier): string|null {
const symbol = this._typeChecker.getSymbolAtLocation(node);
if (!symbol || !symbol.declarations.length) {
return null;
}
const decl = symbol.declarations[0];
if (!ts.isImportSpecifier(decl)) {
return null;
}
const importDecl = decl.parent.parent.parent;
if (!ts.isStringLiteral(importDecl.moduleSpecifier)) {
return null;
}
return importDecl.moduleSpecifier.text;
}
}
1 change: 1 addition & 0 deletions tslint.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@
"no-import-spacing": true,
"no-private-getters": true,
"no-undecorated-base-class-di": true,
"no-undecorated-class-with-ng-fields": true,
"setters-after-getters": true,
"ng-on-changes-property-access": true,
"rxjs-imports": true,
Expand Down

0 comments on commit 9f632cd

Please sign in to comment.