Skip to content

Commit 18eb0a7

Browse files
fix: prevent ASI hazard in no-unused-labels autofix (#21173)
* fix: prevent ASI hazard in `no-unused-labels` autofix When removing an unused label whose body starts with `(`, `[`, `` ` ``, `/`, `+`, or `-`, the autofix could silently change program semantics by merging the body with the preceding statement via ASI. e.g. `foo()\nLABEL: [1,2,3].forEach(x => x)` was incorrectly fixed to `foo()\n[1,2,3].forEach(x => x)`, which parses as `foo()[1,2,3].forEach(...)`. The fix skips autofixing in those cases when the token before the LabeledStatement is not a safe ASI boundary (`;`, `{`, `}`, `:`). * fix: remove } from safe ASI boundary tokens in no-unused-labels } is not always a safe ASI boundary — e.g. a function expression ending with } can still be continued by [ on the next line: const foo = function() {} LABEL: [1, 2, 3].forEach(x => x) Removing LABEL: here would produce `function() {}[1, 2, 3]` which is a member access, not two separate statements. * Apply suggestion from @mdjermanovic getFirstToken(node.body) always returns a token. Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com> * Update tests/lib/rules/no-unused-labels.js This test was already being caught by the isStaticTemplateLiteral check above Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com> * Update tests/lib/rules/no-unused-labels.js Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com> --------- Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com>
1 parent 0a14800 commit 18eb0a7

2 files changed

Lines changed: 71 additions & 0 deletions

File tree

lib/rules/no-unused-labels.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,25 @@ module.exports = {
9797
return false; // potential directive
9898
}
9999
}
100+
101+
/*
102+
* Do not fix if removing the label would create an ASI hazard.
103+
* e.g. `foo()\nLABEL: [1].forEach(x => x)` → `foo()\n[1].forEach(x => x)`
104+
* would be parsed as `foo()[1].forEach(x => x)`.
105+
*/
106+
const SAFE_TOKENS_BEFORE = /^[:;{]$/u;
107+
const UNSAFE_FIRST_CHARS = /^[([\-+/`]/u;
108+
const tokenBefore = sourceCode.getTokenBefore(node);
109+
const firstBodyToken = sourceCode.getFirstToken(node.body);
110+
111+
if (
112+
tokenBefore &&
113+
!SAFE_TOKENS_BEFORE.test(tokenBefore.value) &&
114+
UNSAFE_FIRST_CHARS.test(firstBodyToken.value)
115+
) {
116+
return false;
117+
}
118+
100119
return true;
101120
}
102121

tests/lib/rules/no-unused-labels.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,58 @@ ruleTester.run("no-unused-labels", rule, {
146146
errors: [{ messageId: "unused", data: { name: "A" } }],
147147
},
148148

149+
// Should not autofix if removing the label would create an ASI hazard
150+
{
151+
code: "foo()\nA: [1, 2, 3].forEach(x => x)",
152+
output: null,
153+
errors: [{ messageId: "unused", data: { name: "A" } }],
154+
},
155+
{
156+
code: "foo()\nA: (function() {})()",
157+
output: null,
158+
errors: [{ messageId: "unused", data: { name: "A" } }],
159+
},
160+
{
161+
code: "foo()\nA: /regex/.test(x)",
162+
output: null,
163+
errors: [{ messageId: "unused", data: { name: "A" } }],
164+
},
165+
{
166+
code: "foo()\nA: +x",
167+
output: null,
168+
errors: [{ messageId: "unused", data: { name: "A" } }],
169+
},
170+
{
171+
code: "foo()\nA: -x",
172+
output: null,
173+
errors: [{ messageId: "unused", data: { name: "A" } }],
174+
},
175+
{
176+
code: "foo()\nA: `template` + bar",
177+
output: null,
178+
languageOptions: { ecmaVersion: 6 },
179+
errors: [{ messageId: "unused", data: { name: "A" } }],
180+
},
181+
182+
// Should not autofix if previous token is } (not always safe, e.g. function expression)
183+
{
184+
code: "const foo = function() {}\nA: [1, 2, 3].forEach(x => x)",
185+
output: null,
186+
errors: [{ messageId: "unused", data: { name: "A" } }],
187+
},
188+
189+
// Should autofix if previous statement ends safely (;, {, :)
190+
{
191+
code: "foo();\nA: [1, 2, 3].forEach(x => x)",
192+
output: "foo();\n[1, 2, 3].forEach(x => x)",
193+
errors: [{ messageId: "unused", data: { name: "A" } }],
194+
},
195+
{
196+
code: "OUTER: while (true) { A: [1, 2, 3].forEach(x => x); break OUTER; }",
197+
output: "OUTER: while (true) { [1, 2, 3].forEach(x => x); break OUTER; }",
198+
errors: [{ messageId: "unused", data: { name: "A" } }],
199+
},
200+
149201
/*
150202
* Below is fatal errors.
151203
* "A: break B",

0 commit comments

Comments
 (0)