Skip to content
123 changes: 120 additions & 3 deletions lib/rules/display-name.js
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,123 @@
return false;
}

function hasVariableDeclaration(node, name) {
if (!node) return false;

if (node.type === 'VariableDeclaration') {
return node.declarations.some((decl) => {
if (!decl.id) return false;

// const name = ...
if (decl.id.type === 'Identifier' && decl.id.name === name) {
return true;
}

// const [name] = ...
if (decl.id.type === 'ArrayPattern') {
return decl.id.elements.some(
(el) => el && el.type === 'Identifier' && el.name === name
);
}

// const { name } = ...
if (decl.id.type === 'ObjectPattern') {
return decl.id.properties.some(
(prop) => prop.type === 'Property' && prop.key && prop.key.name === name
);
}

return false;
});
}

if (node.type === 'BlockStatement' && node.body) {
return node.body.some((stmt) => hasVariableDeclaration(stmt, name));
}

return false;
}

function isIdentifierShadowed(node, identifierName) {
let currentNode = node;

while (currentNode && currentNode.parent) {
currentNode = currentNode.parent;

if (
currentNode.type === 'FunctionDeclaration'
|| currentNode.type === 'FunctionExpression'
|| currentNode.type === 'ArrowFunctionExpression'
Comment on lines +208 to +210

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why only these scopes? For example what about BlockStatement:

{ const memo = (cb) => cb() const forwardRef = (cb) => cb() const React = { memo, forwardRef } const BlockReactShadowedMemo = React.memo(() => { return <div>shadowed</div> }) } 
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the review. Let me take a quick look and try to find a better algorithm to cover all test cases.

) {
if (currentNode.body && hasVariableDeclaration(currentNode.body, identifierName)) {
return true;

Check warning on line 213 in lib/rules/display-name.js

View check run for this annotation

Codecov / codecov/patch

lib/rules/display-name.js#L213

Added line #L213 was not covered by tests
}
}

if (currentNode.type === 'BlockStatement') {
if (hasVariableDeclaration(currentNode, identifierName)) {
return true;
}
}

if (
(currentNode.type === 'FunctionDeclaration'
|| currentNode.type === 'FunctionExpression'
|| currentNode.type === 'ArrowFunctionExpression')
&& currentNode.params
) {
const isParamShadowed = currentNode.params.some((param) => {
if (param.type === 'Identifier' && param.name === identifierName) {
return true;

Check warning on line 231 in lib/rules/display-name.js

View check run for this annotation

Codecov / codecov/patch

lib/rules/display-name.js#L230-L231

Added lines #L230 - L231 were not covered by tests
}
if (param.type === 'ObjectPattern') {
return param.properties.some(
(prop) => prop.type === 'Property' && prop.key && prop.key.name === identifierName

Check warning on line 235 in lib/rules/display-name.js

View check run for this annotation

Codecov / codecov/patch

lib/rules/display-name.js#L233-L235

Added lines #L233 - L235 were not covered by tests
);
}
if (param.type === 'ArrayPattern') {
return param.elements.some(
(el) => el && el.type === 'Identifier' && el.name === identifierName

Check warning on line 240 in lib/rules/display-name.js

View check run for this annotation

Codecov / codecov/patch

lib/rules/display-name.js#L238-L240

Added lines #L238 - L240 were not covered by tests
);
}
return false;

Check warning on line 243 in lib/rules/display-name.js

View check run for this annotation

Codecov / codecov/patch

lib/rules/display-name.js#L243

Added line #L243 was not covered by tests
});

if (isParamShadowed) {
return true;

Check warning on line 247 in lib/rules/display-name.js

View check run for this annotation

Codecov / codecov/patch

lib/rules/display-name.js#L247

Added line #L247 was not covered by tests
}
}
}

return false;
}
/**
* Checks whether the component wrapper (e.g. React.memo or forwardRef) is shadowed in the current scope.
* @param {ASTNode} node - The CallExpression AST node representing a potential component wrapper.
* @returns {boolean} True if the wrapper identifier (e.g. 'React', 'memo', 'forwardRef') is shadowed, false otherwise.
*/
function isShadowedComponent(node) {
if (!node || node.type !== 'CallExpression') {
return false;
}

if (
node.callee.type === 'MemberExpression'
&& node.callee.object.name === 'React'
) {
return isIdentifierShadowed(node, 'React');
}

if (node.callee.type === 'Identifier') {
const name = node.callee.name;
if (name === 'memo' || name === 'forwardRef') {
return isIdentifierShadowed(node, name);
}
}

return false;
}

// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
Expand Down Expand Up @@ -269,9 +386,9 @@
'Program:exit'() {
const list = components.list();
// Report missing display name for all components
values(list).filter((component) => !component.hasDisplayName).forEach((component) => {
reportMissingDisplayName(component);
});
values(list)
.filter((component) => !isShadowedComponent(component.node) && !component.hasDisplayName)
.forEach((component) => { reportMissingDisplayName(component); });
if (checkContextObjects) {
// Report missing display name for all context objects
forEach(
Expand Down
142 changes: 142 additions & 0 deletions tests/lib/rules/display-name.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,72 @@ const parserOptions = {
const ruleTester = new RuleTester({ parserOptions });
ruleTester.run('display-name', rule, {
valid: parsers.all([
{
code: `
import React, { memo, forwardRef } from 'react'

const TestComponent = function () {
{
const memo = (cb) => cb()
const forwardRef = (cb) => cb()
const React = { memo, forwardRef }

const BlockMemo = memo(() => <div>shadowed</div>)
const BlockForwardRef = forwardRef(() => <div>shadowed</div>)
const BlockReactMemo = React.memo(() => <div>shadowed</div>)
}
return null
}
`,
},
{
code: `
import React, { memo, forwardRef } from 'react'

const Test1 = function (memo) {
return memo(() => <div>param shadowed</div>)
}

const Test2 = function ({ forwardRef }) {
return forwardRef(() => <div>destructured param</div>)
}
`,
},
{
code: `
import React, { memo, forwardRef } from 'react'

const TestComponent = function () {
function innerFunction() {
const memo = (cb) => cb()
const React = { forwardRef }

const Comp = memo(() => <div>nested</div>)
const ForwardComp = React.forwardRef(() => <div>nested</div>)
return [Comp, ForwardComp]
}
return innerFunction()
}
`,
},
{
code: `
import React, { memo, forwardRef } from 'react'

const MixedShadowed = function () {
const memo = (cb) => cb()
const { forwardRef } = { forwardRef: () => null }
const [React] = [{ memo, forwardRef }]

const Comp = memo(() => <div>shadowed</div>)
const ReactMemo = React.memo(() => null)
const ReactForward = React.forwardRef((props, ref) => \`\${props} \${ref}\`)
const OtherComp = forwardRef((props, ref) => \`\${props} \${ref}\`)

return [Comp, ReactMemo, ReactForward, OtherComp]
}
`,
},
{
code: `
var Hello = createReactClass({
Expand Down Expand Up @@ -848,6 +914,82 @@ ruleTester.run('display-name', rule, {
]),

invalid: parsers.all([
{
code: `
import React, { memo, forwardRef } from 'react'

const TestComponent = function () {
{
const BlockReactMemo = React.memo(() => {
return <div>not shadowed</div>
})

const BlockMemo = memo(() => {
return <div>not shadowed</div>
})

const BlockForwardRef = forwardRef((props, ref) => {
return \`\${props} \${ref}\`
})
}

return null
}
`,
errors: [
{ messageId: 'noDisplayName' },
{ messageId: 'noDisplayName' },
{ messageId: 'noDisplayName' }
],
},
{
code: `
import React, { memo, forwardRef } from 'react'

const Test1 = function () {
const Comp = memo(() => <div>not param shadowed</div>)
return Comp
}

const Test2 = function () {
function innerFunction() {
const Comp = memo(() => <div>nested not shadowed</div>)
const ForwardComp = React.forwardRef(() => <div>nested</div>)
return [Comp, ForwardComp]
}
return innerFunction()
}
`,
errors: [
{ messageId: 'noDisplayName' },
{ messageId: 'noDisplayName' },
{ messageId: 'noDisplayName' }
],
},
{
code: `
import React, { memo, forwardRef } from 'react'

const MixedNotShadowed = function () {
const Comp = memo(() => {
return <div>not shadowed</div>
})
const ReactMemo = React.memo(() => null)
const ReactForward = React.forwardRef((props, ref) => {
return \`\${props} \${ref}\`
})
const OtherComp = forwardRef((props, ref) => \`\${props} \${ref}\`)

return [Comp, ReactMemo, ReactForward, OtherComp]
}
`,
errors: [
{ messageId: 'noDisplayName' },
{ messageId: 'noDisplayName' },
{ messageId: 'noDisplayName' },
{ messageId: 'noDisplayName' }
],
},
{
code: `
var Hello = createReactClass({
Expand Down
Loading