diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 8b1d0f0d7..b7dc7cfca 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -25,6 +25,7 @@ mod import { mod eslint { pub mod array_callback_return; pub mod constructor_super; + pub mod default_case; pub mod default_case_last; pub mod default_param_last; pub mod eqeqeq; @@ -390,6 +391,7 @@ mod tree_shaking { oxc_macros::declare_all_lint_rules! { eslint::array_callback_return, eslint::constructor_super, + eslint::default_case, eslint::default_case_last, eslint::default_param_last, eslint::eqeqeq, diff --git a/crates/oxc_linter/src/rules/eslint/default_case.rs b/crates/oxc_linter/src/rules/eslint/default_case.rs new file mode 100644 index 000000000..3933a5a89 --- /dev/null +++ b/crates/oxc_linter/src/rules/eslint/default_case.rs @@ -0,0 +1,258 @@ +use oxc_ast::AstKind; +use oxc_diagnostics::OxcDiagnostic; +use oxc_macros::declare_oxc_lint; +use oxc_span::Span; +use regex::Regex; +use regex::RegexBuilder; + +use crate::{context::LintContext, rule::Rule, AstNode}; + +fn default_case_diagnostic(span0: Span) -> OxcDiagnostic { + OxcDiagnostic::warn("eslint(default-case): Require default cases in switch statements.") + .with_help("Add a default case.") + .with_labels([span0.into()]) +} + +#[derive(Debug, Default, Clone)] +pub struct DefaultCase(Box); + +#[derive(Debug, Default, Clone)] +pub struct DefaultCaseConfig { + comment_pattern: Option, +} + +impl std::ops::Deref for DefaultCase { + type Target = DefaultCaseConfig; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +declare_oxc_lint!( + /// ### What it does + /// + /// Require default cases in switch statements + /// + /// ### Why is this bad? + /// + /// Some code conventions require that all switch statements have a default case, even if the + /// default case is empty. + /// + /// ### Example + /// ```javascript + /// switch (foo) { + /// case 1: + /// break; + /// } + /// ``` + DefaultCase, + restriction, +); + +impl Rule for DefaultCase { + fn from_configuration(value: serde_json::Value) -> Self { + let mut cfg = DefaultCaseConfig::default(); + + if let Some(config) = value.get(0) { + if let Some(val) = config.get("commentPattern").and_then(serde_json::Value::as_str) { + cfg.comment_pattern = RegexBuilder::new(val).case_insensitive(true).build().ok(); + } + } + + Self(Box::new(cfg)) + } + + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + if let AstKind::SwitchStatement(switch) = node.kind() { + let cases = &switch.cases; + if !cases.is_empty() && !cases.iter().any(|case| case.test.is_none()) { + if let Some(last_case) = cases.last() { + let has_default_comment = ctx + .semantic() + .trivias() + .comments_range(last_case.span.start..switch.span.end) + .last() + .is_some_and(|(start, comment)| { + let raw = Span::new(*start, comment.end) + .source_text(ctx.semantic().source_text()) + .trim(); + match &self.comment_pattern { + Some(comment_pattern) => comment_pattern.is_match(raw), + None => raw.eq_ignore_ascii_case("no default"), + } + }); + + if !has_default_comment { + ctx.diagnostic(default_case_diagnostic(switch.span)); + } + } + } + } + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + ("switch (a) { case 1: break; default: break; }", None), + ("switch (a) { case 1: break; case 2: default: break; }", None), + ( + "switch (a) { case 1: break; default: break; + //no default + }", + None, + ), + ( + "switch (a) { + case 1: break; + + //oh-oh + // no default + }", + None, + ), + ( + "switch (a) { + case 1: + + // no default + }", + None, + ), + ( + "switch (a) { + case 1: + + // No default + }", + None, + ), + ( + "switch (a) { + case 1: + + // no deFAUlt + }", + None, + ), + ( + "switch (a) { + case 1: + + // NO DEFAULT + }", + None, + ), + ( + "switch (a) { + case 1: a = 4; + + // no default + }", + None, + ), + ( + "switch (a) { + case 1: a = 4; + + /* no default */ + }", + None, + ), + ( + "switch (a) { + case 1: a = 4; break; break; + + // no default + }", + None, + ), + ( + "switch (a) { // no default + }", + None, + ), + ("switch (a) { }", None), + ( + "switch (a) { case 1: break; default: break; }", + Some(serde_json::json!([{ + "commentPattern": "default case omitted" + }])), + ), + ( + "switch (a) { case 1: break; + // skip default case + }", + Some(serde_json::json!([{ + "commentPattern": "^skip default" + }])), + ), + ( + "switch (a) { case 1: break; + /* + TODO: + throw error in default case + */ + }", + Some(serde_json::json!([{ + "commentPattern": "default" + }])), + ), + ( + "switch (a) { case 1: break; + // + }", + Some(serde_json::json!([{ + "commentPattern": ".?" + }])), + ), + ]; + + let fail = vec![ + ("switch (a) { case 1: break; }", None), + ( + "switch (a) { + // no default + case 1: break; }", + None, + ), + ( + "switch (a) { case 1: break; + // no default + // nope + }", + None, + ), + ( + "switch (a) { case 1: break; + // no default + }", + Some(serde_json::json!([{ + "commentPattern": "skipped default case" + }])), + ), + ( + "switch (a) { + case 1: break; + // default omitted intentionally + // TODO: add default case + }", + Some(serde_json::json!([{ + "commentPattern": "default omitted" + }])), + ), + ( + "switch (a) { + case 1: break; + }", + Some(serde_json::json!([{ + "commentPattern": ".?" + }])), + ), + ]; + + Tester::new(DefaultCase::NAME, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/default_case.snap b/crates/oxc_linter/src/snapshots/default_case.snap new file mode 100644 index 000000000..245d3907f --- /dev/null +++ b/crates/oxc_linter/src/snapshots/default_case.snap @@ -0,0 +1,53 @@ +--- +source: crates/oxc_linter/src/tester.rs +expression: default_case +--- + ⚠ eslint(default-case): Require default cases in switch statements. + ╭─[default_case.tsx:1:1] + 1 │ switch (a) { case 1: break; } + · ───────────────────────────── + ╰──── + help: Add a default case. + + ⚠ eslint(default-case): Require default cases in switch statements. + ╭─[default_case.tsx:1:1] + 1 │ ╭─▶ switch (a) { + 2 │ │ // no default + 3 │ ╰─▶ case 1: break; } + ╰──── + help: Add a default case. + + ⚠ eslint(default-case): Require default cases in switch statements. + ╭─[default_case.tsx:1:1] + 1 │ ╭─▶ switch (a) { case 1: break; + 2 │ │ // no default + 3 │ │ // nope + 4 │ ╰─▶ } + ╰──── + help: Add a default case. + + ⚠ eslint(default-case): Require default cases in switch statements. + ╭─[default_case.tsx:1:1] + 1 │ ╭─▶ switch (a) { case 1: break; + 2 │ │ // no default + 3 │ ╰─▶ } + ╰──── + help: Add a default case. + + ⚠ eslint(default-case): Require default cases in switch statements. + ╭─[default_case.tsx:1:1] + 1 │ ╭─▶ switch (a) { + 2 │ │ case 1: break; + 3 │ │ // default omitted intentionally + 4 │ │ // TODO: add default case + 5 │ ╰─▶ } + ╰──── + help: Add a default case. + + ⚠ eslint(default-case): Require default cases in switch statements. + ╭─[default_case.tsx:1:1] + 1 │ ╭─▶ switch (a) { + 2 │ │ case 1: break; + 3 │ ╰─▶ } + ╰──── + help: Add a default case.