diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index ed0f403c8..1f832a803 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -31,6 +31,7 @@ oxc_macros::declare_all_lint_rules! { deepscan::bad_bitwise_operator, deepscan::bad_comparison_sequence, deepscan::bad_array_method_on_arguments, + deepscan::missing_throw, use_isnan, valid_typeof, typescript::isolated_declaration diff --git a/crates/oxc_linter/src/rules/deepscan/missing_throw.rs b/crates/oxc_linter/src/rules/deepscan/missing_throw.rs new file mode 100644 index 000000000..12477e5ea --- /dev/null +++ b/crates/oxc_linter/src/rules/deepscan/missing_throw.rs @@ -0,0 +1,86 @@ +use oxc_ast::AstKind; +use oxc_diagnostics::{ + miette::{self, Diagnostic}, + thiserror::Error, +}; +use oxc_macros::declare_oxc_lint; +use oxc_span::Span; + +use crate::{context::LintContext, rule::Rule, AstNode}; + +#[derive(Debug, Error, Diagnostic)] +#[error("Missing throw")] +#[diagnostic( + severity(warning), + help("The `throw` keyword seems to be missing in front of this 'new' expression") +)] +struct MissingThrowDiagnostic(#[label] pub Span); + +/// `https://deepscan.io/docs/rules/missing-throw` +#[derive(Debug, Default, Clone)] +pub struct MissingThrow; + +declare_oxc_lint!( + /// ### What it does + /// + /// Checks whether the `throw` keyword is missing in front of a `new` expression. + /// + /// ### Example + /// ```javascript + /// function foo() { throw Error() } + /// const foo = () => { new Error() } + /// ``` + MissingThrow, + correctness +); + +impl Rule for MissingThrow { + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + if let AstKind::NewExpression(new_expr) = node.kind() + && new_expr.callee.is_specific_id("Error") + && Self::has_missing_throw(node, ctx) { + ctx.diagnostic(MissingThrowDiagnostic(new_expr.span)); + }; + } +} + +impl MissingThrow { + fn has_missing_throw<'a>(node: &AstNode<'a>, ctx: &LintContext<'a>) -> bool { + let mut node_ancestors = ctx.nodes().ancestors(node.id()).skip(1); + + let Some(node_id) = node_ancestors.next() else { + return false + }; + + if matches!(ctx.nodes().kind(node_id), AstKind::ExpressionStatement(_)) { + for node_id in node_ancestors { + match ctx.nodes().kind(node_id) { + // ignore arrow `const foo = () => new Error()` + AstKind::ArrowExpression(arrow_expr) if arrow_expr.expression => return false, + AstKind::ArrayExpression(_) | AstKind::Function(_) => break, + _ => {} + } + } + return true; + } + + false + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + // Note: lone `Error()` should be caught by no-effect-call + let pass = vec![ + ("function foo() { throw new Error() }", None), + ("const foo = () => new Error()", None), + ("[new Error()]", None), + ]; + + let fail = + vec![("function foo() { new Error() }", None), ("const foo = () => { new Error() }", None)]; + + Tester::new(MissingThrow::NAME, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/missing_throw.snap b/crates/oxc_linter/src/snapshots/missing_throw.snap new file mode 100644 index 000000000..0ed1c7bca --- /dev/null +++ b/crates/oxc_linter/src/snapshots/missing_throw.snap @@ -0,0 +1,19 @@ +--- +source: crates/oxc_linter/src/tester.rs +expression: missing_throw +--- + ⚠ Missing throw + ╭─[missing_throw.tsx:1:1] + 1 │ function foo() { new Error() } + · ─────────── + ╰──── + help: The `throw` keyword seems to be missing in front of this 'new' expression + + ⚠ Missing throw + ╭─[missing_throw.tsx:1:1] + 1 │ const foo = () => { new Error() } + · ─────────── + ╰──── + help: The `throw` keyword seems to be missing in front of this 'new' expression + +