diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 27fa2e492..ebde285ca 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -209,6 +209,7 @@ mod react { pub mod no_unknown_property; pub mod react_in_jsx_scope; pub mod require_render_return; + pub mod rules_of_hooks; pub mod void_dom_elements_no_children; } @@ -639,6 +640,7 @@ oxc_macros::declare_all_lint_rules! { react::no_is_mounted, react::no_unknown_property, react::require_render_return, + react::rules_of_hooks, react::void_dom_elements_no_children, react_perf::jsx_no_jsx_as_prop, react_perf::jsx_no_new_array_as_prop, diff --git a/crates/oxc_linter/src/rules/react/rules_of_hooks.rs b/crates/oxc_linter/src/rules/react/rules_of_hooks.rs new file mode 100644 index 000000000..e7f23620c --- /dev/null +++ b/crates/oxc_linter/src/rules/react/rules_of_hooks.rs @@ -0,0 +1,1445 @@ +use itertools::{FoldWhile, Itertools}; +use oxc_ast::{ + ast::{ArrowFunctionExpression, Function}, + AstKind, +}; +use oxc_macros::declare_oxc_lint; +use oxc_semantic::{ + petgraph::{self, graph::NodeIndex}, + pg::neighbors_filtered_by_edge_weight, + AstNodeId, AstNodes, BasicBlockElement, EdgeType, Register, +}; +use oxc_span::Atom; + +use crate::{ + context::LintContext, + rule::Rule, + utils::{is_react_component_or_hook_name, is_react_hook}, + AstNode, +}; + +mod diagnostics { + use oxc_diagnostics::OxcDiagnostic as D; + use oxc_span::Span; + + pub(super) fn function_error(span: Span, hook_name: &str, func_name: &str) -> D { + D::error(format!( + "eslint-plugin-react-hooks(rules-of-hooks): \ + 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) + } + + pub(super) fn conditional_hook(span: Span, hook_name: &str) -> D { + D::error(format!( + "eslint-plugin-react-hooks(rules-of-hooks): \ + React Hook {hook_name:?} is called conditionally. React Hooks must be \ + called in the exact same order in every component render." + )) + .with_label(span) + } + + pub(super) fn look_hook(span: Span, hook_name: &str) -> D { + D::error(format!( + "eslint-plugin-react-hooks(rules-of-hooks): \ + React Hook {hook_name:?} 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." + )) + .with_label(span) + } + + pub(super) fn top_level_hook(span: Span, hook_name: &str) -> D { + D::error(format!( + "eslint-plugin-react-hooks(rules-of-hooks): \ + React Hook {hook_name:?} cannot be called at the top level. React Hooks \ + must be called in a React function component or a custom React \ + Hook function." + )) + .with_label(span) + } + + pub(super) fn async_component(span: Span, func_name: &str) -> D { + D::error(format!( + "eslint-plugin-react-hooks(rules-of-hooks): \ + message: `React Hook {func_name:?} cannot be called in an async function. " + )) + .with_label(span) + } + + pub(super) fn class_component(span: Span, hook_name: &str) -> D { + D::error(format!( + "eslint-plugin-react-hooks(rules-of-hooks): \ + React Hook {hook_name:?} cannot be called in a class component. React Hooks \ + must be called in a React function component or a custom React \ + Hook function." + )) + .with_label(span) + } + + pub(super) fn generic_error(span: Span, hook_name: &str) -> D { + D::error(format!( + "eslint-plugin-react-hooks(rules-of-hooks): \ + React Hook {hook_name:?} cannot be called inside a callback. React Hooks \ + must be called in a React function component or a custom React \ + Hook function." + )) + .with_label(span) + } +} + +#[derive(Debug, Default, Clone)] +pub struct RulesOfHooks; + +declare_oxc_lint!( + /// ### What it does + /// + /// This enforcecs the Rules of Hooks + /// + /// + /// + RulesOfHooks, + correctness +); + +impl Rule for RulesOfHooks { + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + let AstKind::CallExpression(call) = node.kind() else { return }; + + if !is_react_hook(&call.callee) { + return; + } + let span = call.span; + let hook_name = + call.callee_name().expect("We identify hooks using their names so it should be named."); + + let semantic = ctx.semantic(); + let nodes = semantic.nodes(); + + let Some(parent_func) = parent_func(nodes, node) else { + return ctx.diagnostic(diagnostics::top_level_hook(span, hook_name)); + }; + + // Check if our parent function is part of a class. + if matches!( + nodes.parent_kind(parent_func.id()), + Some( + AstKind::MethodDefinition(_) + | AstKind::StaticBlock(_) + | AstKind::PropertyDefinition(_) + ) + ) { + return ctx.diagnostic(diagnostics::class_component(span, hook_name)); + } + + let is_use = hook_name == "use"; + + match parent_func.kind() { + // We are in a named function that isn't a hook or component, which is illegal + AstKind::Function(Function { id: Some(id), .. }) + if !is_react_component_or_hook_name(&id.name) => + { + return ctx.diagnostic(diagnostics::function_error( + id.span, + hook_name, + id.name.as_str(), + )); + } + // Hooks can't be called from async function. + AstKind::Function(Function { id: Some(id), r#async: true, .. }) => { + return ctx.diagnostic(diagnostics::async_component(id.span, id.name.as_str())); + } + // Hooks can't be called from async arrow function. + AstKind::ArrowFunctionExpression(ArrowFunctionExpression { + span, + r#async: true, + .. + }) => { + return ctx.diagnostic(diagnostics::async_component(*span, "Anonymous")); + } + // Hooks are allowed inside of unnamed functions used as arguments. As long as they are + // not used as a callback inside of components or hooks. + AstKind::Function(Function { id: None, .. }) | AstKind::ArrowFunctionExpression(_) + if is_non_react_func_arg(nodes, parent_func.id()) => + { + // This rule doesn't apply to `use(...)`. + if !is_use && is_somewhere_inside_component_or_hook(nodes, parent_func.id()) { + ctx.diagnostic(diagnostics::generic_error(span, hook_name)); + } + return; + } + AstKind::Function(Function { span, id: None, .. }) + | AstKind::ArrowFunctionExpression(ArrowFunctionExpression { span, .. }) => { + let ident = get_declaration_identifier(nodes, parent_func.id()); + + // Hooks cannot be called inside of export default functions or used in a function + // declaration outside of a react component or hook. + // For example these are invalid: + // const notAComponent = () => { + // return () => { + // useState(); + // } + // } + // -------------- + // export default () => { + // if (isVal) { + // useState(0); + // } + // } + // -------------- + // export default function() { + // if (isVal) { + // useState(0); + // } + // } + if ident.is_some_and(|name| !is_react_component_or_hook_name(name)) + || is_export_default(nodes, parent_func.id()) + { + return ctx.diagnostic(diagnostics::function_error( + *span, + hook_name, + "Anonymous", + )); + } + } + _ => {} + } + + // `use(...)` can be called conditionally, And, + // `use(...)` can be called within a loop. + // So we don't need the following checks. + if is_use { + return; + } + + let node_cfg_ix = node.cfg_ix(); + let func_cfg_ix = parent_func.cfg_ix(); + + // there is no branch between us and our parent function + if node_cfg_ix == func_cfg_ix { + return; + } + + if !petgraph::algo::has_path_connecting( + &semantic.cfg().graph, + func_cfg_ix, + node_cfg_ix, + None, + ) { + // There should always be a control flow path between a parent and child node. + // If there is none it means we always do an early exit before reaching our hook call. + // In some cases it might mean that we are operating on an invalid `cfg` but in either + // case, It is somebody else's problem so we just return. + return; + } + + // Is this node cyclic? + if self.is_cyclic(ctx, node_cfg_ix) { + return ctx.diagnostic(diagnostics::look_hook(span, hook_name)); + } + + if self.is_conditional(ctx, func_cfg_ix, node_cfg_ix) + || self.breaks_early(ctx, func_cfg_ix, node_cfg_ix) + { + #[allow(clippy::needless_return)] + return ctx.diagnostic(diagnostics::conditional_hook(span, hook_name)); + } + } +} + +// TODO: all `dijkstra` algorithms can be merged together for better performance. +impl RulesOfHooks { + #![allow(clippy::unused_self, clippy::inline_always)] + #[inline(always)] + fn is_cyclic(&self, ctx: &LintContext, node_cfg_ix: NodeIndex) -> bool { + let graph = &ctx.semantic().cfg().graph; + petgraph::algo::dijkstra(graph, node_cfg_ix, None, |_| 0) + .into_keys() + .flat_map(|it| graph.edges_directed(it, petgraph::Direction::Outgoing)) + .any(|edge| matches!(edge.weight(), EdgeType::Backedge)) + } + + #[inline(always)] + fn is_conditional( + &self, + ctx: &LintContext, + func_cfg_ix: NodeIndex, + node_cfg_ix: NodeIndex, + ) -> bool { + let graph = &ctx.semantic().cfg().graph; + // All nodes should be reachable from our hook, Otherwise we have a conditional/branching flow. + petgraph::algo::dijkstra(graph, func_cfg_ix, Some(node_cfg_ix), |_| 0) + .into_iter() + .any(|(f, _)| !petgraph::algo::has_path_connecting(graph, f, node_cfg_ix, None)) + } + + #[inline(always)] + fn breaks_early( + &self, + ctx: &LintContext, + func_cfg_ix: NodeIndex, + node_cfg_ix: NodeIndex, + ) -> bool { + let cfg = ctx.semantic().cfg(); + neighbors_filtered_by_edge_weight( + &cfg.graph, + func_cfg_ix, + &|e| match e { + EdgeType::Normal => None, + EdgeType::Backedge | EdgeType::NewFunction => Some(State::default()), + }, + &mut |ix: &NodeIndex, mut state: State| { + if node_cfg_ix == *ix { + return (state, false); + } + + let (push, keep_walking) = cfg + .basic_block_by_index(*ix) + .iter() + .fold_while((false, true), |acc, it| match it { + BasicBlockElement::Break(_) => FoldWhile::Done((true, false)), + BasicBlockElement::Unreachable + | BasicBlockElement::Throw(_) + | BasicBlockElement::Assignment(Register::Return, _) => { + FoldWhile::Continue((acc.0, false)) + } + BasicBlockElement::Assignment(_, _) => FoldWhile::Continue(acc), + }) + .into_inner(); + + if push { + state.0.push(*ix); + } + (state, keep_walking) + }, + ) + .iter() + .flat_map(|it| it.0.iter()) + .next() + .is_some() + } +} + +#[derive(Debug, Default, Clone)] +struct State(Vec); + +fn parent_func<'a>(nodes: &'a AstNodes<'a>, node: &AstNode) -> Option<&'a AstNode<'a>> { + nodes.ancestors(node.id()).map(|id| nodes.get_node(id)).find(|it| it.kind().is_function_like()) +} + +/// Checks if the `node_id` is a callback argument, +/// And that function isn't a `React.memo` or `React.forwardRef`. +/// Returns `true` if this node is a function argument and that isn't a React special function. +/// Otherwise it would return `false`. +fn is_non_react_func_arg(nodes: &AstNodes, node_id: AstNodeId) -> bool { + let argument = match nodes.parent_node(node_id) { + Some(parent) if matches!(parent.kind(), AstKind::Argument(_)) => parent, + _ => return false, + }; + + let Some(AstKind::CallExpression(call)) = nodes.parent_kind(argument.id()) else { + return false; + }; + + // TODO make it better, might have false positives. + call.callee_name().is_some_and(|name| !matches!(name, "forwardRef" | "memo")) +} + +fn is_somewhere_inside_component_or_hook(nodes: &AstNodes, node_id: AstNodeId) -> bool { + nodes + .ancestors(node_id) + .map(|id| nodes.get_node(id)) + .filter(|node| node.kind().is_function_like()) + .map(|node| { + ( + node.id(), + match node.kind() { + AstKind::Function(func) => func.id.as_ref().map(|it| it.name.as_str()), + AstKind::ArrowFunctionExpression(_) => { + get_declaration_identifier(nodes, node.id()) + } + _ => unreachable!(), + }, + ) + }) + .any(|(ix, id)| { + id.is_some_and(|name| { + is_react_component_or_hook_name(name) || is_memo_or_forward_ref_callback(nodes, ix) + }) + }) +} + +fn get_declaration_identifier<'a>(nodes: &'a AstNodes<'a>, node_id: AstNodeId) -> Option<&str> { + nodes.ancestors(node_id).map(|id| nodes.get_node(id)).find_map(|node| { + if let AstKind::VariableDeclaration(decl) = node.kind() { + if decl.declarations.len() == 1 { + decl.declarations[0].id.get_identifier().map(Atom::as_str) + } else { + None + } + } else { + None + } + }) +} + +fn is_export_default<'a>(nodes: &'a AstNodes<'a>, node_id: AstNodeId) -> bool { + nodes + .ancestors(node_id) + .map(|id| nodes.get_node(id)) + .nth(1) + .is_some_and(|node| matches!(node.kind(), AstKind::ExportDefaultDeclaration(_))) +} + +/// # Panics +/// `node_id` should always point to a valid `Function`. +fn is_memo_or_forward_ref_callback(nodes: &AstNodes, node_id: AstNodeId) -> bool { + nodes.ancestors(node_id).map(|id| nodes.get_node(id)).any(|node| { + if let AstKind::CallExpression(call) = node.kind() { + call.callee_name().is_some_and(|name| matches!(name, "forwardRef" | "memo")) + } else { + false + } + }) +} + +#[test] +fn test() { + /// Copyright (c) Meta Platforms, Inc. and affiliates. + /// Most of these tests are sourced from the original react `eslint-plugin-react-hooks` package. + /// https://github.com/facebook/react/blob/5b903cdaa94c78e8fabb985d8daca5bd7d266323/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js#L43 + use crate::tester::Tester; + + let pass = vec![ + // Valid because components can use hooks. + " + function ComponentWithHook() { + useHook(); + } + ", + // Valid because components can use hooks. + " + function createComponentWithHook() { + return function ComponentWithHook() { + useHook(); + }; + } + ", + // Valid because hooks can use hooks. + " + function useHookWithHook() { + useHook(); + } + ", + // Valid because hooks can use hooks. + " + function createHook() { + return function useHookWithHook() { + useHook(); + } + } + ", + // Valid because components can call functions. + " + function ComponentWithNormalFunction() { + doSomething(); + } + ", + // Valid because functions can call functions. + " + function normalFunctionWithNormalFunction() { + doSomething(); + } + ", + // Valid because functions can call functions. + " + function normalFunctionWithConditionalFunction() { + if (cond) { + doSomething(); + } + } + ", + // Valid because functions can call functions. + " + function functionThatStartsWithUseButIsntAHook() { + if (cond) { + userFetch(); + } + } + ", + // Valid although unconditional return doesn't make sense and would fail other rules. + // We could make it invalid but it doesn't matter. + " + function useUnreachable() { + return; + useHook(); + } + ", + // Valid because hooks can call hooks. + " + function useHook() { useState(); } + const whatever = function useHook() { useState(); }; + const useHook1 = () => { useState(); }; + let useHook2 = () => useState(); + useHook2 = () => { useState(); }; + ({useHook: () => { useState(); }}); + ({useHook() { useState(); }}); + const {useHook3 = () => { useState(); }} = {}; + ({useHook = () => { useState(); }} = {}); + Namespace.useHook = () => { useState(); }; + ", + // Valid because hooks can call hooks. + " + function useHook() { + useHook1(); + useHook2(); + } + ", + // Valid because hooks can call hooks. + " + function createHook() { + return function useHook() { + useHook1(); + useHook2(); + }; + } + ", + // Valid because hooks can call hooks. + " + function useHook() { + useState() && a; + } + ", + // Valid because hooks can call hooks. + " + function useHook() { + return useHook1() + useHook2(); + } + ", + // Valid because hooks can call hooks. + " + function useHook() { + return useHook1(useHook2()); + } + ", + // Valid because hooks can be used in anonymous arrow-function arguments + // to forwardRef. + " + const FancyButton = React.forwardRef((props, ref) => { + useHook(); + return ; + }); + ", + // Invalid because it's dangerous and might not warn otherwise. + // This *must* be invalid. + // errors: [conditionalError('useCustomHook')], + " + const FancyButton = forwardRef(function(props, ref) { + if (props.fancy) { + useCustomHook(); + } + return ; + }); + ", + // Invalid because it's dangerous and might not warn otherwise. + // This *must* be invalid. + // errors: [conditionalError('useCustomHook')], + " + const MemoizedButton = memo(function(props) { + if (props.fancy) { + useCustomHook(); + } + return ; + }); + ", + // This is invalid because "use"-prefixed functions used in named + // functions are assumed to be hooks. + // errors: [functionError('useProbablyAHook', 'notAComponent')], + " + React.unknownFunction(function notAComponent(foo, bar) { + useProbablyAHook(bar) + }); + ", + // Invalid because it's dangerous. + // Normally, this would crash, but not if you use inline requires. + // This *must* be invalid. + // It's expected to have some false positives, but arguably + // they are confusing anyway due to the use*() convention + // already being associated with Hooks. + // errors: [ + // topLevelError('useState'), + // topLevelError('React.useCallback'), + // topLevelError('useCustomHook'), + // ], + " + useState(); + if (foo) { + const foo = React.useCallback(() => {}); + } + useCustomHook(); + ", + // Technically this is a false positive. + // We *could* make it valid (and it used to be). + // + // However, top-level Hook-like calls can be very dangerous + // in environments with inline requires because they can mask + // the runtime error by accident. + // So we prefer to disallow it despite the false positive. + // errors: [topLevelError('useBasename')], + " + const {createHistory, useBasename} = require('history-2.1.2'); + const browserHistory = useBasename(createHistory)({ + basename: '/', + }); + ", + // errors: [classError('useFeatureFlag')], + " + class ClassComponentWithFeatureFlag extends React.Component { + render() { + if (foo) { + useFeatureFlag(); + } + } + } + ", + // errors: [classError('React.useState')], + " + class ClassComponentWithHook extends React.Component { + render() { + React.useState(); + } + } + ", + // errors: [classError('useState')], + "(class {useHook = () => { useState(); }});", + // errors: [classError('useState')], + "(class {useHook() { useState(); }});", + // errors: [classError('useState')], + "(class {h = () => { useState(); }});", + // errors: [classError('useState')], + "(class {i() { useState(); }});", + // errors: [asyncComponentHookError('useState')], + " + async function AsyncComponent() { + useState(); + } + ", + " + const AsyncComponent = async () => { + useState(); + } + ", + // errors: [asyncComponentHookError('useState')], + " + async function useAsyncHook() { + useState(); + } + ", + // errors: [ + // topLevelError('Hook.use'), + // topLevelError('Hook.useState'), + // topLevelError('Hook.use42'), + // topLevelError('Hook.useHook'), + // ], + " + Hook.use(); + Hook._use(); + Hook.useState(); + Hook._useState(); + Hook.use42(); + Hook.useHook(); + Hook.use_hook(); + ", + // errors: [functionError('use', 'notAComponent')], + " + function notAComponent() { + use(promise); + } + ", + // errors: [topLevelError('use')], + " + const text = use(promise); + function App() { + return + } + ", + // errors: [classError('use')], + " + class C { + m() { + use(promise); + } + } + ", + // errors: [asyncComponentHookError('use')], + " + async function AsyncComponent() { + use(); + } + ", + // errors: [functionError('use', 'notAComponent')], + " + export const notAComponent = () => { + return () => { + useState(); + } + } + ", + // errors: [functionError('use', 'notAComponent')], + " + const notAComponent = () => { + useState(); + } + ", + // errors: [genericError('useState')], + " + export default () => { + if (isVal) { + useState(0); + } + } + ", + // errors: [genericError('useState')], + " + export default function() { + if (isVal) { + useState(0); + } + } + ", + // TODO: This should error but doesn't. + // Original rule also fails to raise this error. + // errors: [genericError('useState')], + // " + // function notAComponent() { + // return new Promise.then(() => { + // useState(); + // }); + // } + // " , + ]; + + Tester::new(RulesOfHooks::NAME, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/rules_of_hooks.snap b/crates/oxc_linter/src/snapshots/rules_of_hooks.snap new file mode 100644 index 000000000..25b1466f9 --- /dev/null +++ b/crates/oxc_linter/src/snapshots/rules_of_hooks.snap @@ -0,0 +1,596 @@ +--- +source: crates/oxc_linter/src/tester.rs +expression: rules_of_hooks +--- + × eslint-plugin-react-hooks(rules-of-hooks): React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render. + ╭─[rules_of_hooks.tsx:4:15] + 3 │ if (a) return; + 4 │ useState(); + · ────────── + 5 │ } + ╰──── + + × eslint-plugin-react-hooks(rules-of-hooks): React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render. + ╭─[rules_of_hooks.tsx:9:15] + 8 │ } + 9 │ useState(); + · ────────── + 10 │ } + ╰──── + + × eslint-plugin-react-hooks(rules-of-hooks): React Hook "useHook" is called conditionally. React Hooks must be called in the exact same order in every component render. + ╭─[rules_of_hooks.tsx:7:15] + 6 │ + 7 │ useHook(); + · ───────── + 8 │ } + ╰──── + + × eslint-plugin-react-hooks(rules-of-hooks): React Hook "useConditionalHook" is called conditionally. React Hooks must be called in the exact same order in every component render. + ╭─[rules_of_hooks.tsx:4:18] + 3 │ if (cond) { + 4 │ useConditionalHook(); + · ──────────────────── + 5 │ } + ╰──── + + × 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. + ╭─[rules_of_hooks.tsx:2:13] + 1 │ + 2 │ Hook.useState(); + · ─────────────── + 3 │ Hook._useState(); + ╰──── + + × eslint-plugin-react-hooks(rules-of-hooks): React Hook "useHook" cannot be called at the top level. React Hooks must be called in a React function component or a custom React Hook function. + ╭─[rules_of_hooks.tsx:5:13] + 4 │ Hook.use42(); + 5 │ Hook.useHook(); + · ────────────── + 6 │ Hook.use_hook(); + ╰──── + + × eslint-plugin-react-hooks(rules-of-hooks): React Hook "useHook" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function. + ╭─[rules_of_hooks.tsx:4:22] + 3 │ m() { + 4 │ This.useHook(); + · ────────────── + 5 │ Super.useHook(); + ╰──── + + × eslint-plugin-react-hooks(rules-of-hooks): React Hook "useHook" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function. + ╭─[rules_of_hooks.tsx:5:22] + 4 │ This.useHook(); + 5 │ Super.useHook(); + · ─────────────── + 6 │ } + ╰──── + + × eslint-plugin-react-hooks(rules-of-hooks): React Hook "useFeatureFlag" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function. + ╭─[rules_of_hooks.tsx:5:25] + 4 │ if (cond) { + 5 │ FooStore.useFeatureFlag(); + · ───────────────────────── + 6 │ } + ╰──── + + × eslint-plugin-react-hooks(rules-of-hooks): React Hook "useConditionalHook" is called conditionally. React Hooks must be called in the exact same order in every component render. + ╭─[rules_of_hooks.tsx:4:21] + 3 │ if (cond) { + 4 │ Namespace.useConditionalHook(); + · ────────────────────────────── + 5 │ } + ╰──── + + × eslint-plugin-react-hooks(rules-of-hooks): React Hook "useConditionalHook" is called conditionally. React Hooks must be called in the exact same order in every component render. + ╭─[rules_of_hooks.tsx:5:29] + 4 │ if (cond) { + 5 │ useConditionalHook(); + · ──────────────────── + 6 │ } + ╰──── + + × eslint-plugin-react-hooks(rules-of-hooks): React Hook "useConditionalHook" is called conditionally. React Hooks must be called in the exact same order in every component render. + ╭─[rules_of_hooks.tsx:4:25] + 3 │ if (cond) { + 4 │ useConditionalHook(); + · ──────────────────── + 5 │ } + ╰──── + + × eslint-plugin-react-hooks(rules-of-hooks): React Hook "useConditionalHook" is called conditionally. React Hooks must be called in the exact same order in every component render. + ╭─[rules_of_hooks.tsx:5:29] + 4 │ if (cond) { + 5 │ useConditionalHook(); + · ──────────────────── + 6 │ } + ╰──── + + × eslint-plugin-react-hooks(rules-of-hooks): React Hook "useTernaryHook" is called conditionally. React Hooks must be called in the exact same order in every component render. + ╭─[rules_of_hooks.tsx:3:28] + 2 │ function ComponentWithTernaryHook() { + 3 │ cond ? useTernaryHook() : null; + · ──────────────── + 4 │ } + ╰──── + + × eslint-plugin-react-hooks(rules-of-hooks): React Hook "useHookInsideCallback" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function. + ╭─[rules_of_hooks.tsx:4:25] + 3 │ useEffect(() => { + 4 │ useHookInsideCallback(); + · ─────────────────────── + 5 │ }); + ╰──── + + × eslint-plugin-react-hooks(rules-of-hooks): React Hook "useHookInsideCallback" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function. + ╭─[rules_of_hooks.tsx:5:29] + 4 │ useEffect(() => { + 5 │ useHookInsideCallback(); + · ─────────────────────── + 6 │ }); + ╰──── + + × eslint-plugin-react-hooks(rules-of-hooks): React Hook "useHookInsideCallback" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function. + ╭─[rules_of_hooks.tsx:4:25] + 3 │ useEffect(() => { + 4 │ useHookInsideCallback(); + · ─────────────────────── + 5 │ }); + ╰──── + + × eslint-plugin-react-hooks(rules-of-hooks): React Hook "useHookInsideCallback" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function. + ╭─[rules_of_hooks.tsx:4:25] + 3 │ useEffect(() => { + 4 │ useHookInsideCallback(); + · ─────────────────────── + 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:3:30] + 2 │ function ComponentWithHookInsideCallback() { + 3 │ function handleClick() { + · ─────────── + 4 │ useState(); + ╰──── + + × 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] + 3 │ return function ComponentWithHookInsideCallback() { + 4 │ function handleClick() { + · ─────────── + 5 │ useState(); + ╰──── + + × 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. + ╭─[rules_of_hooks.tsx:4:25] + 3 │ while (cond) { + 4 │ useHookInsideLoop(); + · ─────────────────── + 5 │ } + ╰──── + + × 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] + 1 │ + 2 │ function renderItem() { + · ────────── + 3 │ useState(); + ╰──── + + × 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] + 1 │ + 2 │ function normalFunctionWithHook() { + · ────────────────────── + 3 │ useHookInsideNormalFunction(); + ╰──── + + × 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] + 1 │ + 2 │ function _normalFunctionWithHook() { + · ─────────────────────── + 3 │ useHookInsideNormalFunction(); + ╰──── + + × 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] + 4 │ } + 5 │ function _useNotAHook() { + · ──────────── + 6 │ useHookInsideNormalFunction(); + ╰──── + + × 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] + 1 │ + 2 │ function normalFunctionWithConditionalHook() { + · ───────────────────────────────── + 3 │ if (cond) { + ╰──── + + × 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. + ╭─[rules_of_hooks.tsx:4:25] + 3 │ while (a) { + 4 │ useHook1(); + · ────────── + 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. + ╭─[rules_of_hooks.tsx:6:25] + 5 │ if (b) return; + 6 │ useHook2(); + · ────────── + 7 │ } + ╰──── + + × eslint-plugin-react-hooks(rules-of-hooks): React Hook "useHook3" 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:9:25] + 8 │ while (c) { + 9 │ useHook3(); + · ────────── + 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. + ╭─[rules_of_hooks.tsx:11:25] + 10 │ if (d) return; + 11 │ useHook4(); + · ────────── + 12 │ } + ╰──── + + × 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. + ╭─[rules_of_hooks.tsx:4:21] + 3 │ while (a) { + 4 │ useHook1(); + · ────────── + 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. + ╭─[rules_of_hooks.tsx:6:21] + 5 │ if (b) continue; + 6 │ useHook2(); + · ────────── + 7 │ } + ╰──── + + × eslint-plugin-react-hooks(rules-of-hooks): React Hook "useHook" is called conditionally. React Hooks must be called in the exact same order in every component render. + ╭─[rules_of_hooks.tsx:5:25] + 4 │ if (a) break label; + 5 │ useHook(); + · ───────── + 6 │ } + ╰──── + + × 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] + 1 │ + 2 │ function a() { useState(); } + · ─ + 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] + 2 │ function a() { useState(); } + 3 │ const whatever = function b() { useState(); }; + · ─ + 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] + 3 │ const whatever = function b() { useState(); }; + 4 │ const c = () => { useState(); }; + · ───────────────────── + 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] + 4 │ const c = () => { useState(); }; + 5 │ let d = () => useState(); + · ──────────────── + 6 │ e = () => { useState(); }; + ╰──── + + × eslint-plugin-react-hooks(rules-of-hooks): React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render. + ╭─[rules_of_hooks.tsx:4:21] + 3 │ if (a) return; + 4 │ useState(); + · ────────── + 5 │ } + ╰──── + + × eslint-plugin-react-hooks(rules-of-hooks): React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render. + ╭─[rules_of_hooks.tsx:9:21] + 8 │ } + 9 │ useState(); + · ────────── + 10 │ } + ╰──── + + × eslint-plugin-react-hooks(rules-of-hooks): React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render. + ╭─[rules_of_hooks.tsx:9:21] + 8 │ if (a) return; + 9 │ useState(); + · ────────── + 10 │ } + ╰──── + + × eslint-plugin-react-hooks(rules-of-hooks): React Hook "useHook1" is called conditionally. React Hooks must be called in the exact same order in every component render. + ╭─[rules_of_hooks.tsx:3:26] + 2 │ function useHook() { + 3 │ a && useHook1(); + · ────────── + 4 │ b && useHook2(); + ╰──── + + × 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. + ╭─[rules_of_hooks.tsx:4:26] + 3 │ a && useHook1(); + 4 │ b && useHook2(); + · ────────── + 5 │ } + ╰──── + + × eslint-plugin-react-hooks(rules-of-hooks): React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render. + ╭─[rules_of_hooks.tsx:5:25] + 4 │ f(); + 5 │ useState(); + · ────────── + 6 │ } catch {} + ╰──── + + × eslint-plugin-react-hooks(rules-of-hooks): React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render. + ╭─[rules_of_hooks.tsx:3:39] + 2 │ function useHook({ bar }) { + 3 │ let foo1 = bar && useState(); + · ────────── + 4 │ let foo2 = bar || useState(); + ╰──── + + × eslint-plugin-react-hooks(rules-of-hooks): React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render. + ╭─[rules_of_hooks.tsx:4:39] + 3 │ let foo1 = bar && useState(); + 4 │ let foo2 = bar || useState(); + · ────────── + 5 │ let foo3 = bar ?? useState(); + ╰──── + + × eslint-plugin-react-hooks(rules-of-hooks): React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render. + ╭─[rules_of_hooks.tsx:5:39] + 4 │ let foo2 = bar || useState(); + 5 │ let foo3 = bar ?? useState(); + · ────────── + 6 │ } + ╰──── + + × eslint-plugin-react-hooks(rules-of-hooks): React Hook "useCustomHook" is called conditionally. React Hooks must be called in the exact same order in every component render. + ╭─[rules_of_hooks.tsx:4:25] + 3 │ if (props.fancy) { + 4 │ useCustomHook(); + · ─────────────── + 5 │ } + ╰──── + + × eslint-plugin-react-hooks(rules-of-hooks): React Hook "useCustomHook" is called conditionally. React Hooks must be called in the exact same order in every component render. + ╭─[rules_of_hooks.tsx:4:25] + 3 │ if (props.fancy) { + 4 │ useCustomHook(); + · ─────────────── + 5 │ } + ╰──── + + × eslint-plugin-react-hooks(rules-of-hooks): React Hook "useCustomHook" is called conditionally. React Hooks must be called in the exact same order in every component render. + ╭─[rules_of_hooks.tsx:4:25] + 3 │ if (props.fancy) { + 4 │ useCustomHook(); + · ─────────────── + 5 │ } + ╰──── + + × 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] + 1 │ + 2 │ React.unknownFunction(function notAComponent(foo, bar) { + · ───────────── + 3 │ useProbablyAHook(bar) + ╰──── + + × 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. + ╭─[rules_of_hooks.tsx:2:13] + 1 │ + 2 │ useState(); + · ────────── + 3 │ if (foo) { + ╰──── + + × eslint-plugin-react-hooks(rules-of-hooks): React Hook "useCallback" cannot be called at the top level. React Hooks must be called in a React function component or a custom React Hook function. + ╭─[rules_of_hooks.tsx:4:29] + 3 │ if (foo) { + 4 │ const foo = React.useCallback(() => {}); + · ─────────────────────────── + 5 │ } + ╰──── + + × eslint-plugin-react-hooks(rules-of-hooks): React Hook "useCustomHook" cannot be called at the top level. React Hooks must be called in a React function component or a custom React Hook function. + ╭─[rules_of_hooks.tsx:6:13] + 5 │ } + 6 │ useCustomHook(); + · ─────────────── + 7 │ + ╰──── + + × eslint-plugin-react-hooks(rules-of-hooks): React Hook "useBasename" cannot be called at the top level. React Hooks must be called in a React function component or a custom React Hook function. + ╭─[rules_of_hooks.tsx:3:36] + 2 │ const {createHistory, useBasename} = require('history-2.1.2'); + 3 │ const browserHistory = useBasename(createHistory)({ + · ────────────────────────── + 4 │ basename: '/', + ╰──── + + × eslint-plugin-react-hooks(rules-of-hooks): React Hook "useFeatureFlag" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function. + ╭─[rules_of_hooks.tsx:5:29] + 4 │ if (foo) { + 5 │ useFeatureFlag(); + · ──────────────── + 6 │ } + ╰──── + + × eslint-plugin-react-hooks(rules-of-hooks): React Hook "useState" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function. + ╭─[rules_of_hooks.tsx:4:25] + 3 │ render() { + 4 │ React.useState(); + · ──────────────── + 5 │ } + ╰──── + + × eslint-plugin-react-hooks(rules-of-hooks): React Hook "useState" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function. + ╭─[rules_of_hooks.tsx:1:27] + 1 │ (class {useHook = () => { useState(); }}); + · ────────── + ╰──── + + × eslint-plugin-react-hooks(rules-of-hooks): React Hook "useState" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function. + ╭─[rules_of_hooks.tsx:1:21] + 1 │ (class {useHook() { useState(); }}); + · ────────── + ╰──── + + × eslint-plugin-react-hooks(rules-of-hooks): React Hook "useState" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function. + ╭─[rules_of_hooks.tsx:1:21] + 1 │ (class {h = () => { useState(); }}); + · ────────── + ╰──── + + × eslint-plugin-react-hooks(rules-of-hooks): React Hook "useState" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function. + ╭─[rules_of_hooks.tsx:1:15] + 1 │ (class {i() { useState(); }}); + · ────────── + ╰──── + + × eslint-plugin-react-hooks(rules-of-hooks): message: `React Hook "AsyncComponent" cannot be called in an async function. + ╭─[rules_of_hooks.tsx:2:32] + 1 │ + 2 │ async function AsyncComponent() { + · ────────────── + 3 │ useState(); + ╰──── + + × eslint-plugin-react-hooks(rules-of-hooks): message: `React Hook "Anonymous" cannot be called in an async function. + ╭─[rules_of_hooks.tsx:2:40] + 1 │ + 2 │ ╭─▶ const AsyncComponent = async () => { + 3 │ │ useState(); + 4 │ ╰─▶ } + 5 │ + ╰──── + + × eslint-plugin-react-hooks(rules-of-hooks): message: `React Hook "useAsyncHook" cannot be called in an async function. + ╭─[rules_of_hooks.tsx:2:32] + 1 │ + 2 │ async function useAsyncHook() { + · ──────────── + 3 │ useState(); + ╰──── + + × 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. + ╭─[rules_of_hooks.tsx:2:13] + 1 │ + 2 │ Hook.use(); + · ────────── + 3 │ Hook._use(); + ╰──── + + × 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. + ╭─[rules_of_hooks.tsx:4:13] + 3 │ Hook._use(); + 4 │ Hook.useState(); + · ─────────────── + 5 │ Hook._useState(); + ╰──── + + × eslint-plugin-react-hooks(rules-of-hooks): React Hook "useHook" cannot be called at the top level. React Hooks must be called in a React function component or a custom React Hook function. + ╭─[rules_of_hooks.tsx:7:13] + 6 │ Hook.use42(); + 7 │ Hook.useHook(); + · ────────────── + 8 │ Hook.use_hook(); + ╰──── + + × 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] + 1 │ + 2 │ function notAComponent() { + · ───────────── + 3 │ use(promise); + ╰──── + + × 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. + ╭─[rules_of_hooks.tsx:2:26] + 1 │ + 2 │ const text = use(promise); + · ──────────── + 3 │ function App() { + ╰──── + + × eslint-plugin-react-hooks(rules-of-hooks): React Hook "use" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function. + ╭─[rules_of_hooks.tsx:4:21] + 3 │ m() { + 4 │ use(promise); + · ──────────── + 5 │ } + ╰──── + + × eslint-plugin-react-hooks(rules-of-hooks): message: `React Hook "AsyncComponent" cannot be called in an async function. + ╭─[rules_of_hooks.tsx:2:28] + 1 │ + 2 │ async function AsyncComponent() { + · ────────────── + 3 │ use(); + ╰──── + + × 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:3:24] + 2 │ export const notAComponent = () => { + 3 │ ╭─▶ return () => { + 4 │ │ useState(); + 5 │ ╰─▶ } + 6 │ } + ╰──── + + × 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] + 1 │ + 2 │ ╭─▶ const notAComponent = () => { + 3 │ │ useState(); + 4 │ ╰─▶ } + 5 │ + ╰──── + + × 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:28] + 1 │ + 2 │ ╭─▶ export default () => { + 3 │ │ if (isVal) { + 4 │ │ useState(0); + 5 │ │ } + 6 │ ╰─▶ } + 7 │ + ╰──── + + × 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:28] + 1 │ + 2 │ ╭─▶ export default function() { + 3 │ │ if (isVal) { + 4 │ │ useState(0); + 5 │ │ } + 6 │ ╰─▶ } + 7 │ + ╰──── diff --git a/crates/oxc_linter/src/utils/react.rs b/crates/oxc_linter/src/utils/react.rs index df31a4bb4..afbcb35eb 100644 --- a/crates/oxc_linter/src/utils/react.rs +++ b/crates/oxc_linter/src/utils/react.rs @@ -1,9 +1,9 @@ use oxc_ast::{ ast::{ CallExpression, Expression, JSXAttributeItem, JSXAttributeName, JSXAttributeValue, - JSXChild, JSXElement, JSXElementName, JSXExpression, JSXOpeningElement, + JSXChild, JSXElement, JSXElementName, JSXExpression, JSXOpeningElement, MemberExpression, }, - AstKind, + match_member_expression, AstKind, }; use oxc_semantic::{AstNode, SymbolFlags}; @@ -273,3 +273,56 @@ pub fn parse_jsx_value(value: &JSXAttributeValue) -> Result { _ => Err(()), } } + +/// Checks whether the `name` follows the official conventions of React Hooks. +/// +/// Identifies `use(...)` as a valid hook. +/// +/// Hook names must start with use followed by a capital letter, +/// like useState (built-in) or useOnlineStatus (custom). +pub fn is_react_hook_name(name: &str) -> bool { + name.starts_with("use") && name.chars().nth(3).map_or(true, char::is_uppercase) + // uncomment this check if react decided to drop the idea of `use` hook. + // It is currently in `Canary` builds. + // name.starts_with("use") && name.chars().nth(3).is_some_and(char::is_uppercase) +} + +/// Checks whether the `name` follows the official conventions of React Hooks. +/// +/// Identifies `use(...)` as a valid hook. +/// +/// Hook names must start with use followed by a capital letter, +/// like useState (built-in) or useOnlineStatus (custom). +pub fn is_react_hook(expr: &Expression) -> bool { + match expr { + match_member_expression!(Expression) => { + // SAFETY: We already have checked that `expr` is a member expression using the + // `match_member_expression` macro. + #[allow(unsafe_code)] + let expr = unsafe { expr.as_member_expression().unwrap_unchecked() }; + let MemberExpression::StaticMemberExpression(static_expr) = expr else { return false }; + let is_valid_namespace = match &static_expr.object { + Expression::Identifier(ident) => { + ident.name.chars().next().is_some_and(char::is_uppercase) + } + Expression::ThisExpression(_) | Expression::Super(_) => false, + _ => true, + }; + is_valid_namespace && expr.static_property_name().is_some_and(is_react_hook_name) + } + Expression::Identifier(ident) => is_react_hook_name(ident.name.as_str()), + _ => false, + } +} + +/// Checks if the node is a React component name. React component names must +/// always start with an uppercase letter. +pub fn is_react_component_name(name: &str) -> bool { + name.chars().next().is_some_and(|c| c.is_ascii_uppercase()) +} + +/// Checks if the node is a React component name or React hook, +/// `is_react_component_name`, `is_react_hook_name` +pub fn is_react_component_or_hook_name(name: &str) -> bool { + is_react_component_name(name) || is_react_hook_name(name) +} diff --git a/crates/oxc_semantic/src/pg.rs b/crates/oxc_semantic/src/pg.rs index e8aa74c38..e02749b24 100644 --- a/crates/oxc_semantic/src/pg.rs +++ b/crates/oxc_semantic/src/pg.rs @@ -1,13 +1,7 @@ use petgraph::{stable_graph::NodeIndex, visit::EdgeRef, Direction, Graph}; /// # Panics -pub fn neighbors_filtered_by_edge_weight< - State: Default + Copy + Clone, - NodeWeight, - EdgeWeight, - F, - G, ->( +pub fn neighbors_filtered_by_edge_weight( graph: &Graph, node: NodeIndex, edge_filter: &F, @@ -37,11 +31,11 @@ where } else { let opposite_dir_of_edge_graph_ix = edge.target(); let (new_state, keep_walking_this_path) = - visitor(&opposite_dir_of_edge_graph_ix, state); + visitor(&opposite_dir_of_edge_graph_ix, state.clone()); if keep_walking_this_path { - q.push((opposite_dir_of_edge_graph_ix, new_state)); + q.push((opposite_dir_of_edge_graph_ix, new_state.clone())); } else { - final_states.push(new_state); + final_states.push(new_state.clone()); } edges += 1; }