mirror of
https://github.com/danbulant/oxc
synced 2026-05-19 04:08:41 +00:00
fix(linter/react): rules_of_hooks add support for property hooks/components. (#3300)
related to #3257
This commit is contained in:
parent
0c09047111
commit
95944419ec
3 changed files with 94 additions and 13 deletions
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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![
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue