diff --git a/CHANGELOG.md b/CHANGELOG.md index a472203deb8..8a373752731 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -237,6 +237,10 @@ multiple files: The rule no longer reports `This constructor calls super() in a loop` when using nested if statements in a constructor. +- Fix [useHookAtTopLevel](https://docs.rome.tools/lint/rules/usehookattoplevel/) 's false positive diagnostics ([#4637](https://github.com/rome/tools/issues/4637)) + + The rule no longer reports false positive diagnostics when accessing properties directly from a hook and calling a hook inside function arguments. + ### Parser ### VSCode diff --git a/crates/rome_js_analyze/src/semantic_analyzers/nursery/use_hook_at_top_level.rs b/crates/rome_js_analyze/src/semantic_analyzers/nursery/use_hook_at_top_level.rs index 7aa571bfcff..8ced07703f8 100644 --- a/crates/rome_js_analyze/src/semantic_analyzers/nursery/use_hook_at_top_level.rs +++ b/crates/rome_js_analyze/src/semantic_analyzers/nursery/use_hook_at_top_level.rs @@ -84,6 +84,9 @@ fn enclosing_function_if_call_is_at_top_level(call: &JsCallExpression) -> Option | JsSyntaxKind::JS_EXPRESSION_STATEMENT | JsSyntaxKind::JS_RETURN_STATEMENT | JsSyntaxKind::JS_CALL_EXPRESSION + | JsSyntaxKind::JS_CALL_ARGUMENT_LIST + | JsSyntaxKind::JS_CALL_ARGUMENTS + | JsSyntaxKind::JS_STATIC_MEMBER_EXPRESSION | JsSyntaxKind::JS_INITIALIZER_CLAUSE | JsSyntaxKind::JS_VARIABLE_DECLARATOR | JsSyntaxKind::JS_VARIABLE_DECLARATOR_LIST diff --git a/crates/rome_js_analyze/tests/specs/nursery/useHookAtTopLevel/invalid.js b/crates/rome_js_analyze/tests/specs/nursery/useHookAtTopLevel/invalid.js index c52adc0dd8a..a18ff75c6ac 100644 --- a/crates/rome_js_analyze/tests/specs/nursery/useHookAtTopLevel/invalid.js +++ b/crates/rome_js_analyze/tests/specs/nursery/useHookAtTopLevel/invalid.js @@ -77,3 +77,16 @@ const Component7 = () => { Component6(); } }; + +const Component8 = () => { + if (a == 1) { + useRef().value; + } + + const [_val, _setter] = useState(a ? useMemo('hello') : null); +}; + +const Component9 = () => { + a ? useEffect() : null; + a ?? useEffect(); +}; diff --git a/crates/rome_js_analyze/tests/specs/nursery/useHookAtTopLevel/invalid.js.snap b/crates/rome_js_analyze/tests/specs/nursery/useHookAtTopLevel/invalid.js.snap index 6d28080b94b..f9d84862361 100644 --- a/crates/rome_js_analyze/tests/specs/nursery/useHookAtTopLevel/invalid.js.snap +++ b/crates/rome_js_analyze/tests/specs/nursery/useHookAtTopLevel/invalid.js.snap @@ -84,6 +84,19 @@ const Component7 = () => { } }; +const Component8 = () => { + if (a == 1) { + useRef().value; + } + + const [_val, _setter] = useState(a ? useMemo('hello') : null); +}; + +const Component9 = () => { + a ? useEffect() : null; + a ?? useEffect(); +}; + ``` # Diagnostics @@ -378,4 +391,79 @@ invalid.js:72:5 lint/nursery/useHookAtTopLevel ━━━━━━━━━━━ ``` +``` +invalid.js:83:9 lint/nursery/useHookAtTopLevel ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This hook is being called conditionally, but all hooks must be called in the exact same order in every component render. + + 81 │ const Component8 = () => { + 82 │ if (a == 1) { + > 83 │ useRef().value; + │ ^^^^^^ + 84 │ } + 85 │ + + i For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order. + + i See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level + + +``` + +``` +invalid.js:86:42 lint/nursery/useHookAtTopLevel ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This hook is being called conditionally, but all hooks must be called in the exact same order in every component render. + + 84 │ } + 85 │ + > 86 │ const [_val, _setter] = useState(a ? useMemo('hello') : null); + │ ^^^^^^^ + 87 │ }; + 88 │ + + i For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order. + + i See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level + + +``` + +``` +invalid.js:90:9 lint/nursery/useHookAtTopLevel ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This hook is being called conditionally, but all hooks must be called in the exact same order in every component render. + + 89 │ const Component9 = () => { + > 90 │ a ? useEffect() : null; + │ ^^^^^^^^^ + 91 │ a ?? useEffect(); + 92 │ }; + + i For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order. + + i See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level + + +``` + +``` +invalid.js:91:10 lint/nursery/useHookAtTopLevel ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This hook is being called conditionally, but all hooks must be called in the exact same order in every component render. + + 89 │ const Component9 = () => { + 90 │ a ? useEffect() : null; + > 91 │ a ?? useEffect(); + │ ^^^^^^^^^ + 92 │ }; + 93 │ + + i For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order. + + i See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level + + +``` + diff --git a/crates/rome_js_analyze/tests/specs/nursery/useHookAtTopLevel/valid.js b/crates/rome_js_analyze/tests/specs/nursery/useHookAtTopLevel/valid.js index 671bc3f3efb..e2a673cf530 100644 --- a/crates/rome_js_analyze/tests/specs/nursery/useHookAtTopLevel/valid.js +++ b/crates/rome_js_analyze/tests/specs/nursery/useHookAtTopLevel/valid.js @@ -35,3 +35,8 @@ export default function Component5() { const Component6 = () => { return useState(); }; + +const Component7 = () => { + const value = useRef().value; + const [_val, _setter] = useState(useMemo('hello')); +} diff --git a/crates/rome_js_analyze/tests/specs/nursery/useHookAtTopLevel/valid.js.snap b/crates/rome_js_analyze/tests/specs/nursery/useHookAtTopLevel/valid.js.snap index 44d3e96949c..351fa107eea 100644 --- a/crates/rome_js_analyze/tests/specs/nursery/useHookAtTopLevel/valid.js.snap +++ b/crates/rome_js_analyze/tests/specs/nursery/useHookAtTopLevel/valid.js.snap @@ -42,6 +42,11 @@ const Component6 = () => { return useState(); }; +const Component7 = () => { + const value = useRef().value; + const [_val, _setter] = useState(useMemo('hello')); +} + ```