Skip to content

Commit 6a42bc9

Browse files
committed
[Fix] display-name: avoid false positive when React is shadowed
Fixes jsx-eslint#3924
1 parent 36b8e43 commit 6a42bc9

File tree

3 files changed

+264
-3
lines changed

3 files changed

+264
-3
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
2222
* Remove extra space from CLI warning ([#3942][] @junaidkbr)
2323
* [`jsx-key`]: detect missing keys in return statement with ternary operator ([#3928][] @hyeonbinHur)
2424
* [`jsx-key`]: detect missing keys in logical expressions ([#3986][] @yalperg)
25+
* [`display-name`]: avoid false positive when React is shadowed ([#3926][] @hyeonbinHur)
2526

2627
### Changed
2728
* [Docs] [`no-array-index-key`]: add template literal examples ([#3978][] @akahoshi1421)
@@ -36,6 +37,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
3637
[#3942]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3942
3738
[#3930]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3930
3839
[#3928]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3928
40+
[#3926]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3926
3941
[#3923]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3923
4042
[#3877]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3877
4143
[#3729]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3729

lib/rules/display-name.js

Lines changed: 120 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,123 @@ module.exports = {
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+
let currentNode = node;
203+
204+
while (currentNode && currentNode.parent) {
205+
currentNode = currentNode.parent;
206+
207+
if (
208+
currentNode.type === 'FunctionDeclaration'
209+
|| currentNode.type === 'FunctionExpression'
210+
|| currentNode.type === 'ArrowFunctionExpression'
211+
) {
212+
if (currentNode.body && hasVariableDeclaration(currentNode.body, identifierName)) {
213+
return true;
214+
}
215+
}
216+
217+
if (currentNode.type === 'BlockStatement') {
218+
if (hasVariableDeclaration(currentNode, identifierName)) {
219+
return true;
220+
}
221+
}
222+
223+
if (
224+
(currentNode.type === 'FunctionDeclaration'
225+
|| currentNode.type === 'FunctionExpression'
226+
|| currentNode.type === 'ArrowFunctionExpression')
227+
&& currentNode.params
228+
) {
229+
const isParamShadowed = currentNode.params.some((param) => {
230+
if (param.type === 'Identifier' && param.name === identifierName) {
231+
return true;
232+
}
233+
if (param.type === 'ObjectPattern') {
234+
return param.properties.some(
235+
(prop) => prop.type === 'Property' && prop.key && prop.key.name === identifierName
236+
);
237+
}
238+
if (param.type === 'ArrayPattern') {
239+
return param.elements.some(
240+
(el) => el && el.type === 'Identifier' && el.name === identifierName
241+
);
242+
}
243+
return false;
244+
});
245+
246+
if (isParamShadowed) {
247+
return true;
248+
}
249+
}
250+
}
251+
252+
return false;
253+
}
254+
/**
255+
* Checks whether the component wrapper (e.g. React.memo or forwardRef) is shadowed in the current scope.
256+
* @param {ASTNode} node - The CallExpression AST node representing a potential component wrapper.
257+
* @returns {boolean} True if the wrapper identifier (e.g. 'React', 'memo', 'forwardRef') is shadowed, false otherwise.
258+
*/
259+
function isShadowedComponent(node) {
260+
if (!node || node.type !== 'CallExpression') {
261+
return false;
262+
}
263+
264+
if (
265+
node.callee.type === 'MemberExpression'
266+
&& node.callee.object.name === 'React'
267+
) {
268+
return isIdentifierShadowed(node, 'React');
269+
}
270+
271+
if (node.callee.type === 'Identifier') {
272+
const name = node.callee.name;
273+
if (name === 'memo' || name === 'forwardRef') {
274+
return isIdentifierShadowed(node, name);
275+
}
276+
}
277+
278+
return false;
279+
}
280+
164281
// --------------------------------------------------------------------------
165282
// Public
166283
// --------------------------------------------------------------------------
@@ -269,9 +386,9 @@ module.exports = {
269386
'Program:exit'() {
270387
const list = components.list();
271388
// Report missing display name for all components
272-
values(list).filter((component) => !component.hasDisplayName).forEach((component) => {
273-
reportMissingDisplayName(component);
274-
});
389+
values(list)
390+
.filter((component) => !isShadowedComponent(component.node) && !component.hasDisplayName)
391+
.forEach((component) => { reportMissingDisplayName(component); });
275392
if (checkContextObjects) {
276393
// Report missing display name for all context objects
277394
forEach(

tests/lib/rules/display-name.js

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,72 @@ const parserOptions = {
2929
const ruleTester = new RuleTester({ parserOptions });
3030
ruleTester.run('display-name', rule, {
3131
valid: parsers.all([
32+
{
33+
code: `
34+
import React, { memo, forwardRef } from 'react'
35+
36+
const TestComponent = function () {
37+
{
38+
const memo = (cb) => cb()
39+
const forwardRef = (cb) => cb()
40+
const React = { memo, forwardRef }
41+
42+
const BlockMemo = memo(() => <div>shadowed</div>)
43+
const BlockForwardRef = forwardRef(() => <div>shadowed</div>)
44+
const BlockReactMemo = React.memo(() => <div>shadowed</div>)
45+
}
46+
return null
47+
}
48+
`,
49+
},
50+
{
51+
code: `
52+
import React, { memo, forwardRef } from 'react'
53+
54+
const Test1 = function (memo) {
55+
return memo(() => <div>param shadowed</div>)
56+
}
57+
58+
const Test2 = function ({ forwardRef }) {
59+
return forwardRef(() => <div>destructured param</div>)
60+
}
61+
`,
62+
},
63+
{
64+
code: `
65+
import React, { memo, forwardRef } from 'react'
66+
67+
const TestComponent = function () {
68+
function innerFunction() {
69+
const memo = (cb) => cb()
70+
const React = { forwardRef }
71+
72+
const Comp = memo(() => <div>nested</div>)
73+
const ForwardComp = React.forwardRef(() => <div>nested</div>)
74+
return [Comp, ForwardComp]
75+
}
76+
return innerFunction()
77+
}
78+
`,
79+
},
80+
{
81+
code: `
82+
import React, { memo, forwardRef } from 'react'
83+
84+
const MixedShadowed = function () {
85+
const memo = (cb) => cb()
86+
const { forwardRef } = { forwardRef: () => null }
87+
const [React] = [{ memo, forwardRef }]
88+
89+
const Comp = memo(() => <div>shadowed</div>)
90+
const ReactMemo = React.memo(() => null)
91+
const ReactForward = React.forwardRef((props, ref) => \`\${props} \${ref}\`)
92+
const OtherComp = forwardRef((props, ref) => \`\${props} \${ref}\`)
93+
94+
return [Comp, ReactMemo, ReactForward, OtherComp]
95+
}
96+
`,
97+
},
3298
{
3399
code: `
34100
var Hello = createReactClass({
@@ -848,6 +914,82 @@ ruleTester.run('display-name', rule, {
848914
]),
849915

850916
invalid: parsers.all([
917+
{
918+
code: `
919+
import React, { memo, forwardRef } from 'react'
920+
921+
const TestComponent = function () {
922+
{
923+
const BlockReactMemo = React.memo(() => {
924+
return <div>not shadowed</div>
925+
})
926+
927+
const BlockMemo = memo(() => {
928+
return <div>not shadowed</div>
929+
})
930+
931+
const BlockForwardRef = forwardRef((props, ref) => {
932+
return \`\${props} \${ref}\`
933+
})
934+
}
935+
936+
return null
937+
}
938+
`,
939+
errors: [
940+
{ messageId: 'noDisplayName' },
941+
{ messageId: 'noDisplayName' },
942+
{ messageId: 'noDisplayName' },
943+
],
944+
},
945+
{
946+
code: `
947+
import React, { memo, forwardRef } from 'react'
948+
949+
const Test1 = function () {
950+
const Comp = memo(() => <div>not param shadowed</div>)
951+
return Comp
952+
}
953+
954+
const Test2 = function () {
955+
function innerFunction() {
956+
const Comp = memo(() => <div>nested not shadowed</div>)
957+
const ForwardComp = React.forwardRef(() => <div>nested</div>)
958+
return [Comp, ForwardComp]
959+
}
960+
return innerFunction()
961+
}
962+
`,
963+
errors: [
964+
{ messageId: 'noDisplayName' },
965+
{ messageId: 'noDisplayName' },
966+
{ messageId: 'noDisplayName' },
967+
],
968+
},
969+
{
970+
code: `
971+
import React, { memo, forwardRef } from 'react'
972+
973+
const MixedNotShadowed = function () {
974+
const Comp = memo(() => {
975+
return <div>not shadowed</div>
976+
})
977+
const ReactMemo = React.memo(() => null)
978+
const ReactForward = React.forwardRef((props, ref) => {
979+
return \`\${props} \${ref}\`
980+
})
981+
const OtherComp = forwardRef((props, ref) => \`\${props} \${ref}\`)
982+
983+
return [Comp, ReactMemo, ReactForward, OtherComp]
984+
}
985+
`,
986+
errors: [
987+
{ messageId: 'noDisplayName' },
988+
{ messageId: 'noDisplayName' },
989+
{ messageId: 'noDisplayName' },
990+
{ messageId: 'noDisplayName' },
991+
],
992+
},
851993
{
852994
code: `
853995
var Hello = createReactClass({

0 commit comments

Comments
 (0)