diff --git a/crates/oxc_linter/src/rules/eslint/no_unused_labels.rs b/crates/oxc_linter/src/rules/eslint/no_unused_labels.rs index a015c35e6a454..80dca2aa3d9ed 100644 --- a/crates/oxc_linter/src/rules/eslint/no_unused_labels.rs +++ b/crates/oxc_linter/src/rules/eslint/no_unused_labels.rs @@ -84,6 +84,12 @@ fn test() { None, ), ("A: { var A = 0; console.log(A); break A; console.log(A); }", None), + ( + "label: while (true) { f = function() { label: while (true) { break label; } }; break label; }", + None, + ), + ("outer: { function f() { inner: { break inner; } } break outer; }", None), + ("A: { const f = () => { B: { break B; } }; break A; }", None), ]; let fail = vec![ @@ -96,6 +102,9 @@ fn test() { ("A: { var A = 0; console.log(A); }", None), ("A: /* comment */ foo", None), ("A /* comment */: foo", None), + // Ensure inner label in function is still marked as unused when not used + ("A: { function f() { B: { } } break A; }", None), + ("label: while (true) { (() => { label: while (false) {} })(); }", None), ]; let fix = vec![ ("A: var foo = 0;", "var foo = 0;", None), diff --git a/crates/oxc_linter/src/snapshots/eslint_no_unused_labels.snap b/crates/oxc_linter/src/snapshots/eslint_no_unused_labels.snap index 4d6e748f2846e..c8da03d4c9263 100644 --- a/crates/oxc_linter/src/snapshots/eslint_no_unused_labels.snap +++ b/crates/oxc_linter/src/snapshots/eslint_no_unused_labels.snap @@ -63,3 +63,24 @@ source: crates/oxc_linter/src/tester.rs · ─ ╰──── help: Replace `A /* comment */: foo` with `foo`. + + ⚠ eslint(no-unused-labels): 'B:' is defined but never used. + ╭─[no_unused_labels.tsx:1:21] + 1 │ A: { function f() { B: { } } break A; } + · ─ + ╰──── + help: Replace `B: { }` with `{ }`. + + ⚠ eslint(no-unused-labels): 'label:' is defined but never used. + ╭─[no_unused_labels.tsx:1:32] + 1 │ label: while (true) { (() => { label: while (false) {} })(); } + · ───── + ╰──── + help: Replace `label: while (false) {}` with `while (false) {}`. + + ⚠ eslint(no-unused-labels): 'label:' is defined but never used. + ╭─[no_unused_labels.tsx:1:1] + 1 │ label: while (true) { (() => { label: while (false) {} })(); } + · ───── + ╰──── + help: Replace `label: while (true) { (() => { label: while (false) {} })(); }` with `while (true) { (() => { label: while (false) {} })(); }`. diff --git a/crates/oxc_semantic/src/builder.rs b/crates/oxc_semantic/src/builder.rs index df2904d365127..72efa3b9ca517 100644 --- a/crates/oxc_semantic/src/builder.rs +++ b/crates/oxc_semantic/src/builder.rs @@ -267,6 +267,9 @@ impl<'a> SemanticBuilder<'a> { let jsdoc = if self.build_jsdoc { self.jsdoc.build() } else { JSDocFinder::default() }; + #[cfg(debug_assertions)] + self.unused_labels.assert_empty(); + let semantic = Semantic { source_text: self.source_text, source_type: self.source_type, @@ -1215,7 +1218,7 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { fn visit_labeled_statement(&mut self, stmt: &LabeledStatement<'a>) { let kind = AstKind::LabeledStatement(self.alloc(stmt)); self.enter_node(kind); - self.unused_labels.add(stmt.label.name.as_str()); + self.unused_labels.add(stmt.label.name.as_str(), self.current_node_id); /* cfg */ let label = &stmt.label.name; @@ -1241,7 +1244,7 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { }); /* cfg */ - self.unused_labels.mark_unused(self.current_node_id); + self.unused_labels.mark_unused(); self.leave_node(kind); } diff --git a/crates/oxc_semantic/src/label.rs b/crates/oxc_semantic/src/label.rs index 260c84641662d..699d08410987e 100644 --- a/crates/oxc_semantic/src/label.rs +++ b/crates/oxc_semantic/src/label.rs @@ -4,34 +4,48 @@ use oxc_syntax::node::NodeId; pub struct LabeledScope<'a> { pub name: &'a str, pub used: bool, - pub parent: usize, + pub node_id: NodeId, } #[derive(Debug, Default)] pub struct UnusedLabels<'a> { - pub scopes: Vec>, - pub curr_scope: usize, + pub stack: Vec>, pub labels: Vec, } impl<'a> UnusedLabels<'a> { - pub fn add(&mut self, name: &'a str) { - self.scopes.push(LabeledScope { name, used: false, parent: self.curr_scope }); - self.curr_scope = self.scopes.len() - 1; + pub fn add(&mut self, name: &'a str, node_id: NodeId) { + self.stack.push(LabeledScope { name, used: false, node_id }); } pub fn reference(&mut self, name: &'a str) { - let scope = self.scopes.iter_mut().rev().find(|x| x.name == name); - if let Some(scope) = scope { - scope.used = true; + for scope in self.stack.iter_mut().rev() { + if scope.name == name { + scope.used = true; + return; + } } } - pub fn mark_unused(&mut self, current_node_id: NodeId) { - let scope = &self.scopes[self.curr_scope]; - if !scope.used { - self.labels.push(current_node_id); + pub fn mark_unused(&mut self) { + debug_assert!( + !self.stack.is_empty(), + "mark_unused called with empty label stack - this indicates mismatched add/mark_unused calls" + ); + + if let Some(scope) = self.stack.pop() { + if !scope.used { + self.labels.push(scope.node_id); + } } - self.curr_scope = scope.parent; + } + + #[cfg(debug_assertions)] + pub fn assert_empty(&self) { + debug_assert!( + self.stack.is_empty(), + "Label stack not empty at end of processing - {} labels remaining. This indicates mismatched add/mark_unused calls", + self.stack.len() + ); } }