diff --git a/apps/oxlint/fixtures/disable_directive_issue_13311/.oxlintrc.json b/apps/oxlint/fixtures/disable_directive_issue_13311/.oxlintrc.json index 88601593e44c1..83307bbf2278d 100644 --- a/apps/oxlint/fixtures/disable_directive_issue_13311/.oxlintrc.json +++ b/apps/oxlint/fixtures/disable_directive_issue_13311/.oxlintrc.json @@ -5,6 +5,7 @@ }, "rules": { "react/exhaustive-deps": "warn", + "react/rules-of-hooks": "error", "typescript/no-unsafe-declaration-merging": "error" } } diff --git a/apps/oxlint/fixtures/disable_directive_issue_13311/test.jsx b/apps/oxlint/fixtures/disable_directive_issue_13311/test.jsx index fb6285de0eb55..ae814f8bb0aff 100644 --- a/apps/oxlint/fixtures/disable_directive_issue_13311/test.jsx +++ b/apps/oxlint/fixtures/disable_directive_issue_13311/test.jsx @@ -12,3 +12,15 @@ function Component() { return null; } +// Test case for issue #14233 - disable directive not working for rules-of-hooks +function useMostRelevantBreakdownType(params, filters) { + // Helper function that starts with "use" but isn't a React hook + console.log(params, filters); +} + +const cleanBreakdownParams = (cleanedParams, filters) => { + // this isn't a react hook + // oxlint-disable-next-line react-hooks/rules-of-hooks + useMostRelevantBreakdownType(cleanedParams, filters); +} + diff --git a/crates/oxc_linter/src/rules/react/rules_of_hooks.rs b/crates/oxc_linter/src/rules/react/rules_of_hooks.rs index 6afda78180771..4ab12e9f20276 100644 --- a/crates/oxc_linter/src/rules/react/rules_of_hooks.rs +++ b/crates/oxc_linter/src/rules/react/rules_of_hooks.rs @@ -10,6 +10,7 @@ use oxc_cfg::{ }; use oxc_macros::declare_oxc_lint; use oxc_semantic::{AstNodes, NodeId}; +use oxc_span::GetSpan; use oxc_syntax::operator::AssignmentOperator; use crate::{ @@ -24,14 +25,22 @@ mod diagnostics { use oxc_span::Span; const SCOPE: &str = "eslint-plugin-react-hooks"; - pub(super) fn function_error(span: Span, hook_name: &str, func_name: &str) -> OxcDiagnostic { + pub(super) fn function_error( + react_hook_span: Span, + outer_function_span: Span, + hook_name: &str, + func_name: &str, + ) -> OxcDiagnostic { OxcDiagnostic::warn(format!( "React Hook {hook_name:?} is called in function {func_name:?} that is neither \ a React function component nor a custom React Hook function. \ React component names must start with an uppercase letter. \ React Hook names must start with the word \"use\".", )) - .with_label(span) + .with_labels(vec![ + react_hook_span.primary_label("Hook is called here"), + outer_function_span.label("Outer function"), + ]) .with_error_code_scope(SCOPE) } @@ -202,6 +211,7 @@ impl Rule for RulesOfHooks { if !is_react_component_or_hook_name(&id.name) => { return ctx.diagnostic(diagnostics::function_error( + call.callee.span(), id.span, hook_name, id.name.as_str(), @@ -247,6 +257,7 @@ impl Rule for RulesOfHooks { // } if ident.is_some_and(|name| !is_react_component_or_hook_name(&name)) { return ctx.diagnostic(diagnostics::function_error( + call.callee.span(), *span, hook_name, "Anonymous", diff --git a/crates/oxc_linter/src/snapshots/react_rules_of_hooks.snap b/crates/oxc_linter/src/snapshots/react_rules_of_hooks.snap index d481c8aa74080..a76267e42243f 100644 --- a/crates/oxc_linter/src/snapshots/react_rules_of_hooks.snap +++ b/crates/oxc_linter/src/snapshots/react_rules_of_hooks.snap @@ -162,19 +162,27 @@ source: crates/oxc_linter/src/tester.rs ╰──── ⚠ eslint-plugin-react-hooks(rules-of-hooks): React Hook "useState" is called in function "handleClick" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use". - ╭─[rules_of_hooks.tsx:3:30] + ╭─[rules_of_hooks.tsx:4:25] 2 │ function ComponentWithHookInsideCallback() { 3 │ function handleClick() { - · ─────────── + · ─────┬───── + · ╰── Outer function 4 │ useState(); + · ────┬─── + · ╰── Hook is called here + 5 │ } ╰──── ⚠ eslint-plugin-react-hooks(rules-of-hooks): React Hook "useState" is called in function "handleClick" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use". - ╭─[rules_of_hooks.tsx:4:34] + ╭─[rules_of_hooks.tsx:5:29] 3 │ return function ComponentWithHookInsideCallback() { 4 │ function handleClick() { - · ─────────── + · ─────┬───── + · ╰── Outer function 5 │ useState(); + · ────┬─── + · ╰── Hook is called here + 6 │ } ╰──── ⚠ eslint-plugin-react-hooks(rules-of-hooks): React Hook "useHookInsideLoop" may be executed more than once. Possibly because it is called in a loop. React Hooks must be called in the exact same order in every component render. @@ -202,43 +210,64 @@ source: crates/oxc_linter/src/tester.rs ╰──── ⚠ eslint-plugin-react-hooks(rules-of-hooks): React Hook "useState" is called in function "renderItem" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use". - ╭─[rules_of_hooks.tsx:2:26] + ╭─[rules_of_hooks.tsx:3:21] 1 │ 2 │ function renderItem() { - · ────────── + · ─────┬──── + · ╰── Outer function 3 │ useState(); + · ────┬─── + · ╰── Hook is called here + 4 │ } ╰──── ⚠ eslint-plugin-react-hooks(rules-of-hooks): React Hook "useHookInsideNormalFunction" is called in function "normalFunctionWithHook" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use". - ╭─[rules_of_hooks.tsx:2:26] + ╭─[rules_of_hooks.tsx:3:21] 1 │ 2 │ function normalFunctionWithHook() { - · ────────────────────── + · ───────────┬────────── + · ╰── Outer function 3 │ useHookInsideNormalFunction(); + · ─────────────┬───────────── + · ╰── Hook is called here + 4 │ } ╰──── ⚠ eslint-plugin-react-hooks(rules-of-hooks): React Hook "useHookInsideNormalFunction" is called in function "_normalFunctionWithHook" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use". - ╭─[rules_of_hooks.tsx:2:26] + ╭─[rules_of_hooks.tsx:3:21] 1 │ 2 │ function _normalFunctionWithHook() { - · ─────────────────────── + · ───────────┬─────────── + · ╰── Outer function 3 │ useHookInsideNormalFunction(); + · ─────────────┬───────────── + · ╰── Hook is called here + 4 │ } ╰──── ⚠ eslint-plugin-react-hooks(rules-of-hooks): React Hook "useHookInsideNormalFunction" is called in function "_useNotAHook" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use". - ╭─[rules_of_hooks.tsx:5:26] + ╭─[rules_of_hooks.tsx:6:21] 4 │ } 5 │ function _useNotAHook() { - · ──────────── + · ──────┬───── + · ╰── Outer function 6 │ useHookInsideNormalFunction(); + · ─────────────┬───────────── + · ╰── Hook is called here + 7 │ } ╰──── ⚠ eslint-plugin-react-hooks(rules-of-hooks): React Hook "useHookInsideNormalFunction" is called in function "normalFunctionWithConditionalHook" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use". - ╭─[rules_of_hooks.tsx:2:26] + ╭─[rules_of_hooks.tsx:4:25] 1 │ 2 │ function normalFunctionWithConditionalHook() { - · ───────────────────────────────── + · ────────────────┬──────────────── + · ╰── Outer function 3 │ if (cond) { + 4 │ useHookInsideNormalFunction(); + · ─────────────┬───────────── + · ╰── Hook is called here + 5 │ } ╰──── ⚠ eslint-plugin-react-hooks(rules-of-hooks): React Hook "useHook1" may be executed more than once. Possibly because it is called in a loop. React Hooks must be called in the exact same order in every component render. @@ -346,66 +375,82 @@ source: crates/oxc_linter/src/tester.rs ╰──── ⚠ eslint-plugin-react-hooks(rules-of-hooks): React Hook "useState" is called in function "a" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use". - ╭─[rules_of_hooks.tsx:2:22] + ╭─[rules_of_hooks.tsx:2:28] 1 │ 2 │ function a() { useState(); } - · ─ + · ┬ ────┬─── + · │ ╰── Hook is called here + · ╰── Outer function 3 │ const whatever = function b() { useState(); }; ╰──── ⚠ eslint-plugin-react-hooks(rules-of-hooks): React Hook "useState" is called in function "b" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use". - ╭─[rules_of_hooks.tsx:3:39] + ╭─[rules_of_hooks.tsx:3:45] 2 │ function a() { useState(); } 3 │ const whatever = function b() { useState(); }; - · ─ + · ┬ ────┬─── + · │ ╰── Hook is called here + · ╰── Outer function 4 │ const c = () => { useState(); }; ╰──── ⚠ eslint-plugin-react-hooks(rules-of-hooks): React Hook "useState" is called in function "Anonymous" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use". - ╭─[rules_of_hooks.tsx:4:23] + ╭─[rules_of_hooks.tsx:4:31] 3 │ const whatever = function b() { useState(); }; 4 │ const c = () => { useState(); }; - · ───────────────────── + · ──────────┬──────────┬ + · │ ╰── Hook is called here + · ╰── Outer function 5 │ let d = () => useState(); ╰──── ⚠ eslint-plugin-react-hooks(rules-of-hooks): React Hook "useState" is called in function "Anonymous" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use". - ╭─[rules_of_hooks.tsx:5:21] + ╭─[rules_of_hooks.tsx:5:27] 4 │ const c = () => { useState(); }; 5 │ let d = () => useState(); - · ──────────────── + · ────────┬───────┬ + · │ ╰── Hook is called here + · ╰── Outer function 6 │ e = () => { useState(); }; ╰──── ⚠ eslint-plugin-react-hooks(rules-of-hooks): React Hook "useState" is called in function "Anonymous" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use". - ╭─[rules_of_hooks.tsx:6:17] + ╭─[rules_of_hooks.tsx:6:25] 5 │ let d = () => useState(); 6 │ e = () => { useState(); }; - · ───────────────────── + · ──────────┬──────────┬ + · │ ╰── Hook is called here + · ╰── Outer function 7 │ ({f: () => { useState(); }}); ╰──── ⚠ eslint-plugin-react-hooks(rules-of-hooks): React Hook "useState" is called in function "Anonymous" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use". - ╭─[rules_of_hooks.tsx:7:18] + ╭─[rules_of_hooks.tsx:7:26] 6 │ e = () => { useState(); }; 7 │ ({f: () => { useState(); }}); - · ───────────────────── + · ──────────┬──────────┬ + · │ ╰── Hook is called here + · ╰── Outer function 8 │ ({g() { useState(); }}); ╰──── ⚠ eslint-plugin-react-hooks(rules-of-hooks): React Hook "useState" is called in function "Anonymous" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use". - ╭─[rules_of_hooks.tsx:8:16] + ╭─[rules_of_hooks.tsx:8:21] 7 │ ({f: () => { useState(); }}); 8 │ ({g() { useState(); }}); - · ────────────────── + · ─────────┬────────┬ + · │ ╰── Hook is called here + · ╰── Outer function 9 │ const {j = () => { useState(); }} = {}; ╰──── ⚠ eslint-plugin-react-hooks(rules-of-hooks): React Hook "useState" is called in function "Anonymous" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use". - ╭─[rules_of_hooks.tsx:9:24] + ╭─[rules_of_hooks.tsx:9:32] 8 │ ({g() { useState(); }}); 9 │ const {j = () => { useState(); }} = {}; - · ───────────────────── + · ──────────┬──────────┬ + · │ ╰── Hook is called here + · ╰── Outer function 10 │ ({k = () => { useState(); }} = {}); ╰──── @@ -506,11 +551,15 @@ source: crates/oxc_linter/src/tester.rs ╰──── ⚠ eslint-plugin-react-hooks(rules-of-hooks): React Hook "useProbablyAHook" is called in function "notAComponent" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use". - ╭─[rules_of_hooks.tsx:2:48] + ╭─[rules_of_hooks.tsx:3:21] 1 │ 2 │ React.unknownFunction(function notAComponent(foo, bar) { - · ───────────── + · ──────┬────── + · ╰── Outer function 3 │ useProbablyAHook(bar) + · ────────┬─────── + · ╰── Hook is called here + 4 │ }); ╰──── ⚠ eslint-plugin-react-hooks(rules-of-hooks): React Hook "useState" cannot be called at the top level. React Hooks must be called in a React function component or a custom React Hook function. @@ -627,11 +676,15 @@ source: crates/oxc_linter/src/tester.rs ╰──── ⚠ eslint-plugin-react-hooks(rules-of-hooks): React Hook "useId" is called in function "notAHook" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use". - ╭─[rules_of_hooks.tsx:2:32] + ╭─[rules_of_hooks.tsx:3:19] 1 │ 2 │ async function notAHook() { - · ──────── + · ────┬─── + · ╰── Outer function 3 │ useId(); + · ──┬── + · ╰── Hook is called here + 4 │ } ╰──── ⚠ eslint-plugin-react-hooks(rules-of-hooks): React Hook "use" cannot be called at the top level. React Hooks must be called in a React function component or a custom React Hook function. @@ -659,11 +712,15 @@ source: crates/oxc_linter/src/tester.rs ╰──── ⚠ eslint-plugin-react-hooks(rules-of-hooks): React Hook "use" is called in function "notAComponent" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use". - ╭─[rules_of_hooks.tsx:2:26] + ╭─[rules_of_hooks.tsx:3:21] 1 │ 2 │ function notAComponent() { - · ───────────── + · ──────┬────── + · ╰── Outer function 3 │ use(promise); + · ─┬─ + · ╰── Hook is called here + 4 │ } ╰──── ⚠ eslint-plugin-react-hooks(rules-of-hooks): React Hook "use" cannot be called at the top level. React Hooks must be called in a React function component or a custom React Hook function. @@ -691,11 +748,14 @@ source: crates/oxc_linter/src/tester.rs ╰──── ⚠ eslint-plugin-react-hooks(rules-of-hooks): React Hook "useState" is called in function "Anonymous" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use". - ╭─[rules_of_hooks.tsx:2:35] + ╭─[rules_of_hooks.tsx:3:17] 1 │ 2 │ ╭─▶ const notAComponent = () => { 3 │ │ useState(); - 4 │ ╰─▶ } + · │ ────┬─── + · │ ╰── Hook is called here + 4 │ ├─▶ } + · ╰──── Outer function 5 │ ╰──── @@ -716,7 +776,9 @@ source: crates/oxc_linter/src/tester.rs ╰──── ⚠ eslint-plugin-react-hooks(rules-of-hooks): React Hook "useHook" is called in function "foo" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use". - ╭─[rules_of_hooks.tsx:1:45] + ╭─[rules_of_hooks.tsx:1:54] 1 │ const MyComponent3 = makeComponent(function foo () { useHook(); }); - · ─── + · ─┬─ ───┬─── + · │ ╰── Hook is called here + · ╰── Outer function ╰────