diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 67cad3c52..760034d31 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -7,6 +7,7 @@ oxc_macros::declare_all_lint_rules! { eq_eq_eq, for_direction, no_debugger, + no_duplicate_case, no_array_constructor, no_caller, no_empty, diff --git a/crates/oxc_linter/src/rules/no_duplicate_case.rs b/crates/oxc_linter/src/rules/no_duplicate_case.rs new file mode 100644 index 000000000..f6b6588c2 --- /dev/null +++ b/crates/oxc_linter/src/rules/no_duplicate_case.rs @@ -0,0 +1,172 @@ +use oxc_ast::{AstKind, GetSpan, Span}; +use oxc_diagnostics::{ + miette::{self, Diagnostic}, + thiserror::Error, +}; +use oxc_macros::declare_oxc_lint; +use rustc_hash::FxHashMap; + +use crate::{ast_util::calculate_hash, context::LintContext, rule::Rule, AstNode}; + +#[derive(Debug, Error, Diagnostic)] +#[error("eslint(no-duplicate-case): Disallow duplicate case labels")] +#[diagnostic(severity(warning), help("Remove the duplicated case"))] +struct NoDuplicateCaseDiagnostic(#[label] pub Span, #[label] pub Span); + +#[derive(Debug, Default, Clone)] +pub struct NoDuplicateCase; + +declare_oxc_lint!( + /// ### What it does + /// + /// Disallow duplicate case labels + /// + /// ### Why is this bad? + /// + /// If a switch statement has duplicate test expressions in case clauses, + /// it is likely that a programmer copied a case clause but forgot to change the test expression. + /// + /// ### Example + /// ```javascript + /// var a = 1; + /// switch (a) { + /// case 1: + /// break; + /// case 1: + /// break; + /// case 2: + /// break; + /// default: + /// break; + /// } + /// ``` + NoDuplicateCase, + correctness +); + +impl Rule for NoDuplicateCase { + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + if let AstKind::SwitchStatement(ss) = node.get().kind() { + let mut map = FxHashMap::default(); + map.reserve(ss.cases.len()); + for case in ss.cases.iter() { + if let Some(test) = case.test.as_ref() { + let hash = calculate_hash(test); + + if let Some(prev_span) = map.insert(hash, test.span()) { + ctx.diagnostic(NoDuplicateCaseDiagnostic(prev_span, test.span())); + } + } + } + } + } +} + +#[test] +#[allow(clippy::too_many_lines)] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + ("var a = 1; switch (a) {case 1: break; case 2: break; default: break;}", None), + ("var a = 1; switch (a) {case 1: break; case '1': break; default: break;}", None), + ("var a = 1; switch (a) {case 1: break; case true: break; default: break;}", None), + ("var a = 1; switch (a) {default: break;}", None), + ( + "var a = 1, p = {p: {p1: 1, p2: 1}}; switch (a) {case p.p.p1: break; case p.p.p2: break; default: break;}", + None, + ), + ( + "var a = 1, f = function(b) { return b ? { p1: 1 } : { p1: 2 }; }; switch (a) {case f(true).p1: break; case f(true, false).p1: break; default: break;}", + None, + ), + ( + "var a = 1, f = function(s) { return { p1: s } }; switch (a) {case f(a + 1).p1: break; case f(a + 2).p1: break; default: break;}", + None, + ), + ( + "var a = 1, f = function(s) { return { p1: s } }; switch (a) {case f(a == 1 ? 2 : 3).p1: break; case f(a === 1 ? 2 : 3).p1: break; default: break;}", + None, + ), + ( + "var a = 1, f1 = function() { return { p1: 1 } }, f2 = function() { return { p1: 2 } }; switch (a) {case f1().p1: break; case f2().p1: break; default: break;}", + None, + ), + ( + "var a = [1,2]; switch(a.toString()){case ([1,2]).toString():break; case ([1]).toString():break; default:break;}", + None, + ), + ("switch(a) { case a: break; } switch(a) { case a: break; }", None), + ("switch(a) { case toString: break; }", None), + ]; + + let fail = vec![ + ( + "var a = 1; switch (a) {case 1: break; case 1: break; case 2: break; default: break;}", + None, + ), + ( + "var a = '1'; switch (a) {case '1': break; case '1': break; case '2': break; default: break;}", + None, + ), + ( + "var a = 1, one = 1; switch (a) {case one: break; case one: break; case 2: break; default: break;}", + None, + ), + ( + "var a = 1, p = {p: {p1: 1, p2: 1}}; switch (a) {case p.p.p1: break; case p.p.p1: break; default: break;}", + None, + ), + ( + "var a = 1, f = function(b) { return b ? { p1: 1 } : { p1: 2 }; }; switch (a) {case f(true).p1: break; case f(true).p1: break; default: break;}", + None, + ), + ( + "var a = 1, f = function(s) { return { p1: s } }; switch (a) {case f(a + 1).p1: break; case f(a + 1).p1: break; default: break;}", + None, + ), + ( + "var a = 1, f = function(s) { return { p1: s } }; switch (a) {case f(a === 1 ? 2 : 3).p1: break; case f(a === 1 ? 2 : 3).p1: break; default: break;}", + None, + ), + ( + "var a = 1, f1 = function() { return { p1: 1 } }; switch (a) {case f1().p1: break; case f1().p1: break; default: break;}", + None, + ), + ( + "var a = [1, 2]; switch(a.toString()){case ([1, 2]).toString():break; case ([1, 2]).toString():break; default:break;}", + None, + ), + ("switch (a) { case a: case a: }", None), + ( + "switch (a) { case a: break; case b: break; case a: break; case c: break; case a: break; }", + None, + ), + ( + "var a = 1, p = {p: {p1: 1, p2: 1}}; switch (a) {case p.p.p1: break; case p. p // comment\n .p1: break; default: break;}", + None, + ), + ( + "var a = 1, p = {p: {p1: 1, p2: 1}}; switch (a) {case p .p\n/* comment */\n.p1: break; case p.p.p1: break; default: break;}", + None, + ), + ( + "var a = 1, p = {p: {p1: 1, p2: 1}}; switch (a) {case p .p\n/* comment */\n.p1: break; case p. p // comment\n .p1: break; default: break;}", + None, + ), + ( + "var a = 1, p = {p: {p1: 1, p2: 1}}; switch (a) {case p.p.p1: break; case p. p // comment\n .p1: break; case p .p\n/* comment */\n.p1: break; default: break;}", + None, + ), + ( + "var a = 1, f = function(s) { return { p1: s } }; switch (a) {case f(a + 1).p1: break; case f(a+1).p1: break; default: break;}", + None, + ), + ( + "var a = 1, f = function(s) { return { p1: s } }; switch (a) {case f(\na + 1 // comment\n).p1: break; case f(a+1)\n.p1: break; default: break;}", + None, + ), + ]; + + Tester::new(NoDuplicateCase::NAME, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/no_duplicate_case.snap b/crates/oxc_linter/src/snapshots/no_duplicate_case.snap new file mode 100644 index 000000000..3892ab0e6 --- /dev/null +++ b/crates/oxc_linter/src/snapshots/no_duplicate_case.snap @@ -0,0 +1,150 @@ +--- +source: crates/oxc_linter/src/tester.rs +assertion_line: 53 +expression: no_duplicate_case +--- + + ⚠ eslint(no-duplicate-case): Disallow duplicate case labels + ╭─[no_duplicate_case.tsx:1:1] + 1 │ var a = 1; switch (a) {case 1: break; case 1: break; case 2: break; default: break;} + · ─ ─ + ╰──── + help: Remove the duplicated case + + ⚠ eslint(no-duplicate-case): Disallow duplicate case labels + ╭─[no_duplicate_case.tsx:1:1] + 1 │ var a = '1'; switch (a) {case '1': break; case '1': break; case '2': break; default: break;} + · ─── ─── + ╰──── + help: Remove the duplicated case + + ⚠ eslint(no-duplicate-case): Disallow duplicate case labels + ╭─[no_duplicate_case.tsx:1:1] + 1 │ var a = 1, one = 1; switch (a) {case one: break; case one: break; case 2: break; default: break;} + · ─── ─── + ╰──── + help: Remove the duplicated case + + ⚠ eslint(no-duplicate-case): Disallow duplicate case labels + ╭─[no_duplicate_case.tsx:1:1] + 1 │ var a = 1, p = {p: {p1: 1, p2: 1}}; switch (a) {case p.p.p1: break; case p.p.p1: break; default: break;} + · ────── ────── + ╰──── + help: Remove the duplicated case + + ⚠ eslint(no-duplicate-case): Disallow duplicate case labels + ╭─[no_duplicate_case.tsx:1:1] + 1 │ var a = 1, f = function(b) { return b ? { p1: 1 } : { p1: 2 }; }; switch (a) {case f(true).p1: break; case f(true).p1: break; default: break;} + · ────────── ────────── + ╰──── + help: Remove the duplicated case + + ⚠ eslint(no-duplicate-case): Disallow duplicate case labels + ╭─[no_duplicate_case.tsx:1:1] + 1 │ var a = 1, f = function(s) { return { p1: s } }; switch (a) {case f(a + 1).p1: break; case f(a + 1).p1: break; default: break;} + · ─────────── ─────────── + ╰──── + help: Remove the duplicated case + + ⚠ eslint(no-duplicate-case): Disallow duplicate case labels + ╭─[no_duplicate_case.tsx:1:1] + 1 │ var a = 1, f = function(s) { return { p1: s } }; switch (a) {case f(a === 1 ? 2 : 3).p1: break; case f(a === 1 ? 2 : 3).p1: break; default: break;} + · ───────────────────── ───────────────────── + ╰──── + help: Remove the duplicated case + + ⚠ eslint(no-duplicate-case): Disallow duplicate case labels + ╭─[no_duplicate_case.tsx:1:1] + 1 │ var a = 1, f1 = function() { return { p1: 1 } }; switch (a) {case f1().p1: break; case f1().p1: break; default: break;} + · ─────── ─────── + ╰──── + help: Remove the duplicated case + + ⚠ eslint(no-duplicate-case): Disallow duplicate case labels + ╭─[no_duplicate_case.tsx:1:1] + 1 │ var a = [1, 2]; switch(a.toString()){case ([1, 2]).toString():break; case ([1, 2]).toString():break; default:break;} + · ─────────────────── ─────────────────── + ╰──── + help: Remove the duplicated case + + ⚠ eslint(no-duplicate-case): Disallow duplicate case labels + ╭─[no_duplicate_case.tsx:1:1] + 1 │ switch (a) { case a: case a: } + · ─ ─ + ╰──── + help: Remove the duplicated case + + ⚠ eslint(no-duplicate-case): Disallow duplicate case labels + ╭─[no_duplicate_case.tsx:1:1] + 1 │ switch (a) { case a: break; case b: break; case a: break; case c: break; case a: break; } + · ─ ─ + ╰──── + help: Remove the duplicated case + + ⚠ eslint(no-duplicate-case): Disallow duplicate case labels + ╭─[no_duplicate_case.tsx:1:1] + 1 │ switch (a) { case a: break; case b: break; case a: break; case c: break; case a: break; } + · ─ ─ + ╰──── + help: Remove the duplicated case + + ⚠ eslint(no-duplicate-case): Disallow duplicate case labels + ╭─[no_duplicate_case.tsx:1:1] + 1 │ ╭─▶ var a = 1, p = {p: {p1: 1, p2: 1}}; switch (a) {case p.p.p1: break; case p. p // comment + · ││ ────── + 2 │ ╰─▶ .p1: break; default: break;} + ╰──── + help: Remove the duplicated case + + ⚠ eslint(no-duplicate-case): Disallow duplicate case labels + ╭─[no_duplicate_case.tsx:1:1] + 1 │ ╭─▶ var a = 1, p = {p: {p1: 1, p2: 1}}; switch (a) {case p .p + 2 │ │ /* comment */ + 3 │ ╰─▶ .p1: break; case p.p.p1: break; default: break;} + · ╰─── ────── + ╰──── + help: Remove the duplicated case + + ⚠ eslint(no-duplicate-case): Disallow duplicate case labels + ╭─[no_duplicate_case.tsx:1:1] + 1 │ ╭──▶ var a = 1, p = {p: {p1: 1, p2: 1}}; switch (a) {case p .p + 2 │ │ /* comment */ + 3 │ ╰──▶ .p1: break; case p. p // comment + 4 │ ╰──▶ .p1: break; default: break;} + ╰──── + help: Remove the duplicated case + + ⚠ eslint(no-duplicate-case): Disallow duplicate case labels + ╭─[no_duplicate_case.tsx:1:1] + 1 │ ╭─▶ var a = 1, p = {p: {p1: 1, p2: 1}}; switch (a) {case p.p.p1: break; case p. p // comment + · ││ ────── + 2 │ ╰─▶ .p1: break; case p .p + 3 │ /* comment */ + ╰──── + help: Remove the duplicated case + + ⚠ eslint(no-duplicate-case): Disallow duplicate case labels + ╭─[no_duplicate_case.tsx:1:1] + 1 │ ╭──▶ var a = 1, p = {p: {p1: 1, p2: 1}}; switch (a) {case p.p.p1: break; case p. p // comment + 2 │ ╰──▶ .p1: break; case p .p + 3 │ │ /* comment */ + 4 │ ╰──▶ .p1: break; default: break;} + ╰──── + help: Remove the duplicated case + + ⚠ eslint(no-duplicate-case): Disallow duplicate case labels + ╭─[no_duplicate_case.tsx:1:1] + 1 │ var a = 1, f = function(s) { return { p1: s } }; switch (a) {case f(a + 1).p1: break; case f(a+1).p1: break; default: break;} + · ─────────── ───────── + ╰──── + help: Remove the duplicated case + + ⚠ eslint(no-duplicate-case): Disallow duplicate case labels + ╭─[no_duplicate_case.tsx:1:1] + 1 │ ╭──▶ var a = 1, f = function(s) { return { p1: s } }; switch (a) {case f( + 2 │ │ a + 1 // comment + 3 │ ╰──▶ ).p1: break; case f(a+1) + 4 │ ╰──▶ .p1: break; default: break;} + ╰──── + help: Remove the duplicated case +