fix(linter/react): fix loop hooks false positives. (#3297)

It is a temporary fix for false positives like
[this](https://github.com/oxc-project/oxc/issues/3257#issuecomment-2110199442).
Uses the node's ancestors for now instead of the cfg.
This commit is contained in:
Ali Rezvani 2024-05-16 06:33:54 +03:30 committed by GitHub
parent cc1882d221
commit d46538ed68
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 52 additions and 17 deletions

View file

@ -42,7 +42,7 @@ mod diagnostics {
.with_label(span) .with_label(span)
} }
pub(super) fn look_hook(span: Span, hook_name: &str) -> D { pub(super) fn loop_hook(span: Span, hook_name: &str) -> D {
D::error(format!( D::error(format!(
"eslint-plugin-react-hooks(rules-of-hooks): \ "eslint-plugin-react-hooks(rules-of-hooks): \
React Hook {hook_name:?} may be executed more than once. Possibly \ React Hook {hook_name:?} may be executed more than once. Possibly \
@ -237,8 +237,8 @@ impl Rule for RulesOfHooks {
} }
// Is this node cyclic? // Is this node cyclic?
if self.is_cyclic(ctx, node_cfg_ix) { if self.is_cyclic(ctx, node, parent_func) {
return ctx.diagnostic(diagnostics::look_hook(span, hook_name)); return ctx.diagnostic(diagnostics::loop_hook(span, hook_name));
} }
if self.is_conditional(ctx, func_cfg_ix, node_cfg_ix) if self.is_conditional(ctx, func_cfg_ix, node_cfg_ix)
@ -254,12 +254,19 @@ impl Rule for RulesOfHooks {
impl RulesOfHooks { impl RulesOfHooks {
#![allow(clippy::unused_self, clippy::inline_always)] #![allow(clippy::unused_self, clippy::inline_always)]
#[inline(always)] #[inline(always)]
fn is_cyclic(&self, ctx: &LintContext, node_cfg_ix: NodeIndex) -> bool { fn is_cyclic(&self, ctx: &LintContext, node: &AstNode, func: &AstNode) -> bool {
let graph = &ctx.semantic().cfg().graph; // TODO: use cfg instead
petgraph::algo::dijkstra(graph, node_cfg_ix, None, |_| 0) ctx.nodes().ancestors(node.id()).take_while(|id| *id != func.id()).any(|id| {
.into_keys() let maybe_loop = ctx.nodes().kind(id);
.flat_map(|it| graph.edges_directed(it, petgraph::Direction::Outgoing)) matches! {
.any(|edge| matches!(edge.weight(), EdgeType::Backedge)) maybe_loop,
| AstKind::DoWhileStatement(_)
| AstKind::ForInStatement(_)
| AstKind::ForOfStatement(_)
| AstKind::ForStatement(_)
| AstKind::WhileStatement(_)
}
})
} }
#[inline(always)] #[inline(always)]
@ -832,12 +839,40 @@ fn test() {
} }
", ",
" "
function useLabeledBlock() { function useLabeledBlock() {
label: { label: {
useHook(); useHook();
if (a) break label; if (a) break label;
}
} }
}
",
"
export const FalsePositive = ({ editor, anchorElem, isLink, linkNodeUrl, close }: Props) => {
// This custom hook invocation seems to trigger false positives below
const [state, setState] = useCustomHook<State>({
inputLinkUrl: linkNodeUrl ?? '',
editable: !isLink,
lastLinkUrl: '',
lastSelection: null
});
const [someThing, setSomeThing] = useState(true);
const onEdit = useCallback(() => setSomeThing(false), [inputLinkUrl, setSomeThing]);
const updateLinkEditor = useCallback(() => {
const rootElement = editor.getRootElement();
if (nativeSelection.anchorNode === rootElement) {
let inner = rootElement;
while (inner.firstElementChild !== null) {
inner = inner.firstElementChild as HTMLElement;
}
}
}, [anchorElem, editor, setSomeThing]);
return <div>test</div>;
};
", ",
]; ];

View file

@ -218,7 +218,7 @@ expression: rules_of_hooks
5 │ if (b) return; 5 │ if (b) return;
╰──── ╰────
× eslint-plugin-react-hooks(rules-of-hooks): React Hook "useHook2" is called conditionally. React Hooks must be called in the exact same order in every component render. × eslint-plugin-react-hooks(rules-of-hooks): React Hook "useHook2" 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.
╭─[rules_of_hooks.tsx:6:25] ╭─[rules_of_hooks.tsx:6:25]
5 │ if (b) return; 5 │ if (b) return;
6 │ useHook2(); 6 │ useHook2();
@ -234,7 +234,7 @@ expression: rules_of_hooks
10 │ if (d) return; 10 │ if (d) return;
╰──── ╰────
× eslint-plugin-react-hooks(rules-of-hooks): React Hook "useHook4" is called conditionally. React Hooks must be called in the exact same order in every component render. × eslint-plugin-react-hooks(rules-of-hooks): React Hook "useHook4" 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.
╭─[rules_of_hooks.tsx:11:25] ╭─[rules_of_hooks.tsx:11:25]
10 │ if (d) return; 10 │ if (d) return;
11 │ useHook4(); 11 │ useHook4();
@ -250,7 +250,7 @@ expression: rules_of_hooks
5 │ if (b) continue; 5 │ if (b) continue;
╰──── ╰────
× eslint-plugin-react-hooks(rules-of-hooks): React Hook "useHook2" is called conditionally. React Hooks must be called in the exact same order in every component render. × eslint-plugin-react-hooks(rules-of-hooks): React Hook "useHook2" 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.
╭─[rules_of_hooks.tsx:6:21] ╭─[rules_of_hooks.tsx:6:21]
5 │ if (b) continue; 5 │ if (b) continue;
6 │ useHook2(); 6 │ useHook2();