From 033a112a83ae58458c328fb7dd64cea62574f754 Mon Sep 17 00:00:00 2001 From: Yiming Pan Date: Tue, 7 Nov 2023 10:52:39 +0800 Subject: [PATCH] feat(linter): support eslint(default-case-last) (#1156) --- crates/oxc_linter/src/rules.rs | 2 + .../src/rules/eslint/default_case_last.rs | 122 ++++++++++++++++++ .../src/snapshots/default_case_last.snap | 103 +++++++++++++++ 3 files changed, 227 insertions(+) create mode 100644 crates/oxc_linter/src/rules/eslint/default_case_last.rs create mode 100644 crates/oxc_linter/src/snapshots/default_case_last.snap diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index b32abdcf0..af77295cb 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -27,6 +27,7 @@ mod deepscan { mod eslint { pub mod array_callback_return; pub mod constructor_super; + pub mod default_case_last; pub mod eqeqeq; pub mod for_direction; pub mod getter_return; @@ -181,6 +182,7 @@ oxc_macros::declare_all_lint_rules! { deepscan::uninvoked_array_callback, eslint::array_callback_return, eslint::constructor_super, + eslint::default_case_last, eslint::eqeqeq, eslint::for_direction, eslint::getter_return, diff --git a/crates/oxc_linter/src/rules/eslint/default_case_last.rs b/crates/oxc_linter/src/rules/eslint/default_case_last.rs new file mode 100644 index 000000000..b384566ed --- /dev/null +++ b/crates/oxc_linter/src/rules/eslint/default_case_last.rs @@ -0,0 +1,122 @@ +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("eslint(default-case-last): Enforce default clauses in switch statements to be last")] +#[diagnostic(severity(warning))] +struct DefaultCaseLastDiagnostic(#[label("Default clause should be the last clause.")] pub Span); + +#[derive(Debug, Default, Clone)] +pub struct DefaultCaseLast; + +declare_oxc_lint!( + /// ### What it does + /// Enforce default clauses in switch statements to be last + /// + /// ### Why is this bad? + /// A switch statement can optionally have a default clause. + /// If present, it’s usually the last clause, but it doesn’t need to be. It is also allowed to put the default clause before all case clauses, or anywhere between. The behavior is mostly the same as if it was the last clause. The default block will be still executed only if there is no match in the case clauses (including those defined after the default), but there is also the ability to “fall through” from the default clause to the following clause in the list. However, such flow is not common and it would be confusing to the readers. + /// Even if there is no “fall through” logic, it’s still unexpected to see the default clause before or between the case clauses. By convention, it is expected to be the last clause. + /// If a switch statement should have a default clause, it’s considered a best practice to define it as the last clause. + /// + /// ### Example + /// ```javascript + /// switch (foo) { + /// default: + /// bar(); + /// break; + /// case "a": + /// baz(); + /// break; + /// } + /// + /// switch (foo) { + /// case 1: + /// bar(); + /// break; + /// default: + /// baz(); + /// break; + /// case 2: + /// qux(); + /// break; + /// } + /// ``` + DefaultCaseLast, + style +); + +impl Rule for DefaultCaseLast { + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + let AstKind::SwitchStatement(switch) = node.kind() else { return }; + let cases = &switch.cases; + let index_of_default = cases.iter().position(|c| c.test.is_none()); + + if let Some(index) = index_of_default { + if index != cases.len() - 1 { + let default_clause = &cases[index]; + ctx.diagnostic(DefaultCaseLastDiagnostic(Span::new( + default_clause.span.start, + default_clause.span.start + 7, + ))); + } + } + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + r#"switch (foo) {}"#, + r#"switch (foo) { case 1: bar(); break; }"#, + r#"switch (foo) { case 1: break; }"#, + r#"switch (foo) { case 1: }"#, + r#"switch (foo) { case 1: bar(); break; case 2: baz(); break; }"#, + r#"switch (foo) { case 1: break; case 2: break; }"#, + r#"switch (foo) { case 1: case 2: break; }"#, + r#"switch (foo) { case 1: case 2: }"#, + r#"switch (foo) { default: bar(); break; }"#, + r#"switch (foo) { default: bar(); }"#, + r#"switch (foo) { default: break; }"#, + r#"switch (foo) { default: }"#, + r#"switch (foo) { case 1: break; default: break; }"#, + r#"switch (foo) { case 1: break; default: }"#, + r#"switch (foo) { case 1: default: break; }"#, + r#"switch (foo) { case 1: default: }"#, + r#"switch (foo) { case 1: baz(); break; case 2: quux(); break; default: quuux(); break; }"#, + r#"switch (foo) { case 1: break; case 2: break; default: break; }"#, + r#"switch (foo) { case 1: break; case 2: break; default: }"#, + r#"switch (foo) { case 1: case 2: break; default: break; }"#, + r#"switch (foo) { case 1: break; case 2: default: break; }"#, + r#"switch (foo) { case 1: break; case 2: default: }"#, + r#"switch (foo) { case 1: case 2: default: }"#, + ]; + + let fail = vec![ + r#"switch (foo) { default: bar(); break; case 1: baz(); break; }"#, + r#"switch (foo) { default: break; case 1: break; }"#, + r#"switch (foo) { default: break; case 1: }"#, + r#"switch (foo) { default: case 1: break; }"#, + r#"switch (foo) { default: case 1: }"#, + r#"switch (foo) { default: break; case 1: break; case 2: break; }"#, + r#"switch (foo) { default: case 1: break; case 2: break; }"#, + r#"switch (foo) { default: case 1: case 2: break; }"#, + r#"switch (foo) { default: case 1: case 2: }"#, + r#"switch (foo) { case 1: break; default: break; case 2: break; }"#, + r#"switch (foo) { case 1: default: break; case 2: break; }"#, + r#"switch (foo) { case 1: break; default: case 2: break; }"#, + r#"switch (foo) { case 1: default: case 2: break; }"#, + r#"switch (foo) { case 1: default: case 2: }"#, + ]; + + Tester::new_without_config(DefaultCaseLast::NAME, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/default_case_last.snap b/crates/oxc_linter/src/snapshots/default_case_last.snap new file mode 100644 index 000000000..0fb4f04b2 --- /dev/null +++ b/crates/oxc_linter/src/snapshots/default_case_last.snap @@ -0,0 +1,103 @@ +--- +source: crates/oxc_linter/src/tester.rs +expression: default_case_last +--- + ⚠ eslint(default-case-last): Enforce default clauses in switch statements to be last + ╭─[default_case_last.tsx:1:1] + 1 │ switch (foo) { default: bar(); break; case 1: baz(); break; } + · ───┬─── + · ╰── Default clause should be the last clause. + ╰──── + + ⚠ eslint(default-case-last): Enforce default clauses in switch statements to be last + ╭─[default_case_last.tsx:1:1] + 1 │ switch (foo) { default: break; case 1: break; } + · ───┬─── + · ╰── Default clause should be the last clause. + ╰──── + + ⚠ eslint(default-case-last): Enforce default clauses in switch statements to be last + ╭─[default_case_last.tsx:1:1] + 1 │ switch (foo) { default: break; case 1: } + · ───┬─── + · ╰── Default clause should be the last clause. + ╰──── + + ⚠ eslint(default-case-last): Enforce default clauses in switch statements to be last + ╭─[default_case_last.tsx:1:1] + 1 │ switch (foo) { default: case 1: break; } + · ───┬─── + · ╰── Default clause should be the last clause. + ╰──── + + ⚠ eslint(default-case-last): Enforce default clauses in switch statements to be last + ╭─[default_case_last.tsx:1:1] + 1 │ switch (foo) { default: case 1: } + · ───┬─── + · ╰── Default clause should be the last clause. + ╰──── + + ⚠ eslint(default-case-last): Enforce default clauses in switch statements to be last + ╭─[default_case_last.tsx:1:1] + 1 │ switch (foo) { default: break; case 1: break; case 2: break; } + · ───┬─── + · ╰── Default clause should be the last clause. + ╰──── + + ⚠ eslint(default-case-last): Enforce default clauses in switch statements to be last + ╭─[default_case_last.tsx:1:1] + 1 │ switch (foo) { default: case 1: break; case 2: break; } + · ───┬─── + · ╰── Default clause should be the last clause. + ╰──── + + ⚠ eslint(default-case-last): Enforce default clauses in switch statements to be last + ╭─[default_case_last.tsx:1:1] + 1 │ switch (foo) { default: case 1: case 2: break; } + · ───┬─── + · ╰── Default clause should be the last clause. + ╰──── + + ⚠ eslint(default-case-last): Enforce default clauses in switch statements to be last + ╭─[default_case_last.tsx:1:1] + 1 │ switch (foo) { default: case 1: case 2: } + · ───┬─── + · ╰── Default clause should be the last clause. + ╰──── + + ⚠ eslint(default-case-last): Enforce default clauses in switch statements to be last + ╭─[default_case_last.tsx:1:1] + 1 │ switch (foo) { case 1: break; default: break; case 2: break; } + · ───┬─── + · ╰── Default clause should be the last clause. + ╰──── + + ⚠ eslint(default-case-last): Enforce default clauses in switch statements to be last + ╭─[default_case_last.tsx:1:1] + 1 │ switch (foo) { case 1: default: break; case 2: break; } + · ───┬─── + · ╰── Default clause should be the last clause. + ╰──── + + ⚠ eslint(default-case-last): Enforce default clauses in switch statements to be last + ╭─[default_case_last.tsx:1:1] + 1 │ switch (foo) { case 1: break; default: case 2: break; } + · ───┬─── + · ╰── Default clause should be the last clause. + ╰──── + + ⚠ eslint(default-case-last): Enforce default clauses in switch statements to be last + ╭─[default_case_last.tsx:1:1] + 1 │ switch (foo) { case 1: default: case 2: break; } + · ───┬─── + · ╰── Default clause should be the last clause. + ╰──── + + ⚠ eslint(default-case-last): Enforce default clauses in switch statements to be last + ╭─[default_case_last.tsx:1:1] + 1 │ switch (foo) { case 1: default: case 2: } + · ───┬─── + · ╰── Default clause should be the last clause. + ╰──── + +