Skip to content

Commit 69fd08d

Browse files
committed
refactor(semantic): improve unused label tracking and add debug assertions (#12812)
closes #12680
1 parent 3957fcc commit 69fd08d

File tree

4 files changed

+63
-16
lines changed

4 files changed

+63
-16
lines changed

crates/oxc_linter/src/rules/eslint/no_unused_labels.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,12 @@ fn test() {
8484
None,
8585
),
8686
("A: { var A = 0; console.log(A); break A; console.log(A); }", None),
87+
(
88+
"label: while (true) { f = function() { label: while (true) { break label; } }; break label; }",
89+
None,
90+
),
91+
("outer: { function f() { inner: { break inner; } } break outer; }", None),
92+
("A: { const f = () => { B: { break B; } }; break A; }", None),
8793
];
8894

8995
let fail = vec![
@@ -96,6 +102,9 @@ fn test() {
96102
("A: { var A = 0; console.log(A); }", None),
97103
("A: /* comment */ foo", None),
98104
("A /* comment */: foo", None),
105+
// Ensure inner label in function is still marked as unused when not used
106+
("A: { function f() { B: { } } break A; }", None),
107+
("label: while (true) { (() => { label: while (false) {} })(); }", None),
99108
];
100109
let fix = vec![
101110
("A: var foo = 0;", "var foo = 0;", None),

crates/oxc_linter/src/snapshots/eslint_no_unused_labels.snap

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,24 @@ source: crates/oxc_linter/src/tester.rs
6363
· ─
6464
╰────
6565
help: Replace `A /* comment */: foo` with `foo`.
66+
67+
eslint(no-unused-labels): 'B:' is defined but never used.
68+
╭─[no_unused_labels.tsx:1:21]
69+
1A: { function f() { B: { } } break A; }
70+
· ─
71+
╰────
72+
help: Replace `B: { }` with `{ }`.
73+
74+
eslint(no-unused-labels): 'label:' is defined but never used.
75+
╭─[no_unused_labels.tsx:1:32]
76+
1label: while (true) { (() => { label: while (false) {} })(); }
77+
· ─────
78+
╰────
79+
help: Replace `label: while (false) {}` with `while (false) {}`.
80+
81+
eslint(no-unused-labels): 'label:' is defined but never used.
82+
╭─[no_unused_labels.tsx:1:1]
83+
1label: while (true) { (() => { label: while (false) {} })(); }
84+
· ─────
85+
╰────
86+
help: Replace `label: while (true) { (() => { label: while (false) {} })(); }` with `while (true) { (() => { label: while (false) {} })(); }`.

crates/oxc_semantic/src/builder.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,9 @@ impl<'a> SemanticBuilder<'a> {
267267

268268
let jsdoc = if self.build_jsdoc { self.jsdoc.build() } else { JSDocFinder::default() };
269269

270+
#[cfg(debug_assertions)]
271+
self.unused_labels.assert_empty();
272+
270273
let semantic = Semantic {
271274
source_text: self.source_text,
272275
source_type: self.source_type,
@@ -1215,7 +1218,7 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> {
12151218
fn visit_labeled_statement(&mut self, stmt: &LabeledStatement<'a>) {
12161219
let kind = AstKind::LabeledStatement(self.alloc(stmt));
12171220
self.enter_node(kind);
1218-
self.unused_labels.add(stmt.label.name.as_str());
1221+
self.unused_labels.add(stmt.label.name.as_str(), self.current_node_id);
12191222

12201223
/* cfg */
12211224
let label = &stmt.label.name;
@@ -1241,7 +1244,7 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> {
12411244
});
12421245
/* cfg */
12431246

1244-
self.unused_labels.mark_unused(self.current_node_id);
1247+
self.unused_labels.mark_unused();
12451248
self.leave_node(kind);
12461249
}
12471250

crates/oxc_semantic/src/label.rs

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,34 +4,48 @@ use oxc_syntax::node::NodeId;
44
pub struct LabeledScope<'a> {
55
pub name: &'a str,
66
pub used: bool,
7-
pub parent: usize,
7+
pub node_id: NodeId,
88
}
99

1010
#[derive(Debug, Default)]
1111
pub struct UnusedLabels<'a> {
12-
pub scopes: Vec<LabeledScope<'a>>,
13-
pub curr_scope: usize,
12+
pub stack: Vec<LabeledScope<'a>>,
1413
pub labels: Vec<NodeId>,
1514
}
1615

1716
impl<'a> UnusedLabels<'a> {
18-
pub fn add(&mut self, name: &'a str) {
19-
self.scopes.push(LabeledScope { name, used: false, parent: self.curr_scope });
20-
self.curr_scope = self.scopes.len() - 1;
17+
pub fn add(&mut self, name: &'a str, node_id: NodeId) {
18+
self.stack.push(LabeledScope { name, used: false, node_id });
2119
}
2220

2321
pub fn reference(&mut self, name: &'a str) {
24-
let scope = self.scopes.iter_mut().rev().find(|x| x.name == name);
25-
if let Some(scope) = scope {
26-
scope.used = true;
22+
for scope in self.stack.iter_mut().rev() {
23+
if scope.name == name {
24+
scope.used = true;
25+
return;
26+
}
2727
}
2828
}
2929

30-
pub fn mark_unused(&mut self, current_node_id: NodeId) {
31-
let scope = &self.scopes[self.curr_scope];
32-
if !scope.used {
33-
self.labels.push(current_node_id);
30+
pub fn mark_unused(&mut self) {
31+
debug_assert!(
32+
!self.stack.is_empty(),
33+
"mark_unused called with empty label stack - this indicates mismatched add/mark_unused calls"
34+
);
35+
36+
if let Some(scope) = self.stack.pop() {
37+
if !scope.used {
38+
self.labels.push(scope.node_id);
39+
}
3440
}
35-
self.curr_scope = scope.parent;
41+
}
42+
43+
#[cfg(debug_assertions)]
44+
pub fn assert_empty(&self) {
45+
debug_assert!(
46+
self.stack.is_empty(),
47+
"Label stack not empty at end of processing - {} labels remaining. This indicates mismatched add/mark_unused calls",
48+
self.stack.len()
49+
);
3650
}
3751
}

0 commit comments

Comments
 (0)