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 4bc8d49a1..e73ba8b7e 100644 --- a/crates/oxc_linter/src/rules/react/rules_of_hooks.rs +++ b/crates/oxc_linter/src/rules/react/rules_of_hooks.rs @@ -15,7 +15,7 @@ use oxc_syntax::operator::AssignmentOperator; use crate::{ context::LintContext, rule::Rule, - utils::{is_react_component_or_hook_name, is_react_hook}, + utils::{is_react_component_or_hook_name, is_react_function_call, is_react_hook}, AstNode, }; @@ -120,6 +120,8 @@ impl Rule for RulesOfHooks { let semantic = ctx.semantic(); let nodes = semantic.nodes(); + let is_use = is_react_function_call(call, "use"); + let Some(parent_func) = parent_func(nodes, node) else { return ctx.diagnostic(diagnostics::top_level_hook(span, hook_name)); }; @@ -136,8 +138,6 @@ impl Rule for RulesOfHooks { 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), .. }) @@ -356,8 +356,7 @@ fn is_non_react_func_arg(nodes: &AstNodes, node_id: AstNodeId) -> bool { return false; }; - // TODO make it better, might have false positives. - call.callee_name().is_some_and(|name| !matches!(name, "forwardRef" | "memo")) + !(is_react_function_call(call, "forwardRef") || is_react_function_call(call, "memo")) } fn is_somewhere_inside_component_or_hook(nodes: &AstNodes, node_id: AstNodeId) -> bool { @@ -618,6 +617,7 @@ fn test() { use_hook(); // also valid because it's not matching the PascalCase namespace jest.useFakeTimer() + AFFiNE.plugins.use('oauth'); ", // Regression test for some internal code. // This shows how the "callback rule" is more relaxed, diff --git a/crates/oxc_linter/src/utils/react.rs b/crates/oxc_linter/src/utils/react.rs index afbcb35eb..01cb1e26a 100644 --- a/crates/oxc_linter/src/utils/react.rs +++ b/crates/oxc_linter/src/utils/react.rs @@ -301,14 +301,16 @@ pub fn is_react_hook(expr: &Expression) -> bool { #[allow(unsafe_code)] let expr = unsafe { expr.as_member_expression().unwrap_unchecked() }; let MemberExpression::StaticMemberExpression(static_expr) = expr else { return false }; + + let is_valid_property = is_react_hook_name(&static_expr.property.name); let is_valid_namespace = match &static_expr.object { Expression::Identifier(ident) => { + // TODO: test PascalCase ident.name.chars().next().is_some_and(char::is_uppercase) } - Expression::ThisExpression(_) | Expression::Super(_) => false, - _ => true, + _ => false, }; - is_valid_namespace && expr.static_property_name().is_some_and(is_react_hook_name) + is_valid_namespace && is_valid_property } Expression::Identifier(ident) => is_react_hook_name(ident.name.as_str()), _ => false, @@ -326,3 +328,20 @@ pub fn is_react_component_name(name: &str) -> bool { pub fn is_react_component_or_hook_name(name: &str) -> bool { is_react_component_name(name) || is_react_hook_name(name) } + +pub fn is_react_function_call(call: &CallExpression, expected_call: &str) -> bool { + let Some(subject) = call.callee_name() else { return false }; + + if subject != expected_call { + return false; + } + + if let Some(member) = call.callee.as_member_expression() { + matches! { + member.object().get_identifier_reference(), + Some(ident) if ident.name.as_str() == PRAGMA + } + } else { + true + } +}