Skip to content

Commit 24d0781

Browse files
committed
build: add lint rule to enforce coercion static properties for setters
Adds a custom tslint rule to enforce that properties which use coercion in a setter also declare a static property to indicate the accepted types to ngtsc. Also handles inherited setters and properties coming from an interface being implemented (necessary to support mixins). Relates to #17528.
1 parent 3fdab10 commit 24d0781

File tree

2 files changed

+164
-0
lines changed

2 files changed

+164
-0
lines changed
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import * as ts from 'typescript';
2+
import * as Lint from 'tslint';
3+
import * as tsutils from 'tsutils';
4+
5+
/**
6+
* TSLint rule that verifies that classes declare corresponding `ngAcceptInputType_*`
7+
* static fields for inputs that use coercion inside of their setters. Also handles
8+
* inherited class members and members that come from an interface.
9+
*/
10+
export class Rule extends Lint.Rules.TypedRule {
11+
applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] {
12+
const walker = new Walker(sourceFile, this.getOptions(), program.getTypeChecker());
13+
return this.applyWithWalker(walker);
14+
}
15+
}
16+
17+
class Walker extends Lint.RuleWalker {
18+
/** Names of the coercion functions that we should be looking for. */
19+
private _coercionFunctions: Set<string>;
20+
21+
/** Mapping of interfaces known to have coercion properties and the property names themselves. */
22+
private _coercionInterfaces: {[interfaceName: string]: string[]};
23+
24+
constructor(sourceFile: ts.SourceFile,
25+
options: Lint.IOptions,
26+
private _typeChecker: ts.TypeChecker) {
27+
super(sourceFile, options);
28+
this._coercionFunctions = new Set(options.ruleArguments[0] || []);
29+
this._coercionInterfaces = options.ruleArguments[1] || {};
30+
}
31+
32+
visitClassDeclaration(node: ts.ClassDeclaration) {
33+
this._lintClass(node, node);
34+
this._lintInheritedProperties(node);
35+
this._lintInterfaces(node);
36+
super.visitClassDeclaration(node);
37+
}
38+
39+
/**
40+
* Goes thrown the own setters of a class declaration and checks whether they use coercion.
41+
* @param node Class declaration to be checked.
42+
* @param sourceClass Class declaration on which to look for static properties that declare the
43+
* accepted values for the setter.
44+
*/
45+
private _lintClass(node: ts.ClassDeclaration, sourceClass: ts.ClassDeclaration): void {
46+
node.members.forEach(member => {
47+
if (ts.isSetAccessor(member) && usesCoercion(member, this._coercionFunctions)) {
48+
this._checkForStaticMember(sourceClass, member.name.getText());
49+
}
50+
});
51+
}
52+
53+
/**
54+
* Goes up the inheritance chain of a class declaration and
55+
* checks whether it has any setters using coercion.
56+
* @param node Class declaration to be checked.
57+
*/
58+
private _lintInheritedProperties(node: ts.ClassDeclaration): void {
59+
let currentClass: ts.ClassDeclaration|null = node;
60+
61+
while (currentClass) {
62+
const baseType = getBaseTypeIdentifier(currentClass);
63+
64+
if (!baseType) {
65+
break;
66+
}
67+
68+
const symbol = this._typeChecker.getTypeAtLocation(baseType).getSymbol();
69+
currentClass = symbol && ts.isClassDeclaration(symbol.valueDeclaration) ?
70+
symbol.valueDeclaration : null;
71+
72+
if (currentClass) {
73+
this._lintClass(currentClass, node);
74+
}
75+
}
76+
}
77+
78+
/**
79+
* Checks whether the interfaces that a class implements contain any known coerced properties.
80+
* @param node Class declaration to be checked.
81+
*/
82+
private _lintInterfaces(node: ts.ClassDeclaration): void {
83+
if (!node.heritageClauses) {
84+
return;
85+
}
86+
87+
node.heritageClauses.forEach(clause => {
88+
if (clause.token === ts.SyntaxKind.ImplementsKeyword) {
89+
clause.types.forEach(clauseType => {
90+
if (ts.isIdentifier(clauseType.expression)) {
91+
const propNames = this._coercionInterfaces[clauseType.expression.text];
92+
93+
if (propNames) {
94+
propNames.forEach(propName => this._checkForStaticMember(node, propName));
95+
}
96+
}
97+
});
98+
}
99+
});
100+
}
101+
102+
/**
103+
* Checks whether a class declaration has a static member, corresponding
104+
* to the specified setter name, and logs a failure if it doesn't.
105+
* @param node
106+
* @param setterName
107+
*/
108+
private _checkForStaticMember(node: ts.ClassDeclaration, setterName: string) {
109+
const coercionPropertyName = `ngAcceptInputType_${setterName}`;
110+
const correspondingCoercionProperty = node.members.find(member => {
111+
return ts.isPropertyDeclaration(member) &&
112+
tsutils.hasModifier(member.modifiers, ts.SyntaxKind.StaticKeyword) &&
113+
member.name.getText() === coercionPropertyName;
114+
});
115+
116+
if (!correspondingCoercionProperty) {
117+
this.addFailureAtNode(node.name || node, `Class must declare static coercion ` +
118+
`property called ${coercionPropertyName}.`);
119+
}
120+
}
121+
}
122+
123+
/**
124+
* Checks whether a setter uses coercion.
125+
* @param setter Setter node that should be checked.
126+
* @param coercionFunctions Names of the coercion functions that we should be looking for.
127+
*/
128+
function usesCoercion(setter: ts.SetAccessorDeclaration, coercionFunctions: Set<string>): boolean {
129+
let coercionWasUsed = false;
130+
131+
setter.forEachChild(function walk(node: ts.Node) {
132+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) &&
133+
coercionFunctions.has(node.expression.text)) {
134+
coercionWasUsed = true;
135+
}
136+
137+
if (!coercionWasUsed) {
138+
node.forEachChild(walk);
139+
}
140+
});
141+
142+
return coercionWasUsed;
143+
}
144+
145+
/** Gets the identifier node of the base type that a class is extending. */
146+
function getBaseTypeIdentifier(node: ts.ClassDeclaration): ts.Identifier|null {
147+
if (node.heritageClauses) {
148+
for (let clause of node.heritageClauses) {
149+
if (clause.token === ts.SyntaxKind.ExtendsKeyword && clause.types.length &&
150+
ts.isIdentifier(clause.types[0].expression)) {
151+
return clause.types[0].expression;
152+
}
153+
}
154+
}
155+
156+
return null;
157+
}

tslint.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,13 @@
109109
"rxjs-imports": true,
110110
"require-breaking-change-version": true,
111111
"class-list-signatures": true,
112+
"coercion-types": [false, // Disabled until #17528 gets in.
113+
["coerceBooleanProperty", "coerceCssPixelValue", "coerceNumberProperty"],
114+
{
115+
"CanDisable": ["disabled"],
116+
"CanDisableRipple": ["disableRipple"]
117+
}
118+
],
112119
"no-host-decorator-in-concrete": [
113120
true,
114121
"HostBinding",

0 commit comments

Comments
 (0)