Skip to content

Commit cff8115

Browse files
committed
build: expand decorators validation rule to cover class members
Expands the lint rule that we use to validate class decorators to also apply to class members. We need this in order to support linting things like `descendants` on `ContentChildren`. These changes switch around the config data structure to make it more flexible, fix some cases where we were using `any` and add some extra properties to the config.
1 parent a5cad10 commit cff8115

File tree

2 files changed

+137
-70
lines changed

2 files changed

+137
-70
lines changed

tools/tslint-rules/validateDecoratorsRule.ts

Lines changed: 118 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,16 @@ import * as minimatch from 'minimatch';
1111
* ```
1212
* "validate-decorators": [true, {
1313
* "Component": {
14-
* "encapsulation": "\\.None$",
15-
* "!styles": ".*"
14+
* "argument": 0,
15+
* "properties": {
16+
* "encapsulation": "\\.None$",
17+
* "!styles": ".*"
18+
* }
1619
* },
17-
* "NgModule": "^(?!\\s*$).+"
20+
* "NgModule": {
21+
* "argument": 0,
22+
* "properties": "^(?!\\s*$).+"
23+
* }
1824
* }, "src/material"]
1925
* ```
2026
*/
@@ -24,15 +30,32 @@ export class Rule extends Lint.Rules.AbstractRule {
2430
}
2531
}
2632

33+
/**
34+
* Token used to indicate that all properties of an object
35+
* should be linted against a single pattern.
36+
*/
37+
const ALL_PROPS_TOKEN = '*';
38+
39+
/** Object that can be used to configured the rule. */
40+
interface RuleConfig {
41+
[key: string]: {
42+
argument: number,
43+
required?: boolean,
44+
properties: {[key: string]: string}
45+
};
46+
}
47+
2748
/** Represents a set of required and forbidden decorator properties. */
2849
type DecoratorRuleSet = {
29-
required: {[key: string]: RegExp},
30-
forbidden: {[key: string]: RegExp},
50+
argument: number,
51+
required: boolean,
52+
requiredProps: {[key: string]: RegExp},
53+
forbiddenProps: {[key: string]: RegExp},
3154
};
3255

3356
/** Represents a map between decorator names and rule sets. */
3457
type DecoratorRules = {
35-
[decorator: string]: DecoratorRuleSet | RegExp
58+
[decorator: string]: DecoratorRuleSet
3659
};
3760

3861
class Walker extends Lint.RuleWalker {
@@ -57,8 +80,16 @@ class Walker extends Lint.RuleWalker {
5780
}
5881

5982
visitClassDeclaration(node: ts.ClassDeclaration) {
60-
if (this._enabled && node.decorators) {
61-
node.decorators.forEach(decorator => this._validatedDecorator(decorator.expression));
83+
if (this._enabled) {
84+
if (node.decorators) {
85+
node.decorators.forEach(decorator => this._validateDecorator(decorator));
86+
}
87+
88+
node.members.forEach(member => {
89+
if (member.decorators) {
90+
member.decorators.forEach(decorator => this._validateDecorator(decorator));
91+
}
92+
});
6293
}
6394

6495
super.visitClassDeclaration(node);
@@ -68,63 +99,76 @@ class Walker extends Lint.RuleWalker {
6899
* Validates that a decorator matches all of the defined rules.
69100
* @param decorator Decorator to be checked.
70101
*/
71-
private _validatedDecorator(decorator: any) {
102+
private _validateDecorator(decorator: ts.Decorator) {
103+
const expression = decorator.expression;
104+
105+
if (!expression || !ts.isCallExpression(expression)) {
106+
return;
107+
}
108+
72109
// Get the rules that are relevant for the current decorator.
73-
const rules = this._rules[decorator.expression.getText()];
110+
const rules = this._rules[expression.expression.getText()];
111+
const args = expression.arguments;
74112

75113
// Don't do anything if there are no rules.
76114
if (!rules) {
77115
return;
78116
}
79117

80-
// If the rule is a regex, extract the arguments as a string and run it against them.
81-
if (rules instanceof RegExp) {
82-
const decoratorText: string = decorator.getText();
83-
const openParenthesisIndex = decoratorText.indexOf('(');
84-
const closeParenthesisIndex = decoratorText.lastIndexOf(')');
85-
const argumentsText = openParenthesisIndex > -1 ? decoratorText.substring(
86-
openParenthesisIndex + 1,
87-
closeParenthesisIndex > -1 ? closeParenthesisIndex : decoratorText.length) : '';
88-
89-
if (!rules.test(argumentsText)) {
90-
this.addFailureAtNode(decorator.parent, `Expected decorator arguments to match "${rules}"`);
91-
}
118+
const allPropsRequirement = rules.requiredProps[ALL_PROPS_TOKEN];
92119

120+
// If we have a rule that applies to all properties, we just run it through once and we exit.
121+
if (allPropsRequirement) {
122+
const argumentText = args[rules.argument] ? args[rules.argument].getText() : '';
123+
if (!allPropsRequirement.test(argumentText)) {
124+
this.addFailureAtNode(expression.parent, `Expected decorator argument ${rules.argument} ` +
125+
`to match "${allPropsRequirement}"`);
126+
}
93127
return;
94128
}
95129

96-
const args = decorator.arguments;
130+
if (!args[rules.argument]) {
131+
if (rules.required) {
132+
this.addFailureAtNode(expression.parent,
133+
`Missing required argument at index ${rules.argument}`);
134+
}
135+
return;
136+
}
97137

98-
if (!args || !args.length || !args[0].properties) {
138+
if (!ts.isObjectLiteralExpression(args[rules.argument])) {
99139
return;
100140
}
101141

102142
// Extract the property names and values.
103-
const props = args[0].properties
104-
.filter((node: ts.PropertyAssignment) => node.name && node.initializer)
105-
.map((node: ts.PropertyAssignment) => ({
106-
name: node.name.getText(),
107-
value: node.initializer.getText(),
108-
node
109-
}));
143+
const props: {name: string, value: string, node: ts.PropertyAssignment}[] = [];
144+
145+
(args[rules.argument] as ts.ObjectLiteralExpression).properties.forEach(prop => {
146+
if (ts.isPropertyAssignment(prop) && prop.name && prop.initializer) {
147+
props.push({
148+
name: prop.name.getText(),
149+
value: prop.initializer.getText(),
150+
node: prop
151+
});
152+
}
153+
});
110154

111155
// Find all of the required rule properties that are missing from the decorator.
112-
const missing = Object.keys(rules.required)
113-
.filter(key => !props.find((prop: any) => prop.name === key));
156+
const missing = Object.keys(rules.requiredProps)
157+
.filter(key => !props.find(prop => prop.name === key));
114158

115159
if (missing.length) {
116160
// Exit early if any of the properties are missing.
117-
this.addFailureAtNode(decorator.expression,
161+
this.addFailureAtNode(expression.expression,
118162
'Missing required properties: ' + missing.join(', '));
119163
} else {
120164
// If all the necessary properties are defined, ensure that
121165
// they match the pattern and aren't in the forbidden list.
122166
props
123-
.filter((prop: any) => rules.required[prop.name] || rules.forbidden[prop.name])
124-
.forEach((prop: any) => {
167+
.filter(prop => rules.requiredProps[prop.name] || rules.forbiddenProps[prop.name])
168+
.forEach(prop => {
125169
const {name, value, node} = prop;
126-
const requiredPattern = rules.required[name];
127-
const forbiddenPattern = rules.forbidden[name];
170+
const requiredPattern = rules.requiredProps[name];
171+
const forbiddenPattern = rules.forbiddenProps[name];
128172

129173
if (requiredPattern && !requiredPattern.test(value)) {
130174
this.addFailureAtNode(node, `Invalid value for property. ` +
@@ -143,33 +187,45 @@ class Walker extends Lint.RuleWalker {
143187
* @param config Config object passed in via the tslint.json.
144188
* @returns Sanitized rules.
145189
*/
146-
private _generateRules(config: {[key: string]: string|{[key: string]: string}}): DecoratorRules {
190+
private _generateRules(config: RuleConfig|null): DecoratorRules {
147191
const output: DecoratorRules = {};
148192

149193
if (config) {
150-
Object.keys(config)
151-
.filter(decoratorName => Object.keys(config[decoratorName]).length > 0)
152-
.forEach(decoratorName => {
153-
const decoratorConfig = config[decoratorName];
154-
155-
if (typeof decoratorConfig === 'string') {
156-
output[decoratorName] = new RegExp(decoratorConfig);
157-
} else {
158-
output[decoratorName] = Object.keys(decoratorConfig).reduce((rules, prop) => {
159-
const isForbidden = prop.startsWith('!');
160-
const cleanName = isForbidden ? prop.slice(1) : prop;
161-
const pattern = new RegExp(decoratorConfig[prop]);
162-
163-
if (isForbidden) {
164-
rules.forbidden[cleanName] = pattern;
165-
} else {
166-
rules.required[cleanName] = pattern;
167-
}
168-
169-
return rules;
170-
}, {required: {}, forbidden: {}} as DecoratorRuleSet);
171-
}
172-
});
194+
Object.keys(config).forEach(decoratorName => {
195+
const decoratorConfig = config[decoratorName];
196+
const {argument, properties, required} = decoratorConfig;
197+
198+
// * is a special token which means to run the pattern across the entire object.
199+
const allProperties = properties[ALL_PROPS_TOKEN];
200+
201+
if (allProperties) {
202+
output[decoratorName] = {
203+
argument,
204+
required: !!required,
205+
requiredProps: {[ALL_PROPS_TOKEN]: new RegExp(allProperties)},
206+
forbiddenProps: {}
207+
};
208+
} else {
209+
output[decoratorName] = Object.keys(decoratorConfig.properties).reduce((rules, prop) => {
210+
const isForbidden = prop.startsWith('!');
211+
const cleanName = isForbidden ? prop.slice(1) : prop;
212+
const pattern = new RegExp(properties[prop]);
213+
214+
if (isForbidden) {
215+
rules.forbiddenProps[cleanName] = pattern;
216+
} else {
217+
rules.requiredProps[cleanName] = pattern;
218+
}
219+
220+
return rules;
221+
}, {
222+
argument,
223+
required: !!required,
224+
requiredProps: {} as {[key: string]: RegExp},
225+
forbiddenProps: {} as {[key: string]: RegExp}
226+
});
227+
}
228+
});
173229
}
174230

175231
return output;

tslint.json

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -133,17 +133,28 @@
133133
}],
134134
"validate-decorators": [true, {
135135
"Component": {
136-
"!host": "\\[class\\]",
137-
"!preserveWhitespaces": ".*",
138-
"!styles": ".*",
139-
"!moduleId": ".*",
140-
"changeDetection": "\\.OnPush$",
141-
"encapsulation": "\\.None$"
136+
"argument": 0,
137+
"properties": {
138+
"!host": "\\[class\\]",
139+
"!preserveWhitespaces": ".*",
140+
"!styles": ".*",
141+
"!moduleId": ".*",
142+
"changeDetection": "\\.OnPush$",
143+
"encapsulation": "\\.None$"
144+
}
142145
},
143146
"Directive": {
144-
"!host": "\\[class\\]"
147+
"argument": 0,
148+
"properties": {
149+
"!host": "\\[class\\]"
150+
}
145151
},
146-
"NgModule": "^(?!\\s*$).+"
152+
"NgModule": {
153+
"argument": 0,
154+
"properties": {
155+
"*": "^(?!\\s*$).+"
156+
}
157+
}
147158
}, "src/!(a11y-demo|e2e-app|components-examples|universal-app|dev-app)/**/!(*.spec).ts"],
148159
"require-license-banner": [
149160
true,

0 commit comments

Comments
 (0)