Skip to content

Commit 0e56104

Browse files
committed
fix(lint/noFloatingPromises): select the matching overload for floating-promise checks
Calling an overloaded function resolved to a single, arbitrary signature, so `noFloatingPromises` judged the call by the first overload's return type. When that overload returned a promise but the selected one did not, the call was flagged incorrectly (#9568). A set of same-name function declarations now resolves to an object carrying one call signature per declaration. At a call site, the first signature whose parameters accept the arguments is selected, in declaration order, using a narrow promise-aware check plus an arity check (no general assignability). The call's return type is then taken from the selected signature, so a non-promise overload is no longer flagged while a promise-returning one still is.
1 parent 505ac00 commit 0e56104

13 files changed

Lines changed: 327 additions & 1 deletion

File tree

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Fixed [#9568](https://github.com/biomejs/biome/issues/9568): [`noFloatingPromises`](https://biomejs.dev/linter/rules/no-floating-promises/) no longer reports a false positive when calling an overloaded function and the selected overload does not return a promise.
6+
7+
```ts
8+
function bestEffort(cb: () => Promise<number>): Promise<number>;
9+
function bestEffort(cb: () => number): number;
10+
function bestEffort(cb: () => number | Promise<number>): Promise<number> | number {
11+
return cb() as Promise<number> | number;
12+
}
13+
14+
// This resolves to the second overload, which returns `number`, so it is no
15+
// longer flagged as a floating promise.
16+
bestEffort(() => 42);
17+
```
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/* should not generate diagnostics */
2+
3+
function bestEffort<T>(cb: () => Promise<T>): Promise<T | undefined>;
4+
function bestEffort<T>(cb: () => T): T | undefined;
5+
function bestEffort<T>(cb: (() => T) | (() => Promise<T>)): Promise<T | undefined> | T | undefined {
6+
return undefined;
7+
}
8+
9+
function syncWork(): number {
10+
return 42;
11+
}
12+
13+
// Generic overload exactly like the reported issue: selects the second
14+
// overload (sync), so it must NOT be flagged.
15+
bestEffort(syncWork);
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
source: crates/biome_js_analyze/tests/spec_tests.rs
3+
expression: issue9568_generic.ts
4+
---
5+
# Input
6+
```ts
7+
/* should not generate diagnostics */
8+
9+
function bestEffort<T>(cb: () => Promise<T>): Promise<T | undefined>;
10+
function bestEffort<T>(cb: () => T): T | undefined;
11+
function bestEffort<T>(cb: (() => T) | (() => Promise<T>)): Promise<T | undefined> | T | undefined {
12+
return undefined;
13+
}
14+
15+
function syncWork(): number {
16+
return 42;
17+
}
18+
19+
// Generic overload exactly like the reported issue: selects the second
20+
// overload (sync), so it must NOT be flagged.
21+
bestEffort(syncWork);
22+
23+
```
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
function bestEffort(cb: () => Promise<number>): Promise<number>;
2+
function bestEffort(cb: () => number): number;
3+
function bestEffort(cb: () => number | Promise<number>): Promise<number> | number {
4+
return cb() as Promise<number> | number;
5+
}
6+
7+
async function asyncWork(): Promise<number> {
8+
return 42;
9+
}
10+
11+
// Resolves to the first overload (async callback) -> returns a promise,
12+
// so this floating call must still be flagged.
13+
bestEffort(asyncWork);
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
---
2+
source: crates/biome_js_analyze/tests/spec_tests.rs
3+
expression: issue9568_invalid.ts
4+
---
5+
# Input
6+
```ts
7+
function bestEffort(cb: () => Promise<number>): Promise<number>;
8+
function bestEffort(cb: () => number): number;
9+
function bestEffort(cb: () => number | Promise<number>): Promise<number> | number {
10+
return cb() as Promise<number> | number;
11+
}
12+
13+
async function asyncWork(): Promise<number> {
14+
return 42;
15+
}
16+
17+
// Resolves to the first overload (async callback) -> returns a promise,
18+
// so this floating call must still be flagged.
19+
bestEffort(asyncWork);
20+
21+
```
22+
23+
# Diagnostics
24+
```
25+
issue9568_invalid.ts:13:1 lint/nursery/noFloatingPromises ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
26+
27+
i A "floating" Promise was found, meaning it is not properly handled and could lead to ignored errors or unexpected behavior.
28+
29+
11 │ // Resolves to the first overload (async callback) -> returns a promise,
30+
12 │ // so this floating call must still be flagged.
31+
> 13 │ bestEffort(asyncWork);
32+
│ ^^^^^^^^^^^^^^^^^^^^^^
33+
14 │
34+
35+
i This happens when a Promise is not awaited, lacks a `.catch` or `.then` rejection handler, or is not explicitly ignored using the `void` operator.
36+
37+
i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
38+
39+
40+
```
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/* should not generate diagnostics */
2+
3+
function bestEffort(cb: () => Promise<number>): Promise<number>;
4+
function bestEffort(cb: () => number): number;
5+
function bestEffort(cb: () => number | Promise<number>): Promise<number> | number {
6+
return cb() as Promise<number> | number;
7+
}
8+
9+
function syncWork(): number {
10+
return 42;
11+
}
12+
13+
// Resolves to the second overload (sync callback) -> returns `number`,
14+
// which is not a promise, so this must NOT be flagged.
15+
bestEffort(syncWork);
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
source: crates/biome_js_analyze/tests/spec_tests.rs
3+
expression: issue9568_valid.ts
4+
---
5+
# Input
6+
```ts
7+
/* should not generate diagnostics */
8+
9+
function bestEffort(cb: () => Promise<number>): Promise<number>;
10+
function bestEffort(cb: () => number): number;
11+
function bestEffort(cb: () => number | Promise<number>): Promise<number> | number {
12+
return cb() as Promise<number> | number;
13+
}
14+
15+
function syncWork(): number {
16+
return 42;
17+
}
18+
19+
// Resolves to the second overload (sync callback) -> returns `number`,
20+
// which is not a promise, so this must NOT be flagged.
21+
bestEffort(syncWork);
22+
23+
```
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/* should not generate diagnostics */
2+
3+
function run(cb: () => void): Promise<void>;
4+
function run(cb: () => void, immediate: boolean): void;
5+
function run(cb: () => void, immediate?: boolean): Promise<void> | void {
6+
return immediate ? cb() : Promise.resolve();
7+
}
8+
9+
function task(): void {}
10+
11+
// Two arguments select the second overload (returns `void`, not a promise),
12+
// so this must NOT be flagged.
13+
run(task, true);
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
source: crates/biome_js_analyze/tests/spec_tests.rs
3+
expression: overloadSelectedByArity.ts
4+
---
5+
# Input
6+
```ts
7+
/* should not generate diagnostics */
8+
9+
function run(cb: () => void): Promise<void>;
10+
function run(cb: () => void, immediate: boolean): void;
11+
function run(cb: () => void, immediate?: boolean): Promise<void> | void {
12+
return immediate ? cb() : Promise.resolve();
13+
}
14+
15+
function task(): void {}
16+
17+
// Two arguments select the second overload (returns `void`, not a promise),
18+
// so this must NOT be flagged.
19+
run(task, true);
20+
21+
```

crates/biome_js_semantic/src/semantic_model/scope.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,15 @@ impl Scope {
131131
data.bindings_by_name.get(name).cloned()
132132
}
133133

134+
/// Returns the [TsBindingReference] of every name bound in this scope.
135+
pub fn binding_references(&self) -> Vec<TsBindingReference> {
136+
self.data.scopes[self.id.index()]
137+
.bindings_by_name
138+
.values()
139+
.cloned()
140+
.collect()
141+
}
142+
134143
/// Checks if the current scope is one of the ancestor of "other". Given
135144
/// that [Scope::ancestors] return "self" as the first scope,
136145
/// this function returns true for:

0 commit comments

Comments
 (0)