fix(linter): detect floating promises through cross-module generic wrappers#9835
Conversation
…appers Two issues prevented `noFloatingPromises` from detecting unhandled promises when async functions were wrapped by cross-module generic identity functions (e.g. `export const fn = trace(asyncFn)` where `trace<F>(fn: F): F`). 1. When types are copied from a module's local (Thin) store to the project-wide (Full) resolver, inner `TypeReference::Resolved` fields inside compound types like `TypeofCallExpression` retained stale Thin-level IDs. Add a remapping pass in `ModuleResolver::resolve_all` that rewrites these inner references to Full-level IDs. 2. `resolve_callee_to_function` did not handle `TypeofExpression` callees, so imported bindings whose types were unflattened call expressions (e.g. `typeof trace(asyncFn)`) could not be resolved to their underlying function type. Add a `TypeofExpression` arm that eagerly flattens the expression. Made-with: Cursor
🦋 Changeset detectedLatest commit: 1335923 The changes in this PR will be included in the next version bump. This PR includes changesets to release 13 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
WalkthroughUpdates enable the Suggested reviewers
🚥 Pre-merge checks | ✅ 2✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.changeset/fix-no-floating-promises-generic-wrappers.md:
- Line 5: Update the changeset description so it begins with the repository
bug-fix prefix; replace the existing first sentence ("The `noFloatingPromises`
rule now detects floating promises through cross-module generic wrapper
functions...") with a line that starts with the bug-fix format, e.g. "Fixed
[`#7332`](https://github.com/biomejs/biome/issues/7332): The `noFloatingPromises`
rule now detects floating promises through cross-module generic wrapper
functions..." to match the required `.changeset/*.md` convention and ensure
release notes parse correctly; edit the file named
.changeset/fix-no-floating-promises-generic-wrappers.md and update the existing
description line accordingly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 4ee854c6-99bb-475d-88ac-d6b94c6a453c
⛔ Files ignored due to path filters (18)
crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/20_invalid.ts.snapis excluded by!**/*.snapand included by**crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/invalidGenericWrapper/index.ts.snapis excluded by!**/*.snapand included by**crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/invalidGenericWrapper/trace.ts.snapis excluded by!**/*.snapand included by**crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/invalidGenericWrapper/wrapper.ts.snapis excluded by!**/*.snapand included by**crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/invalidTwoHopImport/base.ts.snapis excluded by!**/*.snapand included by**crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/invalidTwoHopImport/index.ts.snapis excluded by!**/*.snapand included by**crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/invalidTwoHopImport/reexport.ts.snapis excluded by!**/*.snapand included by**crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/validGenericWrapper/index.ts.snapis excluded by!**/*.snapand included by**crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/validGenericWrapper/trace.ts.snapis excluded by!**/*.snapand included by**crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/validGenericWrapper/wrapper.ts.snapis excluded by!**/*.snapand included by**crates/biome_module_graph/tests/snapshots/test_resolve_generic_mapped_value.snapis excluded by!**/*.snapand included by**crates/biome_module_graph/tests/snapshots/test_resolve_generic_return_value.snapis excluded by!**/*.snapand included by**crates/biome_module_graph/tests/snapshots/test_resolve_generic_return_value_with_multiple_modules.snapis excluded by!**/*.snapand included by**crates/biome_module_graph/tests/snapshots/test_resolve_nested_function_call_with_namespace_in_return_type.snapis excluded by!**/*.snapand included by**crates/biome_module_graph/tests/snapshots/test_resolve_type_of_destructured_field_of_intersection_of_interfaces.snapis excluded by!**/*.snapand included by**crates/biome_module_graph/tests/snapshots/test_resolve_type_of_intersection_of_interfaces.snapis excluded by!**/*.snapand included by**crates/biome_module_graph/tests/snapshots/test_widening_via_assignment.snapis excluded by!**/*.snapand included by**crates/biome_module_graph/tests/snapshots/test_widening_via_assignment_multiple_values.snapis excluded by!**/*.snapand included by**
📒 Files selected for processing (13)
.changeset/fix-no-floating-promises-generic-wrappers.mdcrates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/20_invalid.tscrates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/invalidGenericWrapper/index.tscrates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/invalidGenericWrapper/trace.tscrates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/invalidGenericWrapper/wrapper.tscrates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/invalidTwoHopImport/base.tscrates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/invalidTwoHopImport/index.tscrates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/invalidTwoHopImport/reexport.tscrates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/validGenericWrapper/index.tscrates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/validGenericWrapper/trace.tscrates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/validGenericWrapper/wrapper.tscrates/biome_js_type_info/src/flattening/expressions.rscrates/biome_module_graph/src/js_module_info/module_resolver.rs
… generic wrappers TypeofExpression callees inside union types (e.g. optional calls via `?.`) were being skipped in `flattened_call`, preventing detection. Remove the skip so they fall through to `resolve_callee_to_function` which already handles TypeofExpression flattening. Made-with: Cursor
Can you actually explain it with actual code? Are there tests you can point to? |
Merging this PR will not alter performance
Comparing Footnotes
|
The test for this is |
The Thin→Full inner reference remapping is not required for the floating promise detection fix. Revert the module_resolver.rs changes and associated snapshot updates as requested. Made-with: Cursor
|
This is a great improvement, thanks both! |
Summary
noFloatingPromisesmissed unhandled promises when an async function was wrapped by a generic identity function defined in another module, a common pattern for tracing decorators, instrumentation wrappers, and memoization helpers:The root cause was two gaps in type flattening (
expressions.rs):Unhandled
TypeofExpressioncallees.resolve_callee_to_functioncould resolveFunction,InstanceOf,Interface, andObjectcallees, but notTypeofExpression. When an imported binding's type is an unflattened call expression (e.g.typeof trace(asyncFn)), the callee couldn't be resolved to its underlying function type. Fixed by adding aTypeofExpressionarm that eagerly flattens the expression.TypeofExpressionskipped inside union-typed callees.flattened_callexplicitly skippedTypeofExpressionvariants when iterating through a union callee (e.g.typeof doWork | undefinedfrom an optional callmaybeDoWork?.(...)). This prevented detection even after fix 1. Fixed by removing the skip so these variants fall through toresolve_callee_to_function.Known limitations
Nested/chained generic wrappers (
memoize(trace(asyncFn))) across modules are still not detected. The innerTypeofCallExpressionargument isn't flattened during generic inference inflattened_function_call, so the composed result doesn't resolve to a function type. This is a pre-existing limitation and could be addressed separately.Related issues
Test plan
invalidGenericWrapper/— three-file test (index -> wrapper -> trace) with generictrace<F>(fn: F): F, verifies diagnostics forPromise<void>andPromise<string>return types, plus an optional call through a union-typed binding (maybeDoWork?.("xyz")).validGenericWrapper/— same topology, verifies no false positives when calls are properly handled (await,void,.catch()), and that sync functions through the wrapper are not flagged.invalidTwoHopImport/— two-hop re-export without generics (base -> reexport -> index).20_invalid.ts— single-file generic wrapper (same-module baseline).noFloatingPromisestests, 2326biome_js_analyzespec tests, and 46biome_module_graphtests pass. Clippy clean.