@@ -7,14 +7,18 @@ const pluginName = 'check-prop-types';
7
7
8
8
const reactClassComponentExtends = [ 'Component' , 'PureComponent' ] ;
9
9
10
- const importIdentifier = '_checkPropTypes' ;
11
- const arrowPropertiesIdentifier = '_props' ;
10
+ const importIdentifierName = '_checkPropTypes' ;
11
+ const importSourceName = 'prop-types/checkPropTypes' ;
12
+ const arrowPropertiesIdentifierName = '_props' ;
12
13
13
14
// implementation
14
15
15
16
export default ( { types } ) => {
16
17
let fileName ;
17
- let programBody ;
18
+
19
+ let importIdentifierNode ;
20
+ let importExists = false ;
21
+ let importUsed = false ;
18
22
19
23
// options
20
24
@@ -33,93 +37,105 @@ export default ({ types }) => {
33
37
const getSourceFile = ( ) => fileName && fileName !== 'unknown' ? ` "${ fileName } " file` : '' ;
34
38
const getSource = ( { loc : { start : { line, column } } } ) => `at${ getSourceFile ( ) } ${ line } line ${ column } column` ;
35
39
36
- const warnClass = ( { identifier } , superClassName , superClassObject ) => warn ( optionLogIgnoredClassComponentExtends ,
37
- `Ignored propTypes ${ getSource ( identifier ) } for class "${ identifier . name } " with "${ superClassObject ? ` ${ superClassObject } .` : '' } ${ superClassName } " super class` ) ;
40
+ const warnClass = ( { identifier } , superClassName ) => warn ( optionLogIgnoredClassComponentExtends ,
41
+ `Ignored propTypes ${ getSource ( identifier ) } for class "${ identifier . name } " with "${ superClassName } " super class` ) ;
38
42
39
43
const warnBinding = ( bindingType , { identifier } , type ) => warn ( optionLogIgnoredBinding ,
40
44
`Ignored propTypes ${ getSource ( identifier ) } for ${ bindingType } "${ identifier . name } " with "${ type } " type` ) ;
41
45
42
- // updaters
46
+ // getters
43
47
44
- const updateImport = ( binding ) => {
45
- if ( binding . path . scope . hasBinding ( importIdentifier ) ) return ;
48
+ const getImportIdentifier = ( path ) => {
49
+ const importDeclaration = path . node . body . find ( item => item . type === 'ImportDeclaration'
50
+ && item . source . value === importSourceName
51
+ && item . specifiers . some ( specifier => specifier . type === 'ImportDefaultSpecifier' ) ) ;
46
52
47
- if ( programBody . some ( item => item . type === 'ImportDeclaration'
48
- && item . specifiers . find ( specifier => specifier . local . name === importIdentifier ) ) ) return ;
53
+ if ( importDeclaration ) importExists = true ;
49
54
50
- const importDeclaration = types . importDeclaration (
51
- [ types . importDefaultSpecifier ( types . identifier ( importIdentifier ) ) ] ,
52
- types . stringLiteral ( 'prop-types/checkPropTypes' ) ,
53
- ) ;
54
- programBody . unshift ( importDeclaration ) ;
55
+ importIdentifierNode = importDeclaration
56
+ ? importDeclaration . specifiers [ 0 ] . local
57
+ : path . scope . generateUidIdentifier ( importIdentifierName ) ;
55
58
} ;
56
59
57
- const getMethodArgument = ( methodNode ) => { // eslint-disable-line unicorn/consistent-function-scoping
60
+ const getMethodArgumentIdentifier = ( methodNode ) => {
58
61
const [ firstArgument ] = methodNode . params ;
59
62
60
63
if ( firstArgument ) {
61
- if ( firstArgument . type === 'Identifier' ) return firstArgument . name ;
64
+ if ( firstArgument . type === 'Identifier' ) return firstArgument ;
65
+
62
66
if ( firstArgument . type === 'AssignmentPattern'
63
- && firstArgument . left . type === 'Identifier' ) return firstArgument . left . name ;
67
+ && firstArgument . left . type === 'Identifier' ) return firstArgument . left ;
64
68
}
65
69
66
- return arrowPropertiesIdentifier ;
70
+ return types . identifier ( arrowPropertiesIdentifierName ) ;
67
71
} ;
68
72
69
- const updateMethodArgument = ( methodNode ) => {
70
- const [ firstArgument ] = methodNode . params ;
71
-
72
- if ( ! firstArgument ) {
73
- methodNode . params . push ( types . identifier ( arrowPropertiesIdentifier ) ) ;
74
- }
75
-
76
- else if ( firstArgument . type === 'ObjectPattern' ) {
77
- methodNode . body . body . unshift ( types . variableDeclaration ( 'const' , [
78
- types . variableDeclarator (
79
- firstArgument ,
80
- types . identifier ( arrowPropertiesIdentifier ) ,
81
- ) ,
82
- ] ) ) ;
73
+ // updaters
83
74
84
- methodNode . params [ 0 ] = types . identifier ( arrowPropertiesIdentifier ) ;
85
- }
75
+ const updateImports = ( path ) => {
76
+ if ( importExists || ! importUsed ) return ;
86
77
87
- else if ( firstArgument . type === 'AssignmentPattern' && firstArgument . left . type === 'ObjectPattern' ) {
88
- methodNode . body . body . unshift ( types . variableDeclaration ( 'const' , [
89
- types . variableDeclarator (
90
- firstArgument . left ,
91
- types . identifier ( arrowPropertiesIdentifier ) ,
92
- ) ,
93
- ] ) ) ;
78
+ const importDeclaration = types . importDeclaration (
79
+ [ types . importDefaultSpecifier ( importIdentifierNode ) ] ,
80
+ types . stringLiteral ( importSourceName ) ,
81
+ ) ;
82
+ path . node . body . unshift ( importDeclaration ) ;
83
+ } ;
94
84
95
- firstArgument . left = types . identifier ( arrowPropertiesIdentifier ) ;
96
- }
85
+ const updateMethodBodyWithVariableDeclaration = ( methodNode , identifier ) => {
86
+ methodNode . body . body . unshift ( types . variableDeclaration ( 'const' , [
87
+ types . variableDeclarator (
88
+ identifier ,
89
+ types . identifier ( arrowPropertiesIdentifierName ) ,
90
+ ) ,
91
+ ] ) ) ;
97
92
} ;
98
93
99
- const updateMethodBody = ( binding , methodNode , statements ) => {
94
+ const updateMethodBodyWithValidationExpression = ( binding , methodNode , statements ) => {
100
95
const [ firstNode ] = methodNode . body . body ;
101
96
if ( firstNode && firstNode . type === 'ExpressionStatement'
102
- && firstNode . expression . callee . name === importIdentifier ) return ;
97
+ && firstNode . expression . callee . name === importIdentifierNode . name ) return ;
103
98
104
99
const expression = types . expressionStatement ( types . callExpression (
105
- types . identifier ( importIdentifier ) , statements ,
100
+ importIdentifierNode , [
101
+ ...statements ,
102
+ types . stringLiteral ( 'prop' ) ,
103
+ types . logicalExpression ( '||' ,
104
+ types . identifier ( `${ binding . identifier . name } .displayName` ) ,
105
+ types . stringLiteral ( binding . identifier . name ) ,
106
+ ) ,
107
+ ] ,
106
108
) ) ;
107
109
methodNode . body . body . unshift ( expression ) ;
108
110
109
- updateImport ( binding ) ;
111
+ importUsed = true ;
112
+ } ;
113
+
114
+ const updateMethodArgument = ( methodNode ) => {
115
+ const [ firstArgument ] = methodNode . params ;
116
+
117
+ const arrowPropertiesIdentifier = types . identifier ( arrowPropertiesIdentifierName ) ;
118
+ if ( ! firstArgument ) {
119
+ methodNode . params . push ( arrowPropertiesIdentifier ) ;
120
+ }
121
+
122
+ else if ( firstArgument . type === 'ObjectPattern' ) {
123
+ updateMethodBodyWithVariableDeclaration ( methodNode , firstArgument ) ;
124
+ methodNode . params [ 0 ] = arrowPropertiesIdentifier ;
125
+ }
126
+
127
+ else if ( firstArgument . type === 'AssignmentPattern' && firstArgument . left . type === 'ObjectPattern' ) {
128
+ updateMethodBodyWithVariableDeclaration ( methodNode , firstArgument . left ) ;
129
+ firstArgument . left = arrowPropertiesIdentifier ;
130
+ }
110
131
} ;
111
132
112
133
// visitors
113
134
114
135
const visitFunctionDeclaration = ( binding , functionNode ) => {
115
- updateMethodBody ( binding , functionNode , [
136
+ updateMethodBodyWithValidationExpression ( binding , functionNode , [
116
137
types . identifier ( `${ binding . identifier . name } .propTypes` ) ,
117
138
types . identifier ( 'arguments[0]' ) ,
118
- types . stringLiteral ( 'prop' ) ,
119
- types . logicalExpression ( '||' ,
120
- types . identifier ( `${ binding . identifier . name } .displayName` ) ,
121
- types . stringLiteral ( binding . identifier . name ) ,
122
- ) ,
123
139
] ) ;
124
140
} ;
125
141
@@ -129,97 +145,65 @@ export default ({ types }) => {
129
145
130
146
if ( currentSuperClass . type === 'AssignmentExpression' ) currentSuperClass = currentSuperClass . right ;
131
147
132
- if ( currentSuperClass . name ) {
133
- if ( ! optionClassComponentExtends . includes ( currentSuperClass . name ) ) {
134
- warnClass ( binding , currentSuperClass . name ) ;
148
+ const { name, object, property } = currentSuperClass ;
149
+ if ( name ) {
150
+ if ( ! optionClassComponentExtends . includes ( name ) ) {
151
+ warnClass ( binding , name ) ;
135
152
return ;
136
153
}
137
154
}
138
155
139
- else {
140
- const { object, property } = currentSuperClass ;
141
-
142
- if ( object . name === 'React' ) {
143
- if ( ! reactClassComponentExtends . includes ( property . name ) ) {
144
- warnClass ( binding , property . name , object . name ) ;
145
- return ;
146
- }
147
- }
148
-
149
- else if ( optionClassComponentExtendsObject . includes ( object . name ) ) {
150
- if ( ! optionClassComponentExtends . includes ( property . name ) ) {
151
- warnClass ( binding , property . name , object . name ) ;
152
- return ;
153
- }
154
- }
155
-
156
- else {
157
- warnClass ( binding , property . name , object . name ) ;
158
- return ;
159
- }
156
+ else if ( object . name === 'React'
157
+ ? ! reactClassComponentExtends . includes ( property . name )
158
+ : ( optionClassComponentExtendsObject . includes ( object . name )
159
+ ? ! optionClassComponentExtends . includes ( property . name )
160
+ : true
161
+ ) ) {
162
+ warnClass ( binding , `${ property . name } .${ object . name } ` ) ;
163
+ return ;
160
164
}
161
165
162
- // ignore class if render is not regular method
163
166
const renderNode = classNode . body . body . find ( item => item . kind === 'method'
164
167
&& item . key . name === 'render' && ! item . static ) ;
165
168
if ( ! renderNode ) return ;
166
169
167
- // update class render method with validation call
168
- updateMethodBody ( binding , renderNode , [
169
- // always use final propTypes even for super class render
170
+ updateMethodBodyWithValidationExpression ( binding , renderNode , [
170
171
types . identifier ( 'this.constructor.propTypes' ) ,
171
172
types . identifier ( 'this.props' ) ,
172
- types . stringLiteral ( 'prop' ) ,
173
- types . logicalExpression ( '||' ,
174
- types . identifier ( `${ binding . identifier . name } .displayName` ) ,
175
- types . stringLiteral ( binding . identifier . name ) ,
176
- ) ,
177
173
] ) ;
178
174
} ;
179
175
180
176
const visitVariableDeclaration = ( binding ) => {
181
177
const declarationNode = binding . path . node . init ;
182
178
183
- // follow function visitor logic for function assignment
184
179
if ( declarationNode . type === 'FunctionExpression' ) {
185
180
visitFunctionDeclaration ( binding , declarationNode ) ;
186
181
return ;
187
182
}
188
183
189
- // follow class visitor logic for function assignment
190
184
if ( declarationNode . type === 'ClassExpression' ) {
191
185
visitClassDeclaration ( binding , declarationNode ) ;
192
186
return ;
193
187
}
194
188
195
- // ignore non arrow function assignment
196
189
if ( declarationNode . type !== 'ArrowFunctionExpression' ) {
197
190
warnBinding ( 'assignment' , binding , declarationNode . type ) ;
198
191
return ;
199
192
}
200
193
201
- // update parenthesis arrow function with block statement
202
194
if ( declarationNode . body . type !== 'BlockStatement' ) {
203
195
declarationNode . body = types . blockStatement ( [
204
196
types . returnStatement ( declarationNode . body ) ,
205
197
] ) ;
206
198
}
207
199
208
- // get arrow function argument name
209
- const methodArgument = getMethodArgument ( declarationNode ) ;
200
+ const methodArgumentIdentifier = getMethodArgumentIdentifier ( declarationNode ) ;
210
201
211
- // update arrow function with validation call
212
- updateMethodBody ( binding , declarationNode , [
202
+ updateMethodBodyWithValidationExpression ( binding , declarationNode , [
213
203
types . identifier ( `${ binding . identifier . name } .propTypes` ) ,
214
- types . identifier ( methodArgument ) ,
215
- types . stringLiteral ( 'prop' ) ,
216
- types . logicalExpression ( '||' ,
217
- types . identifier ( `${ binding . identifier . name } .displayName` ) ,
218
- types . stringLiteral ( binding . identifier . name ) ,
219
- ) ,
204
+ methodArgumentIdentifier ,
220
205
] ) ;
221
206
222
- // update arrow function argument if needed
223
207
updateMethodArgument ( declarationNode ) ;
224
208
} ;
225
209
@@ -250,60 +234,61 @@ export default ({ types }) => {
250
234
251
235
const visitAssignmentExpression = ( path ) => {
252
236
const { left } = path . node ;
253
-
254
- // ignore propTypes assignment without property name
255
237
if ( ! left . property || left . property . name !== 'propTypes' ) return ;
256
238
257
- // find propTypes binding
258
239
const binding = path . scope . getBinding ( left . object . name ) ;
259
240
if ( ! binding ) return ;
260
241
261
242
visitBinding ( binding ) ;
262
243
} ;
263
244
264
- // plugin api
245
+ // options
265
246
266
- return {
267
- name : pluginName ,
247
+ const parseOptions = ( {
248
+ classComponentExtendsObject,
249
+ classComponentExtends,
268
250
269
- visitor : {
251
+ logIgnoredBinding,
252
+ logIgnoredClassComponentExtends,
270
253
271
- AssignmentExpression : visitAssignmentExpression ,
254
+ ...unknownOptions
255
+ } ) => {
256
+ if ( classComponentExtendsObject !== undefined ) {
257
+ if ( Array . isArray ( classComponentExtendsObject ) ) optionClassComponentExtendsObject . push ( ...classComponentExtendsObject ) ;
258
+ else warnOptions ( { classComponentExtendsObject } ) ;
259
+ }
272
260
273
- Program : {
274
- enter ( path , { file , opts : {
275
- classComponentExtendsObject ,
276
- classComponentExtends ,
261
+ if ( classComponentExtends !== undefined ) {
262
+ if ( Array . isArray ( classComponentExtends ) ) optionClassComponentExtends . push ( ... classComponentExtends ) ;
263
+ else warnOptions ( { classComponentExtends } ) ;
264
+ }
277
265
278
- logIgnoredBinding,
279
- logIgnoredClassComponentExtends,
266
+ if ( logIgnoredBinding !== undefined ) optionLogIgnoredBinding = Boolean ( logIgnoredBinding ) ;
267
+ if ( logIgnoredClassComponentExtends !== undefined ) optionLogIgnoredClassComponentExtends = Boolean ( logIgnoredClassComponentExtends ) ;
280
268
281
- ... unknownOptions
282
- } } ) {
283
- fileName = file . opts . filename . slice ( file . opts . cwd . length + 1 ) ;
284
- programBody = path . node . body ;
269
+ if ( Object . keys ( unknownOptions ) . length > 0 ) {
270
+ warnOptions ( unknownOptions ) ;
271
+ }
272
+ } ;
285
273
286
- // options parsing
274
+ // plugin api
287
275
288
- if ( classComponentExtendsObject !== undefined ) {
289
- if ( Array . isArray ( classComponentExtendsObject ) ) optionClassComponentExtendsObject . push ( ...classComponentExtendsObject ) ;
290
- else warnOptions ( { classComponentExtendsObject } ) ;
291
- }
276
+ return {
277
+ name : pluginName ,
292
278
293
- if ( classComponentExtends !== undefined ) {
294
- if ( Array . isArray ( classComponentExtends ) ) optionClassComponentExtends . push ( ...classComponentExtends ) ;
295
- else warnOptions ( { classComponentExtends } ) ;
296
- }
279
+ visitor : {
280
+ AssignmentExpression : visitAssignmentExpression ,
297
281
298
- if ( logIgnoredBinding !== undefined ) optionLogIgnoredBinding = Boolean ( logIgnoredBinding ) ;
299
- if ( logIgnoredClassComponentExtends !== undefined ) optionLogIgnoredClassComponentExtends = Boolean ( logIgnoredClassComponentExtends ) ;
282
+ Program : {
283
+ enter ( path , { file, opts } ) {
284
+ fileName = file . opts . filename . slice ( file . opts . cwd . length + 1 ) ;
300
285
301
- if ( Object . keys ( unknownOptions ) . length > 0 ) {
302
- warnOptions ( unknownOptions ) ;
303
- }
286
+ getImportIdentifier ( path ) ;
287
+ parseOptions ( opts ) ;
304
288
} ,
305
- } ,
306
289
290
+ exit : updateImports ,
291
+ } ,
307
292
} ,
308
293
} ;
309
294
} ;
0 commit comments