fix(linter/react): rules_of_hooks add support for property hooks/components. (#3300)

related to #3257
This commit is contained in:
rzvxa 2024-05-16 16:38:12 +00:00
parent 0c09047111
commit 95944419ec
3 changed files with 94 additions and 13 deletions

View file

@ -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),

View file

@ -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<CompactStr> {
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 <div></div>;
},
useTargetModule: (m) => {
useModule(m);
},
};
};
",
];
let fail = vec![

View file

@ -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;