diff --git a/crates/oxc_linter/src/rules/react/display_name.rs b/crates/oxc_linter/src/rules/react/display_name.rs index bc6aac030dd2c..95a1beb8c4fd4 100644 --- a/crates/oxc_linter/src/rules/react/display_name.rs +++ b/crates/oxc_linter/src/rules/react/display_name.rs @@ -175,16 +175,56 @@ impl Rule for DisplayName { if !has_display_name_via_semantic(symbol_id, component_info.name.as_ref(), ctx) { components_to_report.push((component_info.span, component_info.is_context)); } - } else if check_context_objects { - // If the declaration isn't a component, check if any write references assign createContext() - // This handles: var Hello; Hello = createContext(); - if let Some(component_info) = - check_context_assignment_references(symbol_id, decl_node, ctx) - { - // Check if this symbol has a displayName assignment - if !has_display_name_via_semantic(symbol_id, component_info.name.as_ref(), ctx) + } else { + // The primary declaration isn't a component. Check redeclarations too. + // This handles cases where a TypeScript interface/type shares the same name + // as a variable (e.g., `interface Foo {}; const Foo = createContext()`). + // The interface gets the primary declaration, but the variable is a redeclaration. + let mut found_in_redecl = false; + for redecl in ctx.scoping().symbol_redeclarations(symbol_id) { + let redecl_node = ctx.nodes().get_node(redecl.declaration); + if let Some(component_info) = is_react_component_node( + redecl_node, + ctx, + &mut version_cache, + ignore_transpiler_name, + check_context_objects, + ) { + if component_info.name.is_some() + && !ignore_transpiler_name + && !component_info.is_context + { + found_in_redecl = true; + break; + } + if !has_display_name_via_semantic( + symbol_id, + component_info.name.as_ref(), + ctx, + ) { + components_to_report + .push((component_info.span, component_info.is_context)); + } + found_in_redecl = true; + break; + } + } + + if !found_in_redecl && check_context_objects { + // If the declaration isn't a component, check if any write references assign createContext() + // This handles: var Hello; Hello = createContext(); + if let Some(component_info) = + check_context_assignment_references(symbol_id, decl_node, ctx) { - components_to_report.push((component_info.span, component_info.is_context)); + // Check if this symbol has a displayName assignment + if !has_display_name_via_semantic( + symbol_id, + component_info.name.as_ref(), + ctx, + ) { + components_to_report + .push((component_info.span, component_info.is_context)); + } } } } @@ -1738,6 +1778,21 @@ fn test() { Some(serde_json::json!([{ "checkContextObjects": true }])), None, ), + // Regression test for #19607: TS interface + createContext with displayName set (should pass) + ( + r#" + import { createContext } from 'react'; + + interface PostHogGroupContext { + value: string; + } + + const PostHogGroupContext = createContext(null); + PostHogGroupContext.displayName = "PostHogGroupContext"; + "#, + Some(serde_json::json!([{ "checkContextObjects": true }])), + None, + ), ]; let fail = vec![ @@ -2167,6 +2222,32 @@ fn test() { Some(serde_json::json!([{ "checkContextObjects": true }])), None, ), + // Regression test for #19607: TS interface with same name as createContext variable + ( + " + import { createContext } from 'react'; + + interface PostHogGroupContext { + value: string; + } + + const PostHogGroupContext = createContext(null); + ", + Some(serde_json::json!([{ "checkContextObjects": true }])), + None, + ), + // Same issue with type alias instead of interface + ( + " + import { createContext } from 'react'; + + type MyContext = { value: string }; + + const MyContext = createContext(null); + ", + Some(serde_json::json!([{ "checkContextObjects": true }])), + None, + ), ]; Tester::new(DisplayName::NAME, DisplayName::PLUGIN, pass, fail).test_and_snapshot(); diff --git a/crates/oxc_linter/src/snapshots/react_display_name.snap b/crates/oxc_linter/src/snapshots/react_display_name.snap index 95e2036977b26..644a0f7ecf5bc 100644 --- a/crates/oxc_linter/src/snapshots/react_display_name.snap +++ b/crates/oxc_linter/src/snapshots/react_display_name.snap @@ -360,3 +360,21 @@ source: crates/oxc_linter/src/tester.rs 6 │ ╰──── help: Add a `displayName` property to the context. + + ⚠ eslint-plugin-react(display-name): Context definition is missing display name. + ╭─[display_name.tsx:8:27] + 7 │ + 8 │ const PostHogGroupContext = createContext(null); + · ─────────────────── + 9 │ + ╰──── + help: Add a `displayName` property to the context. + + ⚠ eslint-plugin-react(display-name): Context definition is missing display name. + ╭─[display_name.tsx:6:27] + 5 │ + 6 │ const MyContext = createContext(null); + · ───────── + 7 │ + ╰──── + help: Add a `displayName` property to the context.