@@ -31,32 +31,36 @@ const messages = {
31
31
module . exports = {
32
32
meta : {
33
33
docs : {
34
- description : 'Disallow missing displayName in a React component definition' ,
34
+ description :
35
+ 'Disallow missing displayName in a React component definition' ,
35
36
category : 'Best Practices' ,
36
37
recommended : true ,
37
38
url : docsUrl ( 'display-name' ) ,
38
39
} ,
39
40
40
41
messages,
41
42
42
- schema : [ {
43
- type : 'object' ,
44
- properties : {
45
- ignoreTranspilerName : {
46
- type : 'boolean' ,
47
- } ,
48
- checkContextObjects : {
49
- type : 'boolean' ,
43
+ schema : [
44
+ {
45
+ type : 'object' ,
46
+ properties : {
47
+ ignoreTranspilerName : {
48
+ type : 'boolean' ,
49
+ } ,
50
+ checkContextObjects : {
51
+ type : 'boolean' ,
52
+ } ,
50
53
} ,
54
+ additionalProperties : false ,
51
55
} ,
52
- additionalProperties : false ,
53
- } ] ,
56
+ ] ,
54
57
} ,
55
58
56
59
create : Components . detect ( ( context , components , utils ) => {
57
60
const config = context . options [ 0 ] || { } ;
58
61
const ignoreTranspilerName = config . ignoreTranspilerName || false ;
59
- const checkContextObjects = ( config . checkContextObjects || false ) && testReactVersion ( context , '>= 16.3.0' ) ;
62
+ const checkContextObjects = ( config . checkContextObjects || false )
63
+ && testReactVersion ( context , '>= 16.3.0' ) ;
60
64
61
65
const contextObjects = new Map ( ) ;
62
66
@@ -76,10 +80,12 @@ module.exports = {
76
80
* @returns {boolean } True if React.forwardRef is nested inside React.memo, false if not.
77
81
*/
78
82
function isNestedMemo ( node ) {
79
- return astUtil . isCallExpression ( node )
83
+ return (
84
+ astUtil . isCallExpression ( node )
80
85
&& node . arguments
81
86
&& astUtil . isCallExpression ( node . arguments [ 0 ] )
82
- && utils . isPragmaComponentWrapper ( node ) ;
87
+ && utils . isPragmaComponentWrapper ( node )
88
+ ) ;
83
89
}
84
90
85
91
/**
@@ -115,60 +121,142 @@ module.exports = {
115
121
* @returns {boolean } True if component has a name, false if not.
116
122
*/
117
123
function hasTranspilerName ( node ) {
118
- const namedObjectAssignment = (
119
- node . type === 'ObjectExpression'
124
+ const namedObjectAssignment = node . type === 'ObjectExpression'
120
125
&& node . parent
121
126
&& node . parent . parent
122
127
&& node . parent . parent . type === 'AssignmentExpression'
123
- && (
124
- ! node . parent . parent . left . object
128
+ && ( ! node . parent . parent . left . object
125
129
|| node . parent . parent . left . object . name !== 'module'
126
- || node . parent . parent . left . property . name !== 'exports'
127
- )
128
- ) ;
129
- const namedObjectDeclaration = (
130
- node . type === 'ObjectExpression'
130
+ || node . parent . parent . left . property . name !== 'exports' ) ;
131
+ const namedObjectDeclaration = node . type === 'ObjectExpression'
131
132
&& node . parent
132
133
&& node . parent . parent
133
- && node . parent . parent . type === 'VariableDeclarator'
134
- ) ;
135
- const namedClass = (
136
- ( node . type === 'ClassDeclaration' || node . type === 'ClassExpression' )
134
+ && node . parent . parent . type === 'VariableDeclarator' ;
135
+ const namedClass = ( node . type === 'ClassDeclaration' || node . type === 'ClassExpression' )
137
136
&& node . id
138
- && ! ! node . id . name
139
- ) ;
137
+ && ! ! node . id . name ;
140
138
141
- const namedFunctionDeclaration = (
142
- ( node . type === 'FunctionDeclaration' || node . type === 'FunctionExpression' )
139
+ const namedFunctionDeclaration = ( node . type === 'FunctionDeclaration'
140
+ || node . type === 'FunctionExpression' )
143
141
&& node . id
144
- && ! ! node . id . name
145
- ) ;
142
+ && ! ! node . id . name ;
146
143
147
- const namedFunctionExpression = (
148
- astUtil . isFunctionLikeExpression ( node )
144
+ const namedFunctionExpression = astUtil . isFunctionLikeExpression ( node )
149
145
&& node . parent
150
- && ( node . parent . type === 'VariableDeclarator' || node . parent . type === 'Property' || node . parent . method === true )
151
- && ( ! node . parent . parent || ! componentUtil . isES5Component ( node . parent . parent , context ) )
152
- ) ;
146
+ && ( node . parent . type === 'VariableDeclarator'
147
+ || node . parent . type === 'Property'
148
+ || node . parent . method === true )
149
+ && ( ! node . parent . parent
150
+ || ! componentUtil . isES5Component ( node . parent . parent , context ) ) ;
153
151
154
152
if (
155
- namedObjectAssignment || namedObjectDeclaration
153
+ namedObjectAssignment
154
+ || namedObjectDeclaration
156
155
|| namedClass
157
- || namedFunctionDeclaration || namedFunctionExpression
156
+ || namedFunctionDeclaration
157
+ || namedFunctionExpression
158
158
) {
159
159
return true ;
160
160
}
161
161
return false ;
162
162
}
163
163
164
+ function hasVariableDeclaration ( node , name ) {
165
+ if ( ! node ) return false ;
166
+
167
+ if ( node . type === 'VariableDeclaration' ) {
168
+ return node . declarations . some ( ( decl ) => {
169
+ if ( ! decl . id ) return false ;
170
+
171
+ // const name = ...
172
+ if ( decl . id . type === 'Identifier' && decl . id . name === name ) {
173
+ return true ;
174
+ }
175
+
176
+ // const [name] = ...
177
+ if ( decl . id . type === 'ArrayPattern' ) {
178
+ return decl . id . elements . some (
179
+ ( el ) => el && el . type === 'Identifier' && el . name === name
180
+ ) ;
181
+ }
182
+
183
+ // const { name } = ...
184
+ if ( decl . id . type === 'ObjectPattern' ) {
185
+ return decl . id . properties . some (
186
+ ( prop ) => prop . type === 'Property' && prop . key && prop . key . name === name
187
+ ) ;
188
+ }
189
+
190
+ return false ;
191
+ } ) ;
192
+ }
193
+
194
+ if ( node . type === 'BlockStatement' && node . body ) {
195
+ return node . body . some ( ( stmt ) => hasVariableDeclaration ( stmt , name ) ) ;
196
+ }
197
+
198
+ return false ;
199
+ }
200
+
201
+ function isIdentifierShadowed ( node , identifierName ) {
202
+ while ( node && node . parent ) {
203
+ node = node . parent ;
204
+ if (
205
+ node . type === 'FunctionDeclaration'
206
+ || node . type === 'FunctionExpression'
207
+ || node . type === 'ArrowFunctionExpression'
208
+ ) {
209
+ break ;
210
+ }
211
+ }
212
+
213
+ if ( ! node || ! node . body ) {
214
+ return false ;
215
+ }
216
+
217
+ return hasVariableDeclaration ( node . body , identifierName ) ;
218
+ }
219
+
220
+ /**
221
+ *
222
+ * Check is current component shadowed
223
+ * @param {ASTNode } node The AST node being checked.
224
+ * @returns {boolean } True if component has a name, false if not.
225
+ */
226
+
227
+ function isShadowedComponent ( node ) {
228
+ if ( ! node || node . type !== 'CallExpression' ) {
229
+ return false ;
230
+ }
231
+
232
+ if (
233
+ node . callee . type === 'MemberExpression'
234
+ && node . callee . object . name === 'React'
235
+ ) {
236
+ return isIdentifierShadowed ( node , 'React' ) ;
237
+ }
238
+
239
+ if ( node . callee . type === 'Identifier' ) {
240
+ const name = node . callee . name ;
241
+ if ( name === 'memo' || name === 'forwardRef' ) {
242
+ return isIdentifierShadowed ( node , name ) ;
243
+ }
244
+ }
245
+
246
+ return false ;
247
+ }
248
+
164
249
// --------------------------------------------------------------------------
165
250
// Public
166
251
// --------------------------------------------------------------------------
167
252
168
253
return {
169
254
ExpressionStatement ( node ) {
170
255
if ( checkContextObjects && isCreateContext ( node ) ) {
171
- contextObjects . set ( node . expression . left . name , { node, hasDisplayName : false } ) ;
256
+ contextObjects . set ( node . expression . left . name , {
257
+ node,
258
+ hasDisplayName : false ,
259
+ } ) ;
172
260
}
173
261
} ,
174
262
VariableDeclarator ( node ) {
@@ -232,7 +320,10 @@ module.exports = {
232
320
if ( ignoreTranspilerName || ! hasTranspilerName ( node ) ) {
233
321
// Search for the displayName declaration
234
322
node . properties . forEach ( ( property ) => {
235
- if ( ! property . key || ! propsUtil . isDisplayNameDeclaration ( property . key ) ) {
323
+ if (
324
+ ! property . key
325
+ || ! propsUtil . isDisplayNameDeclaration ( property . key )
326
+ ) {
236
327
return ;
237
328
}
238
329
markDisplayNameAsDeclared ( node ) ;
@@ -247,7 +338,10 @@ module.exports = {
247
338
return ;
248
339
}
249
340
250
- if ( node . arguments . length > 0 && astUtil . isFunctionLikeExpression ( node . arguments [ 0 ] ) ) {
341
+ if (
342
+ node . arguments . length > 0
343
+ && astUtil . isFunctionLikeExpression ( node . arguments [ 0 ] )
344
+ ) {
251
345
// Skip over React.forwardRef declarations that are embedded within
252
346
// a React.memo i.e. React.memo(React.forwardRef(/* ... */))
253
347
// This means that we raise a single error for the call to React.memo
@@ -269,9 +363,16 @@ module.exports = {
269
363
'Program:exit' ( ) {
270
364
const list = components . list ( ) ;
271
365
// Report missing display name for all components
272
- values ( list ) . filter ( ( component ) => ! component . hasDisplayName ) . forEach ( ( component ) => {
273
- reportMissingDisplayName ( component ) ;
274
- } ) ;
366
+ values ( list )
367
+ . filter ( ( component ) => {
368
+ if ( isShadowedComponent ( component . node ) ) {
369
+ return false ;
370
+ }
371
+ return ! component . hasDisplayName ;
372
+ } )
373
+ . forEach ( ( component ) => {
374
+ reportMissingDisplayName ( component ) ;
375
+ } ) ;
275
376
if ( checkContextObjects ) {
276
377
// Report missing display name for all context objects
277
378
forEach (
0 commit comments