Skip to content

Commit

Permalink
fix: add error messages.
Browse files Browse the repository at this point in the history
  • Loading branch information
rzvxa committed May 10, 2024
1 parent 1345000 commit b0e22c1
Show file tree
Hide file tree
Showing 2 changed files with 206 additions and 246 deletions.
130 changes: 93 additions & 37 deletions crates/oxc_linter/src/rules/react/rules_of_hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use oxc_semantic::{
pg::neighbors_filtered_by_edge_weight,
AstNodeId, AstNodes, BasicBlockElement, EdgeType, Register,
};
use oxc_span::{Atom, GetSpan, Span};
use oxc_span::{Atom, CompactStr, Span};

use crate::{
context::LintContext,
Expand All @@ -26,38 +26,87 @@ use crate::{
enum RulesOfHooksDiagnostic {
#[error(
"eslint-plugin-react-hooks(rules-of-hooks): \
React Hook \"{hook:?}\" is called in function \"{func:?}\" 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\"."
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\"."
)]
#[diagnostic(severity(warning), help("TODO: FunctionError"))]
#[diagnostic(severity(error))]
FunctionError {
#[label]
span: Span,
hook_name: CompactStr,
func_name: CompactStr,
},
#[error(
"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."
)]
#[diagnostic(severity(error))]
ConditionalHook {
#[label]
span: Span,
hook_name: CompactStr,
},
#[error(
"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."
)]
#[diagnostic(severity(error))]
LoopHook {
#[label]
span: Span,
hook_name: CompactStr,
},
#[error(
"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."
)]
#[diagnostic(severity(error))]
TopLevelHook {
#[label]
span: Span,
hook_name: CompactStr,
},
#[error(
"eslint-plugin-react-hooks(rules-of-hooks): \
message: `React Hook {func_name:?} cannot be called in an async function. "
)]
#[diagnostic(severity(error))]
AsyncComponent {
#[label]
span: Span,
func_name: CompactStr,
},
#[error(
"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."
)]
#[diagnostic(severity(error))]
ClassComponent {
#[label]
hook: Span,
span: Span,
hook_name: CompactStr,
},
#[error(
"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."
)]
#[diagnostic(severity(error))]
GenericError {
#[label]
func: Span,
span: Span,
hook_name: CompactStr,
},
#[error("eslint-plugin-react-hooks(rules-of-hooks): TODO: ConditionalHook")]
#[diagnostic(severity(warning), help("TODO: ConditionalHook"))]
ConditionalHook(#[label] Span),
#[error("eslint-plugin-react-hooks(rules-of-hooks): TODO: LoopHook")]
#[diagnostic(severity(warning), help("TODO: LoopHook"))]
LoopHook(#[label] Span),
#[error("eslint-plugin-react-hooks(rules-of-hooks): TODO: TopLevelHook")]
#[diagnostic(severity(warning), help("TODO: TopLevelHook"))]
TopLevelHook(#[label] Span),
#[error("eslint-plugin-react-hooks(rules-of-hooks): TODO: AsyncComponent")]
#[diagnostic(severity(warning), help("TODO: AsyncComponent"))]
AsyncComponent(#[label] Span),
#[error("eslint-plugin-react-hooks(rules-of-hooks): TODO: AsyncComponent")]
#[diagnostic(severity(warning), help("TODO: ClassComponent"))]
ClassComponent(#[label] Span),
#[error("eslint-plugin-react-hooks(rules-of-hooks): TODO: GenericError")]
#[diagnostic(severity(warning), help("TODO: GenericError"))]
GenericError(#[label] Span),
}

#[derive(Debug, Default, Clone)]
Expand All @@ -81,12 +130,16 @@ impl Rule for RulesOfHooks {
if !is_react_hook(&call.callee) {
return;
}
let span = call.span;
let hook_name = CompactStr::from(
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(RulesOfHooksDiagnostic::TopLevelHook(call.span));
return ctx.diagnostic(RulesOfHooksDiagnostic::TopLevelHook { span, hook_name });
};

// Check if our parent function is part of a class.
Expand All @@ -98,10 +151,10 @@ impl Rule for RulesOfHooks {
| AstKind::PropertyDefinition(_)
)
) {
return ctx.diagnostic(RulesOfHooksDiagnostic::ClassComponent(call.span));
return ctx.diagnostic(RulesOfHooksDiagnostic::ClassComponent { span, hook_name });
}

let is_use = call.callee_name().is_some_and(|name| name == "use");
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
Expand All @@ -110,13 +163,16 @@ impl Rule for RulesOfHooks {
{
return ctx.diagnostic(RulesOfHooksDiagnostic::FunctionError {
span: id.span,
hook: call.callee.span(),
func: id.span,
hook_name,
func_name: id.name.to_compact_str(),
});
}
// Hooks can't be called from async function.
AstKind::Function(Function { id: Some(id), r#async: true, .. }) => {
return ctx.diagnostic(RulesOfHooksDiagnostic::AsyncComponent(id.span));
return ctx.diagnostic(RulesOfHooksDiagnostic::AsyncComponent {
span: id.span,
func_name: id.name.to_compact_str(),
});
}
// 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.
Expand All @@ -125,7 +181,7 @@ impl Rule for RulesOfHooks {
{
// This rules doesn't apply to `use(...)`.
if !is_use && is_somewhere_inside_component_or_hook(nodes, parent_func.id()) {
ctx.diagnostic(RulesOfHooksDiagnostic::GenericError(call.span));
ctx.diagnostic(RulesOfHooksDiagnostic::GenericError { span, hook_name });
}
return;
}
Expand Down Expand Up @@ -166,8 +222,8 @@ impl Rule for RulesOfHooks {
{
return ctx.diagnostic(RulesOfHooksDiagnostic::FunctionError {
span: *span,
hook: call.callee.span(),
func: *span,
hook_name,
func_name: "Anonymous".into(),
});
}
}
Expand Down Expand Up @@ -204,14 +260,14 @@ impl Rule for RulesOfHooks {

// Is this node cyclic?
if self.is_cyclic(ctx, node_cfg_ix) {
return ctx.diagnostic(RulesOfHooksDiagnostic::LoopHook(call.span));
return ctx.diagnostic(RulesOfHooksDiagnostic::LoopHook { 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(RulesOfHooksDiagnostic::ConditionalHook(call.span));
return ctx.diagnostic(RulesOfHooksDiagnostic::ConditionalHook { span, hook_name });
}
}
}
Expand Down

0 comments on commit b0e22c1

Please sign in to comment.