@@ -11,10 +11,16 @@ import * as minimatch from 'minimatch';
11
11
* ```
12
12
* "validate-decorators": [true, {
13
13
* "Component": {
14
- * "encapsulation": "\\.None$",
15
- * "!styles": ".*"
14
+ * "argument": 0,
15
+ * "properties": {
16
+ * "encapsulation": "\\.None$",
17
+ * "!styles": ".*"
18
+ * }
16
19
* },
17
- * "NgModule": "^(?!\\s*$).+"
20
+ * "NgModule": {
21
+ * "argument": 0,
22
+ * "properties": "^(?!\\s*$).+"
23
+ * }
18
24
* }, "src/material"]
19
25
* ```
20
26
*/
@@ -24,15 +30,32 @@ export class Rule extends Lint.Rules.AbstractRule {
24
30
}
25
31
}
26
32
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
+
27
48
/** Represents a set of required and forbidden decorator properties. */
28
49
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 } ,
31
54
} ;
32
55
33
56
/** Represents a map between decorator names and rule sets. */
34
57
type DecoratorRules = {
35
- [ decorator : string ] : DecoratorRuleSet | RegExp
58
+ [ decorator : string ] : DecoratorRuleSet
36
59
} ;
37
60
38
61
class Walker extends Lint . RuleWalker {
@@ -57,8 +80,16 @@ class Walker extends Lint.RuleWalker {
57
80
}
58
81
59
82
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
+ } ) ;
62
93
}
63
94
64
95
super . visitClassDeclaration ( node ) ;
@@ -68,63 +99,76 @@ class Walker extends Lint.RuleWalker {
68
99
* Validates that a decorator matches all of the defined rules.
69
100
* @param decorator Decorator to be checked.
70
101
*/
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
+
72
109
// 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 ;
74
112
75
113
// Don't do anything if there are no rules.
76
114
if ( ! rules ) {
77
115
return ;
78
116
}
79
117
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 ] ;
92
119
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
+ }
93
127
return ;
94
128
}
95
129
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
+ }
97
137
98
- if ( ! args || ! args . length || ! args [ 0 ] . properties ) {
138
+ if ( ! ts . isObjectLiteralExpression ( args [ rules . argument ] ) ) {
99
139
return ;
100
140
}
101
141
102
142
// 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
+ } ) ;
110
154
111
155
// 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 ) ) ;
114
158
115
159
if ( missing . length ) {
116
160
// Exit early if any of the properties are missing.
117
- this . addFailureAtNode ( decorator . expression ,
161
+ this . addFailureAtNode ( expression . expression ,
118
162
'Missing required properties: ' + missing . join ( ', ' ) ) ;
119
163
} else {
120
164
// If all the necessary properties are defined, ensure that
121
165
// they match the pattern and aren't in the forbidden list.
122
166
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 => {
125
169
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 ] ;
128
172
129
173
if ( requiredPattern && ! requiredPattern . test ( value ) ) {
130
174
this . addFailureAtNode ( node , `Invalid value for property. ` +
@@ -143,33 +187,45 @@ class Walker extends Lint.RuleWalker {
143
187
* @param config Config object passed in via the tslint.json.
144
188
* @returns Sanitized rules.
145
189
*/
146
- private _generateRules ( config : { [ key : string ] : string | { [ key : string ] : string } } ) : DecoratorRules {
190
+ private _generateRules ( config : RuleConfig | null ) : DecoratorRules {
147
191
const output : DecoratorRules = { } ;
148
192
149
193
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
+ } ) ;
173
229
}
174
230
175
231
return output ;
0 commit comments