Skip to content

Commit 716b259

Browse files
crisbetommalerba
authored andcommitted
build: add lint rule to enforce coercion static properties for setters (#17536)
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 2f84389 commit 716b259

File tree

2 files changed

+217
-0
lines changed

2 files changed

+217
-0
lines changed
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
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+
if (this._shouldLintClass(node)) {
34+
this._lintClass(node, node);
35+
this._lintSuperClasses(node);
36+
this._lintInterfaces(node, node);
37+
}
38+
super.visitClassDeclaration(node);
39+
}
40+
41+
/**
42+
* Goes through the own setters of a class declaration and checks whether they use coercion.
43+
* @param node Class declaration to be checked.
44+
* @param sourceClass Class declaration on which to look for static properties that declare the
45+
* accepted values for the setter.
46+
*/
47+
private _lintClass(node: ts.ClassDeclaration, sourceClass: ts.ClassDeclaration): void {
48+
node.members.forEach(member => {
49+
if (ts.isSetAccessor(member) && usesCoercion(member, this._coercionFunctions) &&
50+
this._shouldCheckSetter(member)) {
51+
this._checkForStaticMember(sourceClass, member.name.getText());
52+
}
53+
});
54+
}
55+
56+
/**
57+
* Goes up the inheritance chain of a class declaration and
58+
* checks whether it has any setters using coercion.
59+
* @param node Class declaration to be checked.
60+
*/
61+
private _lintSuperClasses(node: ts.ClassDeclaration): void {
62+
let currentClass: ts.ClassDeclaration|null = node;
63+
64+
while (currentClass) {
65+
const baseType = getBaseTypeIdentifier(currentClass);
66+
67+
if (!baseType) {
68+
break;
69+
}
70+
71+
const symbol = this._typeChecker.getTypeAtLocation(baseType).getSymbol();
72+
currentClass = symbol && ts.isClassDeclaration(symbol.valueDeclaration) ?
73+
symbol.valueDeclaration : null;
74+
75+
if (currentClass) {
76+
this._lintClass(currentClass, node);
77+
this._lintInterfaces(currentClass, node);
78+
}
79+
}
80+
}
81+
82+
/**
83+
* Checks whether the interfaces that a class implements contain any known coerced properties.
84+
* @param node Class declaration to be checked.
85+
* @param sourceClass Class declaration on which to look for static properties that declare the
86+
* accepted values for the setter.
87+
*/
88+
private _lintInterfaces(node: ts.ClassDeclaration, sourceClass: ts.ClassDeclaration): void {
89+
if (!node.heritageClauses) {
90+
return;
91+
}
92+
93+
node.heritageClauses.forEach(clause => {
94+
if (clause.token === ts.SyntaxKind.ImplementsKeyword) {
95+
clause.types.forEach(clauseType => {
96+
if (ts.isIdentifier(clauseType.expression)) {
97+
const propNames = this._coercionInterfaces[clauseType.expression.text];
98+
99+
if (propNames) {
100+
propNames.forEach(propName => this._checkForStaticMember(sourceClass, propName));
101+
}
102+
}
103+
});
104+
}
105+
});
106+
}
107+
108+
/**
109+
* Checks whether a class declaration has a static member, corresponding
110+
* to the specified setter name, and logs a failure if it doesn't.
111+
* @param node
112+
* @param setterName
113+
*/
114+
private _checkForStaticMember(node: ts.ClassDeclaration, setterName: string) {
115+
const coercionPropertyName = `ngAcceptInputType_${setterName}`;
116+
const correspondingCoercionProperty = node.members.find(member => {
117+
return ts.isPropertyDeclaration(member) &&
118+
tsutils.hasModifier(member.modifiers, ts.SyntaxKind.StaticKeyword) &&
119+
member.name.getText() === coercionPropertyName;
120+
});
121+
122+
if (!correspondingCoercionProperty) {
123+
this.addFailureAtNode(node.name || node, `Class must declare static coercion ` +
124+
`property called ${coercionPropertyName}.`);
125+
}
126+
}
127+
128+
/** Checks whether this rule should lint a class declaration. */
129+
private _shouldLintClass(node: ts.ClassDeclaration): boolean {
130+
// We don't need to lint undecorated classes.
131+
if (!node.decorators) {
132+
return false;
133+
}
134+
135+
// If the class is a component we should lint.
136+
if (node.decorators.some(decorator => isDecoratorCalled(decorator, 'Component'))) {
137+
return true;
138+
}
139+
140+
const directiveDecorator =
141+
node.decorators.find(decorator => isDecoratorCalled(decorator, 'Directive'));
142+
143+
// We should lint non-abstract directives.
144+
if (directiveDecorator) {
145+
const args = (directiveDecorator.expression as ts.CallExpression).arguments;
146+
const directiveConfig = args.length && ts.isObjectLiteralExpression(args[0]) ?
147+
args[0] as ts.ObjectLiteralExpression : null;
148+
const selector = directiveConfig ? directiveConfig.properties.find(prop => {
149+
return prop.name && ts.isIdentifier(prop.name) && prop.name.text === 'selector';
150+
}) : null;
151+
152+
return !!selector && ts.isPropertyAssignment(selector) && ts.isIdentifier(selector.name) &&
153+
!selector.name.text.startsWith('do-not-use-abstract-');
154+
}
155+
156+
return false;
157+
}
158+
159+
/** Determines whether a setter node should be checked by the lint rule. */
160+
private _shouldCheckSetter(node: ts.SetAccessorDeclaration): boolean {
161+
const param = node.parameters[0];
162+
const types = this._typeChecker.typeToString(this._typeChecker.getTypeAtLocation(param))
163+
.split('|').map(name => name.trim());
164+
// We shouldn't check setters which accept `any` or a `string`.
165+
return types.every(typeName => typeName !== 'any' && typeName !== 'string');
166+
}
167+
}
168+
169+
/**
170+
* Checks whether a setter uses coercion.
171+
* @param setter Setter node that should be checked.
172+
* @param coercionFunctions Names of the coercion functions that we should be looking for.
173+
*/
174+
function usesCoercion(setter: ts.SetAccessorDeclaration, coercionFunctions: Set<string>): boolean {
175+
let coercionWasUsed = false;
176+
177+
setter.forEachChild(function walk(node: ts.Node) {
178+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) &&
179+
coercionFunctions.has(node.expression.text)) {
180+
coercionWasUsed = true;
181+
}
182+
183+
if (!coercionWasUsed) {
184+
node.forEachChild(walk);
185+
}
186+
});
187+
188+
return coercionWasUsed;
189+
}
190+
191+
/** Gets the identifier node of the base type that a class is extending. */
192+
function getBaseTypeIdentifier(node: ts.ClassDeclaration): ts.Identifier|null {
193+
if (node.heritageClauses) {
194+
for (let clause of node.heritageClauses) {
195+
if (clause.token === ts.SyntaxKind.ExtendsKeyword && clause.types.length &&
196+
ts.isIdentifier(clause.types[0].expression)) {
197+
return clause.types[0].expression;
198+
}
199+
}
200+
}
201+
202+
return null;
203+
}
204+
205+
/** Checks whether a node is a decorator with a particular name. */
206+
function isDecoratorCalled(node: ts.Decorator, name: string): boolean {
207+
return ts.isCallExpression(node.expression) &&
208+
ts.isIdentifier(node.expression.expression) &&
209+
node.expression.expression.text === name;
210+
}

tslint.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,13 @@
116116
"rxjs-imports": true,
117117
"require-breaking-change-version": true,
118118
"class-list-signatures": true,
119+
"coercion-types": [false, // Disabled until #17528 gets in.
120+
["coerceBooleanProperty", "coerceCssPixelValue", "coerceNumberProperty"],
121+
{
122+
"CanDisable": ["disabled"],
123+
"CanDisableRipple": ["disableRipple"]
124+
}
125+
],
119126
"no-host-decorator-in-concrete": [
120127
true,
121128
"HostBinding",

0 commit comments

Comments
 (0)