diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index ffd6aa229481..6716e3544423 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -23,6 +23,7 @@ oxc_macros::declare_all_lint_rules! { no_constant_condition, no_compare_neg_zero, no_unsafe_negation, + no_unused_labels, no_bitwise, deepscan::uninvoked_array_callback, deepscan::bad_bitwise_operator, diff --git a/crates/oxc_linter/src/rules/no_unused_labels.rs b/crates/oxc_linter/src/rules/no_unused_labels.rs new file mode 100644 index 000000000000..de7d563a8864 --- /dev/null +++ b/crates/oxc_linter/src/rules/no_unused_labels.rs @@ -0,0 +1,90 @@ +use oxc_ast::{AstKind, Atom, Span}; +use oxc_diagnostics::{ + miette::{self, Diagnostic}, + thiserror::Error, +}; +use oxc_macros::declare_oxc_lint; + +use crate::{context::LintContext, fixer::Fix, rule::Rule, AstNode}; + +#[derive(Debug, Error, Diagnostic)] +#[error("eslint(no-unused-labels): Disallow unused labels")] +#[diagnostic(severity(warning), help("'{0}:' is defined but never used."))] +struct NoUnusedLabelsDiagnostic(Atom, #[label] pub Span); + +#[derive(Debug, Default, Clone)] +pub struct NoUnusedLabels; + +declare_oxc_lint!( + /// ### What it does + /// + /// Disallow unused labels + /// + /// + /// ### Why is this bad? + /// + /// Labels that are declared and not used anywhere in the code are most likely an error due to incomplete refactoring. + /// + /// ### Example + /// ```javascript + /// OUTER_LOOP: + /// for (const student of students) { + /// if (checkScores(student.scores)) { + /// continue; + /// } + /// doSomething(student); + /// } + /// ``` + NoUnusedLabels, + correctness +); + +impl Rule for NoUnusedLabels { + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + if AstKind::Root == node.get().kind() { + for id in ctx.semantic().unused_labels() { + let node = ctx.semantic().nodes()[*id]; + if let AstKind::LabeledStatement(stmt) = node.kind() { + // TODO: Ignore fix where comments exist between label and statement + // e.g. A: /* Comment */ function foo(){} + ctx.diagnostic_with_fix( + NoUnusedLabelsDiagnostic(stmt.label.name.clone(), stmt.label.span), + || Fix::delete(stmt.label.span), + ); + } + } + } + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + ("A: break A;", None), + ("A: { foo(); break A; bar(); }", None), + ("A: if (a) { foo(); if (b) break A; bar(); }", None), + ("A: for (var i = 0; i < 10; ++i) { foo(); if (a) break A; bar(); }", None), + ("A: for (var i = 0; i < 10; ++i) { foo(); if (a) continue A; bar(); }", None), + ( + "A: { B: break B; C: for (var i = 0; i < 10; ++i) { foo(); if (a) break A; if (c) continue C; bar(); } }", + None, + ), + ("A: { var A = 0; console.log(A); break A; console.log(A); }", None), + ]; + + let fail = vec![ + ("A: var foo = 0;", None), + ("A: { foo(); bar(); }", None), + ("A: if (a) { foo(); bar(); }", None), + ("A: for (var i = 0; i < 10; ++i) { foo(); if (a) break; bar(); }", None), + ("A: for (var i = 0; i < 10; ++i) { foo(); if (a) continue; bar(); }", None), + ("A: for (var i = 0; i < 10; ++i) { B: break A; }", None), + ("A: { var A = 0; console.log(A); }", None), + ("A: /* comment */ foo", None), + ("A /* comment */: foo", None), + ]; + + Tester::new(NoUnusedLabels::NAME, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/no_unused_labels.snap b/crates/oxc_linter/src/snapshots/no_unused_labels.snap new file mode 100644 index 000000000000..9ea61edf3c36 --- /dev/null +++ b/crates/oxc_linter/src/snapshots/no_unused_labels.snap @@ -0,0 +1,68 @@ +--- +source: crates/oxc_linter/src/tester.rs +expression: no_unused_labels +--- + + ⚠ eslint(no-unused-labels): Disallow unused labels + ╭─[no_unused_labels.tsx:1:1] + 1 │ A: var foo = 0; + · ─ + ╰──── + help: 'A:' is defined but never used. + + ⚠ eslint(no-unused-labels): Disallow unused labels + ╭─[no_unused_labels.tsx:1:1] + 1 │ A: { foo(); bar(); } + · ─ + ╰──── + help: 'A:' is defined but never used. + + ⚠ eslint(no-unused-labels): Disallow unused labels + ╭─[no_unused_labels.tsx:1:1] + 1 │ A: if (a) { foo(); bar(); } + · ─ + ╰──── + help: 'A:' is defined but never used. + + ⚠ eslint(no-unused-labels): Disallow unused labels + ╭─[no_unused_labels.tsx:1:1] + 1 │ A: for (var i = 0; i < 10; ++i) { foo(); if (a) break; bar(); } + · ─ + ╰──── + help: 'A:' is defined but never used. + + ⚠ eslint(no-unused-labels): Disallow unused labels + ╭─[no_unused_labels.tsx:1:1] + 1 │ A: for (var i = 0; i < 10; ++i) { foo(); if (a) continue; bar(); } + · ─ + ╰──── + help: 'A:' is defined but never used. + + ⚠ eslint(no-unused-labels): Disallow unused labels + ╭─[no_unused_labels.tsx:1:1] + 1 │ A: for (var i = 0; i < 10; ++i) { B: break A; } + · ─ + ╰──── + help: 'B:' is defined but never used. + + ⚠ eslint(no-unused-labels): Disallow unused labels + ╭─[no_unused_labels.tsx:1:1] + 1 │ A: { var A = 0; console.log(A); } + · ─ + ╰──── + help: 'A:' is defined but never used. + + ⚠ eslint(no-unused-labels): Disallow unused labels + ╭─[no_unused_labels.tsx:1:1] + 1 │ A: /* comment */ foo + · ─ + ╰──── + help: 'A:' is defined but never used. + + ⚠ eslint(no-unused-labels): Disallow unused labels + ╭─[no_unused_labels.tsx:1:1] + 1 │ A /* comment */: foo + · ─ + ╰──── + help: 'A:' is defined but never used. + diff --git a/crates/oxc_semantic/src/builder.rs b/crates/oxc_semantic/src/builder.rs index 2af877589b71..5bfeb45cb793 100644 --- a/crates/oxc_semantic/src/builder.rs +++ b/crates/oxc_semantic/src/builder.rs @@ -22,6 +22,18 @@ use crate::{ Semantic, }; +pub struct LabeledScope<'a> { + name: &'a str, + used: bool, + parent: usize, +} + +struct UnusedLabels<'a> { + scopes: Vec>, + curr_scope: usize, + labels: Vec, +} + pub struct SemanticBuilder<'a> { pub source_text: &'a str, @@ -44,6 +56,7 @@ pub struct SemanticBuilder<'a> { with_module_record_builder: bool, pub module_record_builder: ModuleRecordBuilder, + unused_labels: UnusedLabels<'a>, jsdoc: JSDocBuilder<'a>, @@ -76,6 +89,7 @@ impl<'a> SemanticBuilder<'a> { symbols: SymbolTableBuilder::default(), with_module_record_builder: false, module_record_builder: ModuleRecordBuilder::default(), + unused_labels: UnusedLabels { scopes: vec![], curr_scope: 0, labels: vec![] }, jsdoc: JSDocBuilder::new(source_text, trivias), check_syntax_error: false, } @@ -122,6 +136,7 @@ impl<'a> SemanticBuilder<'a> { symbols, module_record, jsdoc: self.jsdoc.build(), + unused_labels: self.unused_labels.labels, }; SemanticBuilderReturn { semantic, errors: self.errors.into_inner() } } @@ -295,6 +310,32 @@ impl<'a> SemanticBuilder<'a> { AstKind::JSXElementName(elem) => { self.reference_jsx_element_name(elem); } + AstKind::LabeledStatement(stmt) => { + self.unused_labels.scopes.push(LabeledScope { + name: stmt.label.name.as_str(), + used: false, + parent: self.unused_labels.curr_scope, + }); + self.unused_labels.curr_scope = self.unused_labels.scopes.len() - 1; + } + AstKind::ContinueStatement(stmt) => { + if let Some(label) = &stmt.label { + let scope = + self.unused_labels.scopes.iter_mut().rev().find(|x| x.name == label.name); + if let Some(scope) = scope { + scope.used = true; + } + } + } + AstKind::BreakStatement(stmt) => { + if let Some(label) = &stmt.label { + let scope = + self.unused_labels.scopes.iter_mut().rev().find(|x| x.name == label.name); + if let Some(scope) = scope { + scope.used = true; + } + } + } _ => {} } } @@ -308,6 +349,13 @@ impl<'a> SemanticBuilder<'a> { AstKind::ModuleDeclaration(decl) => { self.current_symbol_flags -= Self::symbol_flag_from_module_declaration(decl); } + AstKind::LabeledStatement(_) => { + let scope = &self.unused_labels.scopes[self.unused_labels.curr_scope]; + if !scope.used { + self.unused_labels.labels.push(self.current_node_id); + } + self.unused_labels.curr_scope = scope.parent; + } _ => {} } } diff --git a/crates/oxc_semantic/src/lib.rs b/crates/oxc_semantic/src/lib.rs index a17125f475fa..7724565057fa 100644 --- a/crates/oxc_semantic/src/lib.rs +++ b/crates/oxc_semantic/src/lib.rs @@ -41,6 +41,8 @@ pub struct Semantic<'a> { module_record: ModuleRecord, jsdoc: JSDoc<'a>, + + unused_labels: Vec, } impl<'a> Semantic<'a> { @@ -84,6 +86,11 @@ impl<'a> Semantic<'a> { &self.symbols } + #[must_use] + pub fn unused_labels(&self) -> &Vec { + &self.unused_labels + } + #[must_use] pub fn is_unresolved_reference(&self, node_id: AstNodeId) -> bool { let reference_node = &self.nodes()[node_id];