diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 329092a963aaa..5f40d3432ad7c 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -77,6 +77,7 @@ mod typescript { pub mod adjacent_overload_signatures; pub mod consistent_type_exports; pub mod isolated_declaration; + pub mod no_duplicate_enum_values; pub mod no_empty_interface; pub mod no_extra_non_null_assertion; pub mod no_misused_new; @@ -161,6 +162,7 @@ oxc_macros::declare_all_lint_rules! { typescript::adjacent_overload_signatures, typescript::consistent_type_exports, typescript::isolated_declaration, + typescript::no_duplicate_enum_values, typescript::no_empty_interface, typescript::no_extra_non_null_assertion, typescript::no_non_null_asserted_optional_chain, diff --git a/crates/oxc_linter/src/rules/typescript/no_duplicate_enum_values.rs b/crates/oxc_linter/src/rules/typescript/no_duplicate_enum_values.rs new file mode 100644 index 0000000000000..c0a1989d06251 --- /dev/null +++ b/crates/oxc_linter/src/rules/typescript/no_duplicate_enum_values.rs @@ -0,0 +1,203 @@ +use oxc_ast::ast::Expression; +use oxc_ast::AstKind; +use oxc_diagnostics::{ + miette::{self, Diagnostic}, + thiserror::Error, +}; +use oxc_macros::declare_oxc_lint; +use oxc_span::{Atom, Span}; +use rustc_hash::FxHashSet; + +use crate::{context::LintContext, rule::Rule, AstNode}; + +#[derive(Debug, Error, Diagnostic)] +#[error("Disallow duplicate enum member values")] +#[diagnostic(severity(warning))] +struct NoDuplicateEnumValuesDiagnostic(#[label] pub Span); + +#[derive(Debug, Default, Clone)] +pub struct NoDuplicateEnumValues; + +declare_oxc_lint!( + /// ### What it does + /// Disallow duplicate enum member values. + /// + /// ### Why is this bad? + /// Although TypeScript supports duplicate enum member values, people usually expect members to have unique values within the same enum. Duplicate values can lead to bugs that are hard to track down. + /// + /// ### Example + /// ```javascript + /// enum E { + // A = 0, + // B = 0, + // } + /// ``` + NoDuplicateEnumValues, + correctness +); + +impl Rule for NoDuplicateEnumValues { + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + let AstKind::TSEnumBody(enum_body) = node.kind() else { return }; + let mut seen_number_values: Vec = Vec::new(); + let mut seen_string_values: FxHashSet<&Atom> = FxHashSet::default(); + for enum_member in &enum_body.members { + let Some(initializer) = &enum_member.initializer else { continue }; + match initializer { + Expression::NumberLiteral(num) => { + if seen_number_values.contains(&num.value) { + ctx.diagnostic(NoDuplicateEnumValuesDiagnostic(num.span)); + } + seen_number_values.push(num.value); + } + Expression::StringLiteral(str) => { + if seen_string_values.contains(&str.value) { + ctx.diagnostic(NoDuplicateEnumValuesDiagnostic(str.span)); + } + seen_string_values.insert(&str.value); + } + _ => {} + } + } + } +} + +#[allow(clippy::too_many_lines)] +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + ( + " + enum E { + A, + B, + } + ", + None, + ), + ( + " + enum E { + A = 1, + B, + } + ", + None, + ), + ( + " + enum E { + A = 1, + B = 2, + } + ", + None, + ), + ( + " + enum E { + A = 'A', + B = 'B', + } + ", + None, + ), + ( + " + enum E { + A = 'A', + B = 'B', + C, + } + ", + None, + ), + ( + " + enum E { + A = 'A', + B = 'B', + C = 2, + D = 1 + 1, + } + ", + None, + ), + ( + " + enum E { + A = 3, + B = 2, + C, + } + ", + None, + ), + ( + " + enum E { + A = 'A', + B = 'B', + C = 2, + D = foo(), + } + ", + None, + ), + ( + " + enum E { + A = '', + B = 0, + } + ", + None, + ), + ( + " + enum E { + A = 0, + B = -0, + C = NaN, + } + ", + None, + ), + ]; + + let fail = vec![ + ( + " + enum E { + A = 1, + B = 1, + } + ", + None, + ), + ( + " + enum E { + A = 'A', + B = 'A', + } + ", + None, + ), + ( + " + enum E { + A = 'A', + B = 'A', + C = 1, + D = 1, + } + ", + None, + ), + ]; + + Tester::new(NoDuplicateEnumValues::NAME, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/no_duplicate_enum_values.snap b/crates/oxc_linter/src/snapshots/no_duplicate_enum_values.snap new file mode 100644 index 0000000000000..bd7e8ce916713 --- /dev/null +++ b/crates/oxc_linter/src/snapshots/no_duplicate_enum_values.snap @@ -0,0 +1,38 @@ +--- +source: crates/oxc_linter/src/tester.rs +assertion_line: 80 +expression: no_duplicate_enum_values +--- + ⚠ Disallow duplicate enum member values + ╭─[no_duplicate_enum_values.tsx:3:1] + 3 │ A = 1, + 4 │ B = 1, + · ─ + 5 │ } + ╰──── + + ⚠ Disallow duplicate enum member values + ╭─[no_duplicate_enum_values.tsx:3:1] + 3 │ A = 'A', + 4 │ B = 'A', + · ─── + 5 │ } + ╰──── + + ⚠ Disallow duplicate enum member values + ╭─[no_duplicate_enum_values.tsx:3:1] + 3 │ A = 'A', + 4 │ B = 'A', + · ─── + 5 │ C = 1, + ╰──── + + ⚠ Disallow duplicate enum member values + ╭─[no_duplicate_enum_values.tsx:5:1] + 5 │ C = 1, + 6 │ D = 1, + · ─ + 7 │ } + ╰──── + +