Skip to content

Commit cb4d7d7

Browse files
authored
fix(noShadow): detect destructured patterns in sibling scopes (#9344)
1 parent 4509fc6 commit cb4d7d7

File tree

7 files changed

+540
-27
lines changed

7 files changed

+540
-27
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Fixed [#6921](https://github.com/biomejs/biome/issues/6921): `noShadow` no longer incorrectly flags destructured variable bindings in sibling scopes as shadowing. Object destructuring, array destructuring, nested patterns, and rest elements are now properly recognized as declarations.

.claude/skills/testing-codegen/SKILL.md

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -141,15 +141,31 @@ cargo test
141141

142142
### Create Test Files
143143

144-
**Single file tests** - Place in `tests/specs/{group}/{rule}/`:
144+
**Single file tests** - Place in `tests/specs/{group}/{rule}/` under the appropriate `*_analyze` crate for the language:
145145

146146
```
147147
tests/specs/nursery/noVar/
148-
├── invalid.js # Code that triggers the rule
149-
├── valid.js # Code that doesn't trigger
148+
├── invalid.js # Code that should generate diagnostics
149+
├── valid.js # Code that should not generate diagnostics
150150
└── options.json # Optional: rule configuration
151151
```
152152

153+
**File and folder naming conventions (IMPORTANT):**
154+
155+
- Use `valid` or `invalid` in file names or parent folder names to indicate expected behaviour.
156+
- Files/folders with `valid` in the name (but not `invalid`) are expected to produce **no diagnostics**.
157+
- Files/folders with `invalid` in the name are expected to produce **diagnostics**.
158+
- When testing cases inside a folder, prefix the name of folder using `valid`/`invalid` e.g. `validResolutionReact`/`invalidResolutionReact`
159+
160+
```
161+
tests/specs/nursery/noShadow/
162+
├── invalid.js # should generate diagnostics
163+
├── valid.js # should not generate diagnostics
164+
├── validResolutionReact/
165+
└───── file.js # should generate diagnostics
166+
└── file2.js # should not generate diagnostics
167+
```
168+
153169
**Multiple test cases** - Use `.jsonc` files with arrays:
154170

155171
```jsonc
@@ -182,27 +198,19 @@ tests/specs/nursery/noVar/
182198

183199
### Top-Level Comment Convention (REQUIRED)
184200

185-
Every JS/TS test spec file **must** begin with a top-level comment declaring whether it expects diagnostics. The test runner (
186-
`assert_diagnostics_expectation_comment` in `biome_test_utils`) enforces this and panics if the rules are violated.
201+
Every test spec file **must** begin with a top-level comment declaring whether it expects diagnostics. The test runner
202+
(`assert_diagnostics_expectation_comment` in `biome_test_utils`) enforces this and panics if the rules are violated.
187203

188-
**For files whose name contains "valid" (but not "invalid"):**
204+
Write the marker text using whatever comment syntax the language under test supports.
205+
For languages that do not support comments at all, rely on the file/folder naming convention (`valid`/`invalid`) instead.
189206

190-
```js
191-
/* should not generate diagnostics */
192-
import {foo} from "./foo.js";
193-
```
207+
**For files whose name contains "valid" (but not "invalid"):**
194208

195-
This is **enforced** — the test panics if the comment is absent.
209+
The comment is **mandatory** — the test panics if it is absent.
196210

197211
**For files whose name contains "invalid" (or other names):**
198212

199-
```js
200-
/* should generate diagnostics */
201-
var x = 1;
202-
var y = 2;
203-
```
204-
205-
This is strongly recommended convention and is also enforced when present: if the comment says
213+
The comment is strongly recommended and is also enforced when present: if the comment says
206214
`should generate diagnostics` but no diagnostics appear, the test panics.
207215

208216
**Rules enforced by the test runner:**

crates/biome_js_analyze/src/lint/nursery/no_shadow.rs

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ use biome_diagnostics::Severity;
66
use biome_js_semantic::{Binding, SemanticModel};
77
use biome_js_syntax::{
88
JsClassExpression, JsFunctionExpression, JsIdentifierBinding, JsParameterList,
9-
JsVariableDeclarator, TsIdentifierBinding, TsPropertySignatureTypeMember,
10-
TsTypeAliasDeclaration, TsTypeParameter, TsTypeParameterName,
9+
TsIdentifierBinding, TsPropertySignatureTypeMember, TsTypeParameter, TsTypeParameterName,
10+
binding_ext::AnyJsBindingDeclaration,
1111
};
1212
use biome_rowan::{AstNode, SyntaxNodeCast, TokenText, declare_node_union};
1313
use biome_rule_options::no_shadow::NoShadowOptions;
@@ -218,9 +218,14 @@ declare_node_union! {
218218
/// var a = function() { function a() {} };
219219
/// ```
220220
fn is_on_initializer(a: &Binding, b: &Binding) -> bool {
221-
if let Some(b_initializer_expression) = b
222-
.tree()
223-
.parent::<JsVariableDeclarator>()
221+
let b_declarator = b.tree().declaration().and_then(|decl| {
222+
let decl = decl.parent_binding_pattern_declaration().unwrap_or(decl);
223+
match decl {
224+
AnyJsBindingDeclaration::JsVariableDeclarator(d) => Some(d),
225+
_ => None,
226+
}
227+
});
228+
if let Some(b_initializer_expression) = b_declarator
224229
.and_then(|d| d.initializer())
225230
.and_then(|i| i.expression().ok())
226231
&& let Some(a_parent) = a.tree().parent::<AnyIdentifiableExpression>()
@@ -232,17 +237,26 @@ fn is_on_initializer(a: &Binding, b: &Binding) -> bool {
232237
false
233238
}
234239

235-
/// Whether the binding is a declaration or not.
240+
/// Whether the binding is a variable or type alias declaration.
236241
///
237-
/// Examples of declarations:
242+
/// This also handles bindings inside destructuring patterns, e.g.:
238243
/// ```js
239244
/// var a;
240245
/// let b;
241246
/// const c;
247+
/// const { d } = obj;
248+
/// const [e] = arr;
242249
/// ```
243250
fn is_declaration(binding: &Binding) -> bool {
244-
binding.tree().parent::<JsVariableDeclarator>().is_some()
245-
|| binding.tree().parent::<TsTypeAliasDeclaration>().is_some()
251+
let Some(decl) = binding.tree().declaration() else {
252+
return false;
253+
};
254+
let decl = decl.parent_binding_pattern_declaration().unwrap_or(decl);
255+
matches!(
256+
decl,
257+
AnyJsBindingDeclaration::JsVariableDeclarator(_)
258+
| AnyJsBindingDeclaration::TsTypeAliasDeclaration(_)
259+
)
246260
}
247261

248262
fn is_inside_type_parameter(binding: &Binding) -> bool {
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/* should generate diagnostics */
2+
3+
// Object destructuring shadowing outer variable
4+
const x = 1;
5+
function shadowObj() {
6+
const { a: x } = { a: 2 };
7+
}
8+
9+
// Array destructuring shadowing outer variable
10+
const y = 1;
11+
function shadowArr() {
12+
const [y] = [2];
13+
}
14+
15+
// Nested destructuring shadowing outer variable
16+
const z = 1;
17+
function shadowNested() {
18+
const { a: { b: z } } = { a: { b: 2 } };
19+
}
20+
21+
// Shorthand object destructuring shadowing outer variable
22+
const w = 1;
23+
function shadowShorthand() {
24+
const { w } = { w: 2 };
25+
}
26+
27+
// Mixed nested destructuring shadowing outer variable
28+
const m = 1;
29+
function shadowMixed() {
30+
const [{ m }] = [{ m: 2 }];
31+
}
32+
33+
// Rest in array destructuring shadowing outer variable
34+
const rest = 1;
35+
function shadowRestArr() {
36+
const [, ...rest] = [1, 2, 3];
37+
}
38+
39+
// Rest in object destructuring shadowing outer variable
40+
const other = 1;
41+
function shadowRestObj() {
42+
const { a, ...other } = { a: 1, b: 2 };
43+
}

0 commit comments

Comments
 (0)