diff --git a/lib/rules/display-name.js b/lib/rules/display-name.js index b85ec34f1c..9c812767be 100644 --- a/lib/rules/display-name.js +++ b/lib/rules/display-name.js @@ -161,6 +161,123 @@ module.exports = { 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' + ) { + if (currentNode.body && hasVariableDeclaration(currentNode.body, identifierName)) { + return true; + } + } + + 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; + } + if (param.type === 'ObjectPattern') { + return param.properties.some( + (prop) => prop.type === 'Property' && prop.key && prop.key.name === identifierName + ); + } + if (param.type === 'ArrayPattern') { + return param.elements.some( + (el) => el && el.type === 'Identifier' && el.name === identifierName + ); + } + return false; + }); + + if (isParamShadowed) { + return true; + } + } + } + + 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 // -------------------------------------------------------------------------- @@ -269,9 +386,9 @@ module.exports = { '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( diff --git a/tests/lib/rules/display-name.js b/tests/lib/rules/display-name.js index 18fd1ca83c..44b271de55 100644 --- a/tests/lib/rules/display-name.js +++ b/tests/lib/rules/display-name.js @@ -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(() =>