diff --git a/crates/oxc_ast/src/ast/js.rs b/crates/oxc_ast/src/ast/js.rs index 9ce226294..9cb8b2cca 100644 --- a/crates/oxc_ast/src/ast/js.rs +++ b/crates/oxc_ast/src/ast/js.rs @@ -1135,6 +1135,12 @@ pub enum AssignmentTarget<'a> { } } +impl<'a> AssignmentTarget<'a> { + pub fn get_identifier(&self) -> Option<&str> { + self.as_simple_assignment_target().and_then(|it| it.get_identifier()) + } +} + /// Macro for matching `AssignmentTarget`'s variants. /// Includes `SimpleAssignmentTarget`'s and `AssignmentTargetPattern`'s variants. #[macro_export] @@ -1197,6 +1203,14 @@ macro_rules! match_simple_assignment_target { pub use match_simple_assignment_target; impl<'a> SimpleAssignmentTarget<'a> { + pub fn get_identifier(&self) -> Option<&str> { + match self { + Self::AssignmentTargetIdentifier(ident) => Some(ident.name.as_str()), + match_member_expression!(Self) => self.to_member_expression().static_property_name(), + _ => None, + } + } + pub fn get_expression(&self) -> Option<&Expression<'a>> { match self { Self::TSAsExpression(expr) => Some(&expr.expression), 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 568961c50..4bc8d49a1 100644 --- a/crates/oxc_linter/src/rules/react/rules_of_hooks.rs +++ b/crates/oxc_linter/src/rules/react/rules_of_hooks.rs @@ -9,7 +9,8 @@ use oxc_semantic::{ pg::neighbors_filtered_by_edge_weight, AstNodeId, AstNodes, BasicBlockElement, EdgeType, Register, }; -use oxc_span::Atom; +use oxc_span::{Atom, CompactStr}; +use oxc_syntax::operator::AssignmentOperator; use crate::{ context::LintContext, @@ -195,7 +196,7 @@ impl Rule for RulesOfHooks { // useState(0); // } // } - if ident.is_some_and(|name| !is_react_component_or_hook_name(name)) + if ident.is_some_and(|name| !is_react_component_or_hook_name(name.as_str())) || is_export_default(nodes, parent_func.id()) { return ctx.diagnostic(diagnostics::function_error( @@ -368,7 +369,7 @@ fn is_somewhere_inside_component_or_hook(nodes: &AstNodes, node_id: AstNodeId) - ( node.id(), match node.kind() { - AstKind::Function(func) => func.id.as_ref().map(|it| it.name.as_str()), + AstKind::Function(func) => func.id.as_ref().map(|it| it.name.to_compact_str()), AstKind::ArrowFunctionExpression(_) => { get_declaration_identifier(nodes, node.id()) } @@ -378,21 +379,37 @@ fn is_somewhere_inside_component_or_hook(nodes: &AstNodes, node_id: AstNodeId) - }) .any(|(ix, id)| { id.is_some_and(|name| { - is_react_component_or_hook_name(name) || is_memo_or_forward_ref_callback(nodes, ix) + is_react_component_or_hook_name(name.as_str()) + || 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 +fn get_declaration_identifier<'a>( + nodes: &'a AstNodes<'a>, + node_id: AstNodeId, +) -> Option { + nodes.ancestors(node_id).map(|id| nodes.kind(id)).find_map(|kind| { + match kind { + // const useHook = () => {}; + AstKind::VariableDeclaration(decl) if decl.declarations.len() == 1 => { + decl.declarations[0].id.get_identifier().map(Atom::to_compact_str) } - } else { - None + // useHook = () => {}; + AstKind::AssignmentExpression(expr) + if matches!(expr.operator, AssignmentOperator::Assign) => + { + expr.left.get_identifier().map(std::convert::Into::into) + } + // const {useHook = () => {}} = {}; + // ({useHook = () => {}} = {}); + AstKind::AssignmentPattern(patt) => { + patt.left.get_identifier().map(Atom::to_compact_str) + } + // { useHook: () => {} } + // { useHook() {} } + AstKind::ObjectProperty(prop) => prop.key.name(), + _ => None, } }) } @@ -888,6 +905,24 @@ fn test() { useHook(); } ", + " + + export const Component = () => { + return { + Target: () => { + useEffect(() => { + return () => { + something.value = true; + }; + }, []); + return
; + }, + useTargetModule: (m) => { + useModule(m); + }, + }; + }; + ", ]; let fail = vec![ diff --git a/crates/oxc_linter/src/snapshots/rules_of_hooks.snap b/crates/oxc_linter/src/snapshots/rules_of_hooks.snap index 3a3151bf5..a282a5582 100644 --- a/crates/oxc_linter/src/snapshots/rules_of_hooks.snap +++ b/crates/oxc_linter/src/snapshots/rules_of_hooks.snap @@ -298,6 +298,38 @@ expression: rules_of_hooks 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] + 5 │ let d = () => useState(); + 6 │ e = () => { useState(); }; + · ───────────────────── + 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] + 6 │ e = () => { useState(); }; + 7 │ ({f: () => { useState(); }}); + · ───────────────────── + 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] + 7 │ ({f: () => { useState(); }}); + 8 │ ({g() { useState(); }}); + · ────────────────── + 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] + 8 │ ({g() { useState(); }}); + 9 │ const {j = () => { useState(); }} = {}; + · ───────────────────── + 10 │ ({k = () => { 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;