Skip to content

Commit 30f0022

Browse files
committed
[Fix] display-name: avoid false positive when React is shadowed
Fixes #3924
1 parent dd2e968 commit 30f0022

File tree

2 files changed

+271
-60
lines changed

2 files changed

+271
-60
lines changed

lib/rules/display-name.js

Lines changed: 147 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -31,32 +31,36 @@ const messages = {
3131
module.exports = {
3232
meta: {
3333
docs: {
34-
description: 'Disallow missing displayName in a React component definition',
34+
description:
35+
'Disallow missing displayName in a React component definition',
3536
category: 'Best Practices',
3637
recommended: true,
3738
url: docsUrl('display-name'),
3839
},
3940

4041
messages,
4142

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+
},
5053
},
54+
additionalProperties: false,
5155
},
52-
additionalProperties: false,
53-
}],
56+
],
5457
},
5558

5659
create: Components.detect((context, components, utils) => {
5760
const config = context.options[0] || {};
5861
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');
6064

6165
const contextObjects = new Map();
6266

@@ -76,10 +80,12 @@ module.exports = {
7680
* @returns {boolean} True if React.forwardRef is nested inside React.memo, false if not.
7781
*/
7882
function isNestedMemo(node) {
79-
return astUtil.isCallExpression(node)
83+
return (
84+
astUtil.isCallExpression(node)
8085
&& node.arguments
8186
&& astUtil.isCallExpression(node.arguments[0])
82-
&& utils.isPragmaComponentWrapper(node);
87+
&& utils.isPragmaComponentWrapper(node)
88+
);
8389
}
8490

8591
/**
@@ -115,60 +121,142 @@ module.exports = {
115121
* @returns {boolean} True if component has a name, false if not.
116122
*/
117123
function hasTranspilerName(node) {
118-
const namedObjectAssignment = (
119-
node.type === 'ObjectExpression'
124+
const namedObjectAssignment = node.type === 'ObjectExpression'
120125
&& node.parent
121126
&& node.parent.parent
122127
&& node.parent.parent.type === 'AssignmentExpression'
123-
&& (
124-
!node.parent.parent.left.object
128+
&& (!node.parent.parent.left.object
125129
|| 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'
131132
&& node.parent
132133
&& 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')
137136
&& node.id
138-
&& !!node.id.name
139-
);
137+
&& !!node.id.name;
140138

141-
const namedFunctionDeclaration = (
142-
(node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression')
139+
const namedFunctionDeclaration = (node.type === 'FunctionDeclaration'
140+
|| node.type === 'FunctionExpression')
143141
&& node.id
144-
&& !!node.id.name
145-
);
142+
&& !!node.id.name;
146143

147-
const namedFunctionExpression = (
148-
astUtil.isFunctionLikeExpression(node)
144+
const namedFunctionExpression = astUtil.isFunctionLikeExpression(node)
149145
&& 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));
153151

154152
if (
155-
namedObjectAssignment || namedObjectDeclaration
153+
namedObjectAssignment
154+
|| namedObjectDeclaration
156155
|| namedClass
157-
|| namedFunctionDeclaration || namedFunctionExpression
156+
|| namedFunctionDeclaration
157+
|| namedFunctionExpression
158158
) {
159159
return true;
160160
}
161161
return false;
162162
}
163163

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+
164249
// --------------------------------------------------------------------------
165250
// Public
166251
// --------------------------------------------------------------------------
167252

168253
return {
169254
ExpressionStatement(node) {
170255
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+
});
172260
}
173261
},
174262
VariableDeclarator(node) {
@@ -232,7 +320,10 @@ module.exports = {
232320
if (ignoreTranspilerName || !hasTranspilerName(node)) {
233321
// Search for the displayName declaration
234322
node.properties.forEach((property) => {
235-
if (!property.key || !propsUtil.isDisplayNameDeclaration(property.key)) {
323+
if (
324+
!property.key
325+
|| !propsUtil.isDisplayNameDeclaration(property.key)
326+
) {
236327
return;
237328
}
238329
markDisplayNameAsDeclared(node);
@@ -247,7 +338,10 @@ module.exports = {
247338
return;
248339
}
249340

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+
) {
251345
// Skip over React.forwardRef declarations that are embedded within
252346
// a React.memo i.e. React.memo(React.forwardRef(/* ... */))
253347
// This means that we raise a single error for the call to React.memo
@@ -269,9 +363,16 @@ module.exports = {
269363
'Program:exit'() {
270364
const list = components.list();
271365
// 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+
});
275376
if (checkContextObjects) {
276377
// Report missing display name for all context objects
277378
forEach(

0 commit comments

Comments
 (0)