diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 8389370662e5..a09d97288fbc 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -75,6 +75,7 @@ mod eslint { pub mod no_inner_declarations; pub mod no_irregular_whitespace; pub mod no_iterator; + pub mod no_label_var; pub mod no_loss_of_precision; pub mod no_multi_str; pub mod no_new; @@ -452,6 +453,7 @@ oxc_macros::declare_all_lint_rules! { eslint::no_case_declarations, eslint::no_class_assign, eslint::no_multi_str, + eslint::no_label_var, eslint::require_await, eslint::no_compare_neg_zero, eslint::no_cond_assign, diff --git a/crates/oxc_linter/src/rules/eslint/no_label_var.rs b/crates/oxc_linter/src/rules/eslint/no_label_var.rs new file mode 100644 index 000000000000..420e43aa2dee --- /dev/null +++ b/crates/oxc_linter/src/rules/eslint/no_label_var.rs @@ -0,0 +1,79 @@ +use oxc_ast::AstKind; +use oxc_diagnostics::OxcDiagnostic; +use oxc_macros::declare_oxc_lint; +use oxc_span::Span; + +use crate::{context::LintContext, rule::Rule, AstNode}; + +fn no_label_var_diagnostic(x0: &str, span0: Span, span1: Span) -> OxcDiagnostic { + OxcDiagnostic::warn(format!( + "eslint(no-label-var): Found identifier '{x0}' with the same name as a label." + )) + .with_labels([ + span0.label(format!("Identifier '{x0}' found here.")), + span1.label("Label with the same name."), + ]) +} + +#[derive(Debug, Default, Clone)] +pub struct NoLabelVar; + +declare_oxc_lint!( + /// ### What it does + /// + /// Disallow labels that share a name with a variable. + /// + /// ### Why is this bad? + /// + /// This rule aims to create clearer code by disallowing the bad practice of creating a label + /// that shares a name with a variable that is in scope. + /// + /// ### Example + /// ```javascript + /// var x = foo; + /// function bar() { + /// x: + /// for (;;) { + /// break x; + /// } + /// } + /// ``` + NoLabelVar, + style, +); + +impl Rule for NoLabelVar { + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + let AstKind::LabeledStatement(labeled_stmt) = node.kind() else { return }; + + if let Some(symbol_id) = + ctx.scopes().find_binding(node.scope_id(), &labeled_stmt.label.name) + { + let decl_span = ctx.symbols().get_span(symbol_id); + let label_decl = labeled_stmt.span.start; + ctx.diagnostic(no_label_var_diagnostic( + &labeled_stmt.label.name, + decl_span, + Span::new(label_decl, label_decl + 1), + )); + } + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + "function bar() { q: for(;;) { break q; } } function foo () { var q = t; }", + "function bar() { var x = foo; q: for(;;) { break q; } }", + ]; + + let fail = vec![ + "var x = foo; function bar() { x: for(;;) { break x; } }", + "function bar() { var x = foo; x: for(;;) { break x; } }", + "function bar(x) { x: for(;;) { break x; } }", + ]; + + Tester::new(NoLabelVar::NAME, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/no_label_var.snap b/crates/oxc_linter/src/snapshots/no_label_var.snap new file mode 100644 index 000000000000..b4562e8ed7da --- /dev/null +++ b/crates/oxc_linter/src/snapshots/no_label_var.snap @@ -0,0 +1,26 @@ +--- +source: crates/oxc_linter/src/tester.rs +--- + ⚠ eslint(no-label-var): Found identifier 'x' with the same name as a label. + ╭─[no_label_var.tsx:1:5] + 1 │ var x = foo; function bar() { x: for(;;) { break x; } } + · ┬ ┬ + · │ ╰── Label with the same name. + · ╰── Identifier 'x' found here. + ╰──── + + ⚠ eslint(no-label-var): Found identifier 'x' with the same name as a label. + ╭─[no_label_var.tsx:1:22] + 1 │ function bar() { var x = foo; x: for(;;) { break x; } } + · ┬ ┬ + · │ ╰── Label with the same name. + · ╰── Identifier 'x' found here. + ╰──── + + ⚠ eslint(no-label-var): Found identifier 'x' with the same name as a label. + ╭─[no_label_var.tsx:1:14] + 1 │ function bar(x) { x: for(;;) { break x; } } + · ┬ ┬ + · │ ╰── Label with the same name. + · ╰── Identifier 'x' found here. + ╰────