Skip to content

Commit d8c3e87

Browse files
authored
fix(js_analyze): useErrorCause shorthand property (#10667)
1 parent c2fd653 commit d8c3e87

4 files changed

Lines changed: 117 additions & 58 deletions

File tree

.changeset/full-jobs-admire.md

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 [#10664](https://github.com/biomejs/biome/issues/10664): [useErrorCause](https://biomejs.dev/linter/rules/use-error-cause/) now correctly detects a shorthand property.

crates/biome_js_analyze/src/lint/style/use_error_cause.rs

Lines changed: 72 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
use biome_analyze::{Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule};
22
use biome_console::markup;
33
use biome_js_semantic::SemanticModel;
4-
use biome_js_syntax::{AnyJsBindingPattern, JsCatchClause, JsThrowStatement};
4+
use biome_js_syntax::{
5+
AnyJsBindingPattern, AnyJsObjectMember, JsCatchClause, JsIdentifierBinding,
6+
JsReferenceIdentifier, JsThrowStatement,
7+
};
58
use biome_rowan::{AstNode, AstSeparatedList, TextRange};
69
use biome_rule_options::use_error_cause::UseErrorCauseOptions;
710

@@ -199,32 +202,59 @@ impl Rule for UseErrorCause {
199202
};
200203

201204
for member in obj_expr.members().iter().flatten() {
202-
if let Some(prop) = member.as_js_property_object_member() {
203-
let is_cause_prop = prop
204-
.name()
205-
.ok()
206-
.and_then(|name_node| name_node.name())
207-
.is_some_and(|name| name == "cause");
205+
match member {
206+
AnyJsObjectMember::JsPropertyObjectMember(prop) => {
207+
let is_cause_prop = prop
208+
.name()
209+
.ok()
210+
.and_then(|name_node| name_node.name())
211+
.is_some_and(|name| name == "cause");
208212

209-
if is_cause_prop && let Ok(value) = prop.value() {
210-
match is_cause_value_correct_error(
211-
&value,
212-
identifier_binding,
213-
model,
214-
) {
215-
CauseValueCheckResult::Correct => return None,
216-
CauseValueCheckResult::Shadowed => {
217-
return Some(State::ShadowedCause {
218-
cause_range: value.range(),
219-
catch_binding_range: identifier_binding.range(),
220-
});
213+
if is_cause_prop && let Ok(value) = prop.value() {
214+
match is_cause_value_correct_error(
215+
&value,
216+
identifier_binding,
217+
model,
218+
) {
219+
CauseValueCheckResult::Correct => return None,
220+
CauseValueCheckResult::Shadowed => {
221+
return Some(State::ShadowedCause {
222+
cause_range: value.range(),
223+
catch_binding_range: identifier_binding.range(),
224+
});
225+
}
226+
CauseValueCheckResult::Incorrect => {
227+
// Continue checking other properties, another `cause` might be present.
228+
// This is unlikely to be valid JS, but we handle it.
229+
}
221230
}
222-
CauseValueCheckResult::Incorrect => {
223-
// Continue checking other properties, another `cause` might be present.
224-
// This is unlikely to be valid JS, but we handle it.
231+
}
232+
}
233+
AnyJsObjectMember::JsShorthandPropertyObjectMember(prop) => {
234+
if let Ok(cause_reference) = prop.name()
235+
&& cause_reference
236+
.value_token()
237+
.is_ok_and(|token| token.text_trimmed() == "cause")
238+
{
239+
match is_cause_reference_correct_error(
240+
&cause_reference,
241+
identifier_binding,
242+
model,
243+
) {
244+
CauseValueCheckResult::Correct => return None,
245+
CauseValueCheckResult::Shadowed => {
246+
return Some(State::ShadowedCause {
247+
cause_range: cause_reference.range(),
248+
catch_binding_range: identifier_binding.range(),
249+
});
250+
}
251+
CauseValueCheckResult::Incorrect => {
252+
// Continue checking other properties, another `cause` might be present.
253+
}
225254
}
226255
}
227256
}
257+
_ => {}
228258
}
229259
}
230260

@@ -299,7 +329,7 @@ enum CauseValueCheckResult {
299329

300330
fn is_cause_value_correct_error(
301331
value: &biome_js_syntax::AnyJsExpression,
302-
catch_error_binding: &biome_js_syntax::JsIdentifierBinding,
332+
catch_error_binding: &JsIdentifierBinding,
303333
model: &SemanticModel,
304334
) -> CauseValueCheckResult {
305335
let Some(cause_identifier_expr) = value.as_js_identifier_expression() else {
@@ -309,7 +339,25 @@ fn is_cause_value_correct_error(
309339
return CauseValueCheckResult::Incorrect;
310340
};
311341

312-
let Some(cause_binding) = model.binding(&cause_reference) else {
342+
is_cause_reference_correct_error(&cause_reference, catch_error_binding, model)
343+
}
344+
345+
fn is_cause_reference_correct_error(
346+
cause_reference: &JsReferenceIdentifier,
347+
catch_error_binding: &JsIdentifierBinding,
348+
model: &SemanticModel,
349+
) -> CauseValueCheckResult {
350+
let cause_name = cause_reference.value_token().ok();
351+
let catch_name = catch_error_binding.name_token().ok();
352+
353+
let Some(cause_binding) = model.binding(cause_reference) else {
354+
if cause_name.as_ref().map(|t| t.text_trimmed())
355+
== catch_name.as_ref().map(|t| t.text_trimmed())
356+
{
357+
// Fall back to textual equality when semantic binding data is unavailable.
358+
return CauseValueCheckResult::Correct;
359+
}
360+
313361
return CauseValueCheckResult::Incorrect;
314362
};
315363

@@ -318,12 +366,6 @@ fn is_cause_value_correct_error(
318366
if cause_binding == catch_binding {
319367
CauseValueCheckResult::Correct
320368
} else {
321-
let cause_name = cause_identifier_expr
322-
.name()
323-
.ok()
324-
.and_then(|n| n.value_token().ok());
325-
let catch_name = catch_error_binding.name_token().ok();
326-
327369
if cause_name.as_ref().map(|t| t.text_trimmed())
328370
== catch_name.as_ref().map(|t| t.text_trimmed())
329371
{
Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,41 @@
11
// should not generate diagnostics
22

33
try {
4-
throw new Error("Original error");
4+
throw new Error("Original error");
55
} catch (err) {
6-
throw new Error("Wrapper error", { cause: err });
6+
throw new Error("Wrapper error", { cause: err });
77
}
88

99
try {
10-
throw new Error("Original error");
10+
throw new Error("Original error");
1111
} catch (err) {
12-
console.error("Caught error:", err);
12+
console.error("Caught error:", err);
1313
}
1414

1515

1616
class CustomError extends Error {
17-
constructor(message, options) {
18-
super(message, options);
19-
this.name = "CustomError";
20-
}
17+
constructor(message, options) {
18+
super(message, options);
19+
this.name = "CustomError";
20+
}
2121
}
2222
try {
23-
throw new Error("Original error");
23+
throw new Error("Original error");
2424
} catch (err) {
25-
throw new CustomError("Custom failure", { cause: err });
25+
throw new CustomError("Custom failure", { cause: err });
2626
}
2727

2828

2929
try {
30-
throw new Error("Original error");
30+
throw new Error("Original error");
3131
} catch (err) {
32-
throw new Error(`Failed to process: ${err.message}`, { cause: err });
32+
throw new Error(`Failed to process: ${err.message}`, { cause: err });
3333
}
3434

3535
try {
36-
doSomething();
36+
doSomething();
3737
} catch (e) {
38-
console.error(e);
38+
console.error(e);
3939
}
4040

4141
try {
@@ -47,3 +47,9 @@ try {
4747
}
4848
};
4949
}
50+
51+
try {
52+
throw new Error("Original error");
53+
} catch (cause) {
54+
throw new Error("Wrapper error", { cause });
55+
}

crates/biome_js_analyze/tests/specs/style/useErrorCause/valid.js.snap

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,41 +7,41 @@ expression: valid.js
77
// should not generate diagnostics
88
99
try {
10-
throw new Error("Original error");
10+
throw new Error("Original error");
1111
} catch (err) {
12-
throw new Error("Wrapper error", { cause: err });
12+
throw new Error("Wrapper error", { cause: err });
1313
}
1414
1515
try {
16-
throw new Error("Original error");
16+
throw new Error("Original error");
1717
} catch (err) {
18-
console.error("Caught error:", err);
18+
console.error("Caught error:", err);
1919
}
2020
2121
2222
class CustomError extends Error {
23-
constructor(message, options) {
24-
super(message, options);
25-
this.name = "CustomError";
26-
}
23+
constructor(message, options) {
24+
super(message, options);
25+
this.name = "CustomError";
26+
}
2727
}
2828
try {
29-
throw new Error("Original error");
29+
throw new Error("Original error");
3030
} catch (err) {
31-
throw new CustomError("Custom failure", { cause: err });
31+
throw new CustomError("Custom failure", { cause: err });
3232
}
3333
3434
3535
try {
36-
throw new Error("Original error");
36+
throw new Error("Original error");
3737
} catch (err) {
38-
throw new Error(`Failed to process: ${err.message}`, { cause: err });
38+
throw new Error(`Failed to process: ${err.message}`, { cause: err });
3939
}
4040
4141
try {
42-
doSomething();
42+
doSomething();
4343
} catch (e) {
44-
console.error(e);
44+
console.error(e);
4545
}
4646
4747
try {
@@ -54,4 +54,10 @@ try {
5454
};
5555
}
5656
57+
try {
58+
throw new Error("Original error");
59+
} catch (cause) {
60+
throw new Error("Wrapper error", { cause });
61+
}
62+
5763
```

0 commit comments

Comments
 (0)