diff --git a/.changeset/fix-type-inference-imported-bindings.md b/.changeset/fix-type-inference-imported-bindings.md new file mode 100644 index 000000000000..bf9877b0610a --- /dev/null +++ b/.changeset/fix-type-inference-imported-bindings.md @@ -0,0 +1,33 @@ +--- +"@biomejs/biome": patch +--- + +Fixed [#7905](https://github.com/biomejs/biome/issues/7905). Improved the accuracy of type-aware lint rules when analyzing re-exported functions and values. + +Previously, when a binding was imported from another module, its type was not correctly inferred during the type analysis phase. This caused type-aware lint rules to fail to detect issues when working with re-exported imports. + +The following rules now correctly handle re-exported imports: +- [`useAwaitThenable`](https://biomejs.dev/linter/rules/use-await-thenable/) +- [`noFloatingPromises`](https://biomejs.dev/linter/rules/no-floating-promises/) +- [`noMisusedPromises`](https://biomejs.dev/linter/rules/no-misused-promises/) +- [`useArraySortCompare`](https://biomejs.dev/linter/rules/use-array-sort-compare/) + +Example of now-working detection: + +```ts +// getValue.ts +export async function getValue(): Promise { + return 42; +} + +// reexport.ts +export { getValue } from "./getValue"; + +// index.ts +import { getValue } from "./reexport"; + +// Previously: no diagnostic (type was unknown) +// Now: correctly detects that getValue() returns a Promise +await getValue(); // Valid - properly awaited +getValue(); // Diagnostic - floating promise +``` diff --git a/Cargo.lock b/Cargo.lock index bd059a056b4a..c77f87ffcaf6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1102,6 +1102,7 @@ dependencies = [ "biome_formatter", "biome_js_formatter", "biome_js_parser", + "biome_js_semantic", "biome_js_syntax", "biome_js_type_info_macros", "biome_resolver", @@ -1755,6 +1756,7 @@ dependencies = [ "biome_fs", "biome_js_analyze", "biome_js_parser", + "biome_js_semantic", "biome_js_type_info", "biome_json_parser", "biome_module_graph", diff --git a/crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/invalidReexportedPromise/getValue.ts b/crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/invalidReexportedPromise/getValue.ts new file mode 100644 index 000000000000..867f7b1dfd55 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/invalidReexportedPromise/getValue.ts @@ -0,0 +1,5 @@ +/* should not generate diagnostics */ + +export async function getValue(): Promise { + return 42; +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/invalidReexportedPromise/getValue.ts.snap b/crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/invalidReexportedPromise/getValue.ts.snap new file mode 100644 index 000000000000..1b7ab6d68733 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/invalidReexportedPromise/getValue.ts.snap @@ -0,0 +1,13 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: getValue.ts +--- +# Input +```ts +/* should not generate diagnostics */ + +export async function getValue(): Promise { + return 42; +} + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/invalidReexportedPromise/index.ts b/crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/invalidReexportedPromise/index.ts new file mode 100644 index 000000000000..20a03a30d2b4 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/invalidReexportedPromise/index.ts @@ -0,0 +1,8 @@ +/* should generate diagnostics */ + +import { getValue } from "./reexport"; + +function test() { + // Invalid: Promise is not handled (no await, no .then, no void operator) + getValue(); +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/invalidReexportedPromise/index.ts.snap b/crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/invalidReexportedPromise/index.ts.snap new file mode 100644 index 000000000000..a5c7f155db49 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/invalidReexportedPromise/index.ts.snap @@ -0,0 +1,36 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: index.ts +--- +# Input +```ts +/* should generate diagnostics */ + +import { getValue } from "./reexport"; + +function test() { + // Invalid: Promise is not handled (no await, no .then, no void operator) + getValue(); +} + +``` + +# Diagnostics +``` +index.ts:7:5 lint/nursery/noFloatingPromises ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i A "floating" Promise was found, meaning it is not properly handled and could lead to ignored errors or unexpected behavior. + + 5 │ function test() { + 6 │ // Invalid: Promise is not handled (no await, no .then, no void operator) + > 7 │ getValue(); + │ ^^^^^^^^^^^ + 8 │ } + 9 │ + + 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. + + 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. + + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/invalidReexportedPromise/reexport.ts b/crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/invalidReexportedPromise/reexport.ts new file mode 100644 index 000000000000..807020105374 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/invalidReexportedPromise/reexport.ts @@ -0,0 +1,3 @@ +/* should not generate diagnostics */ + +export { getValue } from "./getValue"; diff --git a/crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/invalidReexportedPromise/reexport.ts.snap b/crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/invalidReexportedPromise/reexport.ts.snap new file mode 100644 index 000000000000..d1bd71c36728 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/invalidReexportedPromise/reexport.ts.snap @@ -0,0 +1,11 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: reexport.ts +--- +# Input +```ts +/* should not generate diagnostics */ + +export { getValue } from "./getValue"; + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/validReexportedPromise/getValue.ts b/crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/validReexportedPromise/getValue.ts new file mode 100644 index 000000000000..867f7b1dfd55 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/validReexportedPromise/getValue.ts @@ -0,0 +1,5 @@ +/* should not generate diagnostics */ + +export async function getValue(): Promise { + return 42; +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/validReexportedPromise/getValue.ts.snap b/crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/validReexportedPromise/getValue.ts.snap new file mode 100644 index 000000000000..1b7ab6d68733 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/validReexportedPromise/getValue.ts.snap @@ -0,0 +1,13 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: getValue.ts +--- +# Input +```ts +/* should not generate diagnostics */ + +export async function getValue(): Promise { + return 42; +} + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/validReexportedPromise/index.ts b/crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/validReexportedPromise/index.ts new file mode 100644 index 000000000000..82b000b214ca --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/validReexportedPromise/index.ts @@ -0,0 +1,8 @@ +/* should not generate diagnostics */ + +import { getValue } from "./reexport"; + +async function test() { + // Valid: Promise is properly handled with await + await getValue(); +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/validReexportedPromise/index.ts.snap b/crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/validReexportedPromise/index.ts.snap new file mode 100644 index 000000000000..4f8241028ed2 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/validReexportedPromise/index.ts.snap @@ -0,0 +1,16 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: index.ts +--- +# Input +```ts +/* should not generate diagnostics */ + +import { getValue } from "./reexport"; + +async function test() { + // Valid: Promise is properly handled with await + await getValue(); +} + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/validReexportedPromise/reexport.ts b/crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/validReexportedPromise/reexport.ts new file mode 100644 index 000000000000..807020105374 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/validReexportedPromise/reexport.ts @@ -0,0 +1,3 @@ +/* should not generate diagnostics */ + +export { getValue } from "./getValue"; diff --git a/crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/validReexportedPromise/reexport.ts.snap b/crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/validReexportedPromise/reexport.ts.snap new file mode 100644 index 000000000000..d1bd71c36728 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/validReexportedPromise/reexport.ts.snap @@ -0,0 +1,11 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: reexport.ts +--- +# Input +```ts +/* should not generate diagnostics */ + +export { getValue } from "./getValue"; + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noMisusedPromises/invalidReexportedPromise/getValue.ts b/crates/biome_js_analyze/tests/specs/nursery/noMisusedPromises/invalidReexportedPromise/getValue.ts new file mode 100644 index 000000000000..867f7b1dfd55 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noMisusedPromises/invalidReexportedPromise/getValue.ts @@ -0,0 +1,5 @@ +/* should not generate diagnostics */ + +export async function getValue(): Promise { + return 42; +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/noMisusedPromises/invalidReexportedPromise/getValue.ts.snap b/crates/biome_js_analyze/tests/specs/nursery/noMisusedPromises/invalidReexportedPromise/getValue.ts.snap new file mode 100644 index 000000000000..1b7ab6d68733 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noMisusedPromises/invalidReexportedPromise/getValue.ts.snap @@ -0,0 +1,13 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: getValue.ts +--- +# Input +```ts +/* should not generate diagnostics */ + +export async function getValue(): Promise { + return 42; +} + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noMisusedPromises/invalidReexportedPromise/index.ts b/crates/biome_js_analyze/tests/specs/nursery/noMisusedPromises/invalidReexportedPromise/index.ts new file mode 100644 index 000000000000..2d8af492b2ae --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noMisusedPromises/invalidReexportedPromise/index.ts @@ -0,0 +1,10 @@ +/* should generate diagnostics */ + +import { getValue } from "./reexport"; + +function test() { + // Invalid: Promise used in boolean context (if condition) + if (getValue()) { + console.log("test"); + } +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/noMisusedPromises/invalidReexportedPromise/index.ts.snap b/crates/biome_js_analyze/tests/specs/nursery/noMisusedPromises/invalidReexportedPromise/index.ts.snap new file mode 100644 index 000000000000..7e42335ce7ce --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noMisusedPromises/invalidReexportedPromise/index.ts.snap @@ -0,0 +1,40 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: index.ts +--- +# Input +```ts +/* should generate diagnostics */ + +import { getValue } from "./reexport"; + +function test() { + // Invalid: Promise used in boolean context (if condition) + if (getValue()) { + console.log("test"); + } +} + +``` + +# Diagnostics +``` +index.ts:7:9 lint/nursery/noMisusedPromises ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i A Promise was found where a conditional was expected. + + 5 │ function test() { + 6 │ // Invalid: Promise used in boolean context (if condition) + > 7 │ if (getValue()) { + │ ^^^^^^^^^^ + 8 │ console.log("test"); + 9 │ } + + i A Promise is always truthy, so this is most likely a mistake. + + i You may have intended to `await` the Promise instead. + + 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. + + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noMisusedPromises/invalidReexportedPromise/reexport.ts b/crates/biome_js_analyze/tests/specs/nursery/noMisusedPromises/invalidReexportedPromise/reexport.ts new file mode 100644 index 000000000000..807020105374 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noMisusedPromises/invalidReexportedPromise/reexport.ts @@ -0,0 +1,3 @@ +/* should not generate diagnostics */ + +export { getValue } from "./getValue"; diff --git a/crates/biome_js_analyze/tests/specs/nursery/noMisusedPromises/invalidReexportedPromise/reexport.ts.snap b/crates/biome_js_analyze/tests/specs/nursery/noMisusedPromises/invalidReexportedPromise/reexport.ts.snap new file mode 100644 index 000000000000..d1bd71c36728 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noMisusedPromises/invalidReexportedPromise/reexport.ts.snap @@ -0,0 +1,11 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: reexport.ts +--- +# Input +```ts +/* should not generate diagnostics */ + +export { getValue } from "./getValue"; + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noMisusedPromises/validReexportedPromise/getValue.ts b/crates/biome_js_analyze/tests/specs/nursery/noMisusedPromises/validReexportedPromise/getValue.ts new file mode 100644 index 000000000000..867f7b1dfd55 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noMisusedPromises/validReexportedPromise/getValue.ts @@ -0,0 +1,5 @@ +/* should not generate diagnostics */ + +export async function getValue(): Promise { + return 42; +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/noMisusedPromises/validReexportedPromise/getValue.ts.snap b/crates/biome_js_analyze/tests/specs/nursery/noMisusedPromises/validReexportedPromise/getValue.ts.snap new file mode 100644 index 000000000000..1b7ab6d68733 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noMisusedPromises/validReexportedPromise/getValue.ts.snap @@ -0,0 +1,13 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: getValue.ts +--- +# Input +```ts +/* should not generate diagnostics */ + +export async function getValue(): Promise { + return 42; +} + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noMisusedPromises/validReexportedPromise/index.ts b/crates/biome_js_analyze/tests/specs/nursery/noMisusedPromises/validReexportedPromise/index.ts new file mode 100644 index 000000000000..e56385b32d8b --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noMisusedPromises/validReexportedPromise/index.ts @@ -0,0 +1,8 @@ +/* should not generate diagnostics */ + +import { getValue } from "./reexport"; + +async function test() { + // Valid: Promise returned from async function + return getValue(); +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/noMisusedPromises/validReexportedPromise/index.ts.snap b/crates/biome_js_analyze/tests/specs/nursery/noMisusedPromises/validReexportedPromise/index.ts.snap new file mode 100644 index 000000000000..7463c2379bad --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noMisusedPromises/validReexportedPromise/index.ts.snap @@ -0,0 +1,16 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: index.ts +--- +# Input +```ts +/* should not generate diagnostics */ + +import { getValue } from "./reexport"; + +async function test() { + // Valid: Promise returned from async function + return getValue(); +} + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noMisusedPromises/validReexportedPromise/reexport.ts b/crates/biome_js_analyze/tests/specs/nursery/noMisusedPromises/validReexportedPromise/reexport.ts new file mode 100644 index 000000000000..807020105374 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noMisusedPromises/validReexportedPromise/reexport.ts @@ -0,0 +1,3 @@ +/* should not generate diagnostics */ + +export { getValue } from "./getValue"; diff --git a/crates/biome_js_analyze/tests/specs/nursery/noMisusedPromises/validReexportedPromise/reexport.ts.snap b/crates/biome_js_analyze/tests/specs/nursery/noMisusedPromises/validReexportedPromise/reexport.ts.snap new file mode 100644 index 000000000000..d1bd71c36728 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noMisusedPromises/validReexportedPromise/reexport.ts.snap @@ -0,0 +1,11 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: reexport.ts +--- +# Input +```ts +/* should not generate diagnostics */ + +export { getValue } from "./getValue"; + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/useArraySortCompare/invalidReexportedArray/getArray.ts b/crates/biome_js_analyze/tests/specs/nursery/useArraySortCompare/invalidReexportedArray/getArray.ts new file mode 100644 index 000000000000..c02de096a6f3 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useArraySortCompare/invalidReexportedArray/getArray.ts @@ -0,0 +1,5 @@ +/* should not generate diagnostics */ + +export function getArray(): number[] { + return [1, 2, 3]; +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/useArraySortCompare/invalidReexportedArray/getArray.ts.snap b/crates/biome_js_analyze/tests/specs/nursery/useArraySortCompare/invalidReexportedArray/getArray.ts.snap new file mode 100644 index 000000000000..171331382866 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useArraySortCompare/invalidReexportedArray/getArray.ts.snap @@ -0,0 +1,13 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: getArray.ts +--- +# Input +```ts +/* should not generate diagnostics */ + +export function getArray(): number[] { + return [1, 2, 3]; +} + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/useArraySortCompare/invalidReexportedArray/index.ts b/crates/biome_js_analyze/tests/specs/nursery/useArraySortCompare/invalidReexportedArray/index.ts new file mode 100644 index 000000000000..c983266274a2 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useArraySortCompare/invalidReexportedArray/index.ts @@ -0,0 +1,8 @@ +/* should generate diagnostics */ + +import { getArray } from "./reexport"; + +function test() { + // Invalid: Number array sorted without compare function + getArray().sort(); +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/useArraySortCompare/invalidReexportedArray/index.ts.snap b/crates/biome_js_analyze/tests/specs/nursery/useArraySortCompare/invalidReexportedArray/index.ts.snap new file mode 100644 index 000000000000..012c1a138f56 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useArraySortCompare/invalidReexportedArray/index.ts.snap @@ -0,0 +1,38 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: index.ts +--- +# Input +```ts +/* should generate diagnostics */ + +import { getArray } from "./reexport"; + +function test() { + // Invalid: Number array sorted without compare function + getArray().sort(); +} + +``` + +# Diagnostics +``` +index.ts:7:5 lint/nursery/useArraySortCompare ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Compare function missing. + + 5 │ function test() { + 6 │ // Invalid: Number array sorted without compare function + > 7 │ getArray().sort(); + │ ^^^^^^^^^^^^^^^^^ + 8 │ } + 9 │ + + i When called without a compare function, Array#sort() and Array#toSorted() converts all non-undefined array elements into strings and then compares said strings based off their UTF-16 code units. + + i Add a compare function to prevent unexpected sorting. + + 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. + + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/useArraySortCompare/invalidReexportedArray/reexport.ts b/crates/biome_js_analyze/tests/specs/nursery/useArraySortCompare/invalidReexportedArray/reexport.ts new file mode 100644 index 000000000000..c6486c12d10d --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useArraySortCompare/invalidReexportedArray/reexport.ts @@ -0,0 +1,3 @@ +/* should not generate diagnostics */ + +export { getArray } from "./getArray"; diff --git a/crates/biome_js_analyze/tests/specs/nursery/useArraySortCompare/invalidReexportedArray/reexport.ts.snap b/crates/biome_js_analyze/tests/specs/nursery/useArraySortCompare/invalidReexportedArray/reexport.ts.snap new file mode 100644 index 000000000000..8bcf051f4f89 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useArraySortCompare/invalidReexportedArray/reexport.ts.snap @@ -0,0 +1,11 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: reexport.ts +--- +# Input +```ts +/* should not generate diagnostics */ + +export { getArray } from "./getArray"; + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/useArraySortCompare/validReexportedArray/getArray.ts b/crates/biome_js_analyze/tests/specs/nursery/useArraySortCompare/validReexportedArray/getArray.ts new file mode 100644 index 000000000000..5b304827a8aa --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useArraySortCompare/validReexportedArray/getArray.ts @@ -0,0 +1,5 @@ +/* should not generate diagnostics */ + +export function getArray(): string[] { + return ["a", "b", "c"]; +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/useArraySortCompare/validReexportedArray/getArray.ts.snap b/crates/biome_js_analyze/tests/specs/nursery/useArraySortCompare/validReexportedArray/getArray.ts.snap new file mode 100644 index 000000000000..6fc2607f86fd --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useArraySortCompare/validReexportedArray/getArray.ts.snap @@ -0,0 +1,13 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: getArray.ts +--- +# Input +```ts +/* should not generate diagnostics */ + +export function getArray(): string[] { + return ["a", "b", "c"]; +} + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/useArraySortCompare/validReexportedArray/index.ts b/crates/biome_js_analyze/tests/specs/nursery/useArraySortCompare/validReexportedArray/index.ts new file mode 100644 index 000000000000..58909cd51415 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useArraySortCompare/validReexportedArray/index.ts @@ -0,0 +1,8 @@ +/* should not generate diagnostics */ + +import { getArray } from "./reexport"; + +function test() { + // Valid: Array sorted with compare function + getArray().sort((a, b) => a.localeCompare(b)); +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/useArraySortCompare/validReexportedArray/index.ts.snap b/crates/biome_js_analyze/tests/specs/nursery/useArraySortCompare/validReexportedArray/index.ts.snap new file mode 100644 index 000000000000..abbab8fe957e --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useArraySortCompare/validReexportedArray/index.ts.snap @@ -0,0 +1,16 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: index.ts +--- +# Input +```ts +/* should not generate diagnostics */ + +import { getArray } from "./reexport"; + +function test() { + // Valid: Array sorted with compare function + getArray().sort((a, b) => a.localeCompare(b)); +} + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/useArraySortCompare/validReexportedArray/reexport.ts b/crates/biome_js_analyze/tests/specs/nursery/useArraySortCompare/validReexportedArray/reexport.ts new file mode 100644 index 000000000000..c6486c12d10d --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useArraySortCompare/validReexportedArray/reexport.ts @@ -0,0 +1,3 @@ +/* should not generate diagnostics */ + +export { getArray } from "./getArray"; diff --git a/crates/biome_js_analyze/tests/specs/nursery/useArraySortCompare/validReexportedArray/reexport.ts.snap b/crates/biome_js_analyze/tests/specs/nursery/useArraySortCompare/validReexportedArray/reexport.ts.snap new file mode 100644 index 000000000000..8bcf051f4f89 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useArraySortCompare/validReexportedArray/reexport.ts.snap @@ -0,0 +1,11 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: reexport.ts +--- +# Input +```ts +/* should not generate diagnostics */ + +export { getArray } from "./getArray"; + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/useAwaitThenable/reexportedImport/getValue.ts b/crates/biome_js_analyze/tests/specs/nursery/useAwaitThenable/reexportedImport/getValue.ts new file mode 100644 index 000000000000..1e7b70119ebe --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useAwaitThenable/reexportedImport/getValue.ts @@ -0,0 +1,8 @@ +/* should not generate diagnostics */ + +/** + * @returns {Promise} + */ +export async function getValue(): Promise { + return 42; +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/useAwaitThenable/reexportedImport/getValue.ts.snap b/crates/biome_js_analyze/tests/specs/nursery/useAwaitThenable/reexportedImport/getValue.ts.snap new file mode 100644 index 000000000000..b95df36a3b8e --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useAwaitThenable/reexportedImport/getValue.ts.snap @@ -0,0 +1,16 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: getValue.ts +--- +# Input +```ts +/* should not generate diagnostics */ + +/** + * @returns {Promise} + */ +export async function getValue(): Promise { + return 42; +} + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/useAwaitThenable/reexportedImport/index.ts b/crates/biome_js_analyze/tests/specs/nursery/useAwaitThenable/reexportedImport/index.ts new file mode 100644 index 000000000000..e58c51e1e797 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useAwaitThenable/reexportedImport/index.ts @@ -0,0 +1,8 @@ +/* should not generate diagnostics */ + +import { getValue } from "./reexport"; + +async function test() { + // This should be valid - getValue returns a Promise + await getValue(); +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/useAwaitThenable/reexportedImport/index.ts.snap b/crates/biome_js_analyze/tests/specs/nursery/useAwaitThenable/reexportedImport/index.ts.snap new file mode 100644 index 000000000000..6c241af44df4 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useAwaitThenable/reexportedImport/index.ts.snap @@ -0,0 +1,16 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: index.ts +--- +# Input +```ts +/* should not generate diagnostics */ + +import { getValue } from "./reexport"; + +async function test() { + // This should be valid - getValue returns a Promise + await getValue(); +} + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/useAwaitThenable/reexportedImport/reexport.ts b/crates/biome_js_analyze/tests/specs/nursery/useAwaitThenable/reexportedImport/reexport.ts new file mode 100644 index 000000000000..807020105374 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useAwaitThenable/reexportedImport/reexport.ts @@ -0,0 +1,3 @@ +/* should not generate diagnostics */ + +export { getValue } from "./getValue"; diff --git a/crates/biome_js_analyze/tests/specs/nursery/useAwaitThenable/reexportedImport/reexport.ts.snap b/crates/biome_js_analyze/tests/specs/nursery/useAwaitThenable/reexportedImport/reexport.ts.snap new file mode 100644 index 000000000000..d1bd71c36728 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useAwaitThenable/reexportedImport/reexport.ts.snap @@ -0,0 +1,11 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: reexport.ts +--- +# Input +```ts +/* should not generate diagnostics */ + +export { getValue } from "./getValue"; + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/useAwaitThenable/reexportedNonPromise/getValue.ts b/crates/biome_js_analyze/tests/specs/nursery/useAwaitThenable/reexportedNonPromise/getValue.ts new file mode 100644 index 000000000000..5275d6a799ac --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useAwaitThenable/reexportedNonPromise/getValue.ts @@ -0,0 +1,8 @@ +/* should not generate diagnostics */ + +/** + * @returns {number} + */ +export function getValue(): number { + return 42; +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/useAwaitThenable/reexportedNonPromise/getValue.ts.snap b/crates/biome_js_analyze/tests/specs/nursery/useAwaitThenable/reexportedNonPromise/getValue.ts.snap new file mode 100644 index 000000000000..d9ea695ae8a8 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useAwaitThenable/reexportedNonPromise/getValue.ts.snap @@ -0,0 +1,16 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: getValue.ts +--- +# Input +```ts +/* should not generate diagnostics */ + +/** + * @returns {number} + */ +export function getValue(): number { + return 42; +} + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/useAwaitThenable/reexportedNonPromise/index.ts b/crates/biome_js_analyze/tests/specs/nursery/useAwaitThenable/reexportedNonPromise/index.ts new file mode 100644 index 000000000000..40cc64bd4a50 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useAwaitThenable/reexportedNonPromise/index.ts @@ -0,0 +1,8 @@ +/* should generate diagnostics */ + +import { getValue } from "./reexport"; + +async function test() { + // This should trigger the rule - getValue returns a number, not a Promise + await getValue(); +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/useAwaitThenable/reexportedNonPromise/index.ts.snap b/crates/biome_js_analyze/tests/specs/nursery/useAwaitThenable/reexportedNonPromise/index.ts.snap new file mode 100644 index 000000000000..4ed099c9bed1 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useAwaitThenable/reexportedNonPromise/index.ts.snap @@ -0,0 +1,38 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: index.ts +--- +# Input +```ts +/* should generate diagnostics */ + +import { getValue } from "./reexport"; + +async function test() { + // This should trigger the rule - getValue returns a number, not a Promise + await getValue(); +} + +``` + +# Diagnostics +``` +index.ts:7:5 lint/nursery/useAwaitThenable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i `await` used on a non-Promise value. + + 5 │ async function test() { + 6 │ // This should trigger the rule - getValue returns a number, not a Promise + > 7 │ await getValue(); + │ ^^^^^^^^^^^^^^^^ + 8 │ } + 9 │ + + i This may happen if you accidentally used `await` on a synchronous value. + + i Please ensure the value is not a custom "thenable" implementation before removing the `await`: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise#thenables + + 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. + + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/useAwaitThenable/reexportedNonPromise/reexport.ts b/crates/biome_js_analyze/tests/specs/nursery/useAwaitThenable/reexportedNonPromise/reexport.ts new file mode 100644 index 000000000000..807020105374 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useAwaitThenable/reexportedNonPromise/reexport.ts @@ -0,0 +1,3 @@ +/* should not generate diagnostics */ + +export { getValue } from "./getValue"; diff --git a/crates/biome_js_analyze/tests/specs/nursery/useAwaitThenable/reexportedNonPromise/reexport.ts.snap b/crates/biome_js_analyze/tests/specs/nursery/useAwaitThenable/reexportedNonPromise/reexport.ts.snap new file mode 100644 index 000000000000..d1bd71c36728 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useAwaitThenable/reexportedNonPromise/reexport.ts.snap @@ -0,0 +1,11 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: reexport.ts +--- +# Input +```ts +/* should not generate diagnostics */ + +export { getValue } from "./getValue"; + +``` diff --git a/crates/biome_js_semantic/src/format_semantic_model.rs b/crates/biome_js_semantic/src/format_semantic_model.rs index ec7c0fc9fb43..8bfa22ccccc4 100644 --- a/crates/biome_js_semantic/src/format_semantic_model.rs +++ b/crates/biome_js_semantic/src/format_semantic_model.rs @@ -8,7 +8,7 @@ use biome_js_syntax::TextSize; use crate::{Binding, BindingId, Scope, ScopeId, SemanticModel}; -struct FormatSemanticModelOptions; +pub struct FormatSemanticModelOptions; impl FormatOptions for FormatSemanticModelOptions { fn indent_style(&self) -> IndentStyle { @@ -42,7 +42,7 @@ impl FormatOptions for FormatSemanticModelOptions { } } -struct FormatSemanticModelContext; +pub(crate) struct FormatSemanticModelContext; impl FormatContext for FormatSemanticModelContext { type Options = FormatSemanticModelOptions; diff --git a/crates/biome_js_semantic/src/semantic_model/binding.rs b/crates/biome_js_semantic/src/semantic_model/binding.rs index 80c36f8c7a44..8095f47f0915 100644 --- a/crates/biome_js_semantic/src/semantic_model/binding.rs +++ b/crates/biome_js_semantic/src/semantic_model/binding.rs @@ -1,5 +1,7 @@ use super::*; +use crate::format_semantic_model::FormatSemanticModelContext; use biome_js_syntax::{TextRange, TsTypeParameterName, binding_ext::AnyJsIdentifierBinding}; +use std::fmt::{Display, Formatter}; use std::sync::Arc; /// Internal type with all the semantic data of a specific binding @@ -55,6 +57,19 @@ impl std::fmt::Debug for Binding { } } +impl Display for Binding { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let formatted = biome_formatter::format!(FormatSemanticModelContext, [&self]) + .expect("Formatting not to throw any FormatErrors"); + f.write_str( + formatted + .print() + .expect("Expected a valid document") + .as_code(), + ) + } +} + impl Binding { /// Returns the scope of this binding pub fn scope(&self) -> Scope { diff --git a/crates/biome_js_semantic/src/semantic_model/model.rs b/crates/biome_js_semantic/src/semantic_model/model.rs index cc36a8c5e6a4..9c8dd81a027a 100644 --- a/crates/biome_js_semantic/src/semantic_model/model.rs +++ b/crates/biome_js_semantic/src/semantic_model/model.rs @@ -7,13 +7,13 @@ use std::sync::Arc; pub struct BindingId(pub(crate) u32); impl BindingId { - pub fn new(index: usize) -> Self { + pub const fn new(index: usize) -> Self { // SAFETY: We didn't handle files exceeding `u32::MAX` bytes. // Thus, it isn't possible to exceed `u32::MAX` bindings. Self(index as u32) } - pub fn index(self) -> usize { + pub const fn index(self) -> usize { self.0 as usize } } @@ -45,7 +45,9 @@ pub struct ScopeId(pub(crate) std::num::NonZeroU32); // We don't implement `From for ScopeId` and `From for usize` // to ensure that the API consumers don't create `ScopeId`. impl ScopeId { - pub fn new(index: usize) -> Self { + pub const GLOBAL: Self = Self::new(0); + + pub const fn new(index: usize) -> Self { // SAFETY: We didn't handle files exceeding `u32::MAX` bytes. // Thus, it isn't possible to exceed `u32::MAX` scopes. // @@ -57,7 +59,7 @@ impl ScopeId { Self(unsafe { std::num::NonZeroU32::new_unchecked(index.unchecked_add(1) as u32) }) } - pub fn index(self) -> usize { + pub const fn index(self) -> usize { // SAFETY: The internal representation ensures that the value is never equal to 0. // Thus, it is safe to subtract 1. (unsafe { self.0.get().unchecked_sub(1) }) as usize @@ -239,6 +241,40 @@ impl SemanticModel { } } + /// Returns the most specific [Scope] that covers the given range. + /// + /// This is useful when you have a text range but not a syntax node. + /// If you have a syntax node, prefer using [SemanticModel::scope] instead. + /// + /// # Panics + /// Panics if the range is empty (has zero length). + pub fn scope_for_range(&self, range: TextRange) -> Scope { + let id = self.data.scope(range); + Scope { + data: self.data.clone(), + id, + } + } + + /// Creates a [Scope] from a [ScopeId]. + /// + /// This is useful when you have a scope ID and need to access its data. + /// + /// # Panics + /// Panics in debug mode if the scope_id is invalid. + pub fn scope_from_id(&self, scope_id: ScopeId) -> Scope { + debug_assert!( + scope_id.index() < self.data.scopes.len(), + "Invalid scope_id: index {} is out of bounds (max: {})", + scope_id.index(), + self.data.scopes.len() + ); + Scope { + data: self.data.clone(), + id: scope_id, + } + } + /// Returns the [Scope] which the specified syntax node was hoisted to, if any. /// Can also be called from [AstNode]::scope_hoisted_to extension method. /// diff --git a/crates/biome_js_semantic/src/semantic_model/scope.rs b/crates/biome_js_semantic/src/semantic_model/scope.rs index 1eea5a43178a..9fc628b0beb8 100644 --- a/crates/biome_js_semantic/src/semantic_model/scope.rs +++ b/crates/biome_js_semantic/src/semantic_model/scope.rs @@ -41,6 +41,11 @@ impl PartialEq for Scope { impl Eq for Scope {} impl Scope { + /// Returns the unique identifier for this scope. + pub fn id(&self) -> ScopeId { + self.id + } + pub fn is_global_scope(&self) -> bool { self.id.index() == 0 } diff --git a/crates/biome_js_type_info/Cargo.toml b/crates/biome_js_type_info/Cargo.toml index 26f248adbe01..ecb626a8f0a7 100644 --- a/crates/biome_js_type_info/Cargo.toml +++ b/crates/biome_js_type_info/Cargo.toml @@ -13,6 +13,7 @@ publish = true [dependencies] biome_formatter = { workspace = true } +biome_js_semantic = { workspace = true } biome_js_syntax = { workspace = true } biome_js_type_info_macros = { workspace = true } biome_resolver = { workspace = true } diff --git a/crates/biome_js_type_info/src/type_data.rs b/crates/biome_js_type_info/src/type_data.rs index 8478a7d944ab..cf452f844b27 100644 --- a/crates/biome_js_type_info/src/type_data.rs +++ b/crates/biome_js_type_info/src/type_data.rs @@ -1454,27 +1454,16 @@ impl TypeReferenceQualifier { } } -#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] -pub struct BindingId(u32); - -impl BindingId { - pub const fn new(index: usize) -> Self { - // SAFETY: We don't handle files exceeding `u32::MAX` bytes. - // Thus, it isn't possible to exceed `u32::MAX` bindings. - Self(index as u32) - } - - pub const fn index(self) -> usize { - self.0 as usize - } -} +// Re-export BindingId and ScopeId from biome_js_semantic to avoid duplication. +// These types represent the same semantic concepts and should have a single source of truth. +pub use biome_js_semantic::{BindingId, ScopeId}; // We allow conversion from `BindingId` into `TypeId`, and vice versa, because // for project-level `ResolvedTypeId` instances, the `TypeId` is an indirection // that is resolved through a binding. impl From for TypeId { fn from(id: BindingId) -> Self { - Self::new(id.0 as usize) + Self::new(id.index()) } } @@ -1484,34 +1473,6 @@ impl From for BindingId { } } -// We use `NonZeroU32` to allow niche optimizations. -#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] -pub struct ScopeId(pub(crate) std::num::NonZeroU32); - -// We don't implement `From for ScopeId` and `From for usize` -// to ensure that the API consumers don't create `ScopeId`. -impl ScopeId { - pub const GLOBAL: Self = Self::new(0); - - pub const fn new(index: usize) -> Self { - // SAFETY: We don't handle files exceeding `u32::MAX` bytes. - // Thus, it isn't possible to exceed `u32::MAX` scopes. - // - // Adding 1 ensures that the value is never equal to 0. - // Instead of adding 1, we could XOR the value with `u32::MAX`. - // This is what the [nonmax](https://docs.rs/nonmax/latest/nonmax/) crate does. - // However, this doesn't preserve the order. - // It is why we opted for adding 1. - Self(unsafe { std::num::NonZeroU32::new_unchecked(index.unchecked_add(1) as u32) }) - } - - pub const fn index(self) -> usize { - // SAFETY: The internal representation ensures that the value is never equal to 0. - // Thus, it is safe to subtract 1. - (unsafe { self.0.get().unchecked_sub(1) }) as usize - } -} - /// Accessibility of a type member. #[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq, Resolvable)] pub enum TypeMemberAccessibility { diff --git a/crates/biome_js_type_info/tests/local_inference.rs b/crates/biome_js_type_info/tests/local_inference.rs index a899fc73851f..45e8057345ad 100644 --- a/crates/biome_js_type_info/tests/local_inference.rs +++ b/crates/biome_js_type_info/tests/local_inference.rs @@ -1,6 +1,7 @@ mod utils; -use biome_js_type_info::{GlobalsResolver, ScopeId, TypeData}; +use biome_js_semantic::ScopeId; +use biome_js_type_info::{GlobalsResolver, TypeData}; use utils::{ assert_type_data_snapshot, assert_typed_bindings_snapshot, get_expression, diff --git a/crates/biome_js_type_info/tests/resolver.rs b/crates/biome_js_type_info/tests/resolver.rs index a676229e6b2d..0f642011a361 100644 --- a/crates/biome_js_type_info/tests/resolver.rs +++ b/crates/biome_js_type_info/tests/resolver.rs @@ -1,7 +1,8 @@ mod utils; +use biome_js_semantic::ScopeId; use biome_js_syntax::{AnyJsModuleItem, AnyJsRoot, AnyJsStatement, JsExpressionStatement}; -use biome_js_type_info::{GlobalsResolver, Resolvable, ScopeId, TypeData}; +use biome_js_type_info::{GlobalsResolver, Resolvable, TypeData}; use utils::{ HardcodedSymbolResolver, assert_type_data_snapshot, assert_typed_bindings_snapshot, diff --git a/crates/biome_module_graph/benches/module_graph.rs b/crates/biome_module_graph/benches/module_graph.rs index 20e59ec9b1ca..639c95b24b24 100644 --- a/crates/biome_module_graph/benches/module_graph.rs +++ b/crates/biome_module_graph/benches/module_graph.rs @@ -1,9 +1,11 @@ use biome_fs::{BiomePath, FileSystem, MemoryFileSystem}; use biome_js_parser::JsParserOptions; +use biome_js_semantic::{SemanticModelOptions, semantic_model}; use biome_js_syntax::{AnyJsRoot, JsFileSource}; use biome_module_graph::ModuleGraph; use biome_project_layout::ProjectLayout; use divan::Bencher; +use std::sync::Arc; #[cfg(target_os = "windows")] #[global_allocator] @@ -65,14 +67,15 @@ fn bench_index_d_ts(bencher: Bencher, name: &str) { let path = BiomePath::new(name); let root = get_js_root(&fs, &path); - (fs, path, root) + let semantic_model = Arc::new(semantic_model(&root, SemanticModelOptions::default())); + (fs, path, root, semantic_model) }) - .bench_local_values(|(fs, path, root)| { + .bench_local_values(|(fs, path, root, semantic_model)| { let module_graph = ModuleGraph::default(); module_graph.update_graph_for_js_paths( &fs, &ProjectLayout::default(), - &[(&path, root)], + &[(&path, root, semantic_model)], true, ); divan::black_box(&module_graph); diff --git a/crates/biome_module_graph/src/format_module_graph.rs b/crates/biome_module_graph/src/format_module_graph.rs index c4b7c6ec6961..2c1ddfd98c22 100644 --- a/crates/biome_module_graph/src/format_module_graph.rs +++ b/crates/biome_module_graph/src/format_module_graph.rs @@ -1,9 +1,9 @@ use crate::js_module_info::{Exports, Imports, JsBindingData}; -use crate::{JsExport, JsImport, JsModuleInfo, JsOwnExport, JsReexport}; +use crate::{BindingTypeData, JsExport, JsImport, JsModuleInfo, JsOwnExport, JsReexport}; use biome_formatter::prelude::*; use biome_formatter::{format_args, write}; use biome_js_type_info::FormatTypeContext; -use biome_rowan::TextSize; +use biome_rowan::{TextRange, TextSize}; use std::fmt::Formatter; use std::ops::Deref; @@ -20,6 +20,52 @@ impl std::fmt::Display for JsModuleInfo { } } +impl Format for BindingTypeData { + fn fmt( + &self, + f: &mut biome_formatter::formatter::Formatter, + ) -> FormatResult<()> { + let ranges: Vec = self + .export_ranges + .iter() + .map(|range| range.into()) + .collect(); + let export_ranges = format_with(|f| { + let mut join = f.join(); + + for range in ranges.clone() { + join.entry(&range); + } + join.finish() + }); + + let jsdoc = format_with(|f| { + if self.jsdoc.is_some() { + write!(f, [&self.jsdoc, token(","), hard_line_break()])?; + }; + Ok(()) + }); + write!( + f, + [ + token("BindingTypeData {"), + &group(&block_indent(&format_args![ + token("Types "), + &self.ty, + token(","), + hard_line_break(), + jsdoc, + token("Exported Ranges: "), + &export_ranges + ])), + token("}") + ] + )?; + + Ok(()) + } +} + impl Format for JsModuleInfo { fn fmt( &self, @@ -310,14 +356,17 @@ impl Format for JsOwnExport { f: &mut biome_formatter::formatter::Formatter, ) -> FormatResult<()> { match self { - Self::Binding(binding_id) => write!( - f, - [&format_args![ - token("JsOwnExport::Binding("), - text(&binding_id.index().to_string(), TextSize::default()), - token(")") - ]] - ), + Self::Binding(binding_range) => { + let range_str = std::format!("{:?}", binding_range); + write!( + f, + [&format_args![ + token("JsOwnExport::Binding("), + text(&range_str, TextSize::default()), + token(")") + ]] + ) + } Self::Type(resolved_type_id) => write!( f, [&format_args![ @@ -365,3 +414,48 @@ impl Format for JsImport { Ok(()) } } +#[derive(Clone)] +struct TypedRange(TextRange); + +impl From<&TextRange> for TypedRange { + fn from(value: &TextRange) -> Self { + Self(*value) + } +} + +#[derive(Clone)] +struct TypedSize(TextSize); + +impl From for TypedSize { + fn from(value: TextSize) -> Self { + Self(value) + } +} + +impl Format for TypedSize { + fn fmt( + &self, + f: &mut biome_formatter::formatter::Formatter, + ) -> FormatResult<()> { + let value = std::format!("{}", self.0); + write!(f, [text(&value, TextSize::default())]) + } +} + +impl Format for TypedRange { + fn fmt( + &self, + f: &mut biome_formatter::formatter::Formatter, + ) -> FormatResult<()> { + write!( + f, + [ + token("("), + TypedSize::from(self.0.start()), + token(".."), + TypedSize::from(self.0.end()), + token(")") + ] + ) + } +} diff --git a/crates/biome_module_graph/src/js_module_info.rs b/crates/biome_module_graph/src/js_module_info.rs index 84f9a407d84d..e677cfa174f5 100644 --- a/crates/biome_module_graph/src/js_module_info.rs +++ b/crates/biome_module_graph/src/js_module_info.rs @@ -6,8 +6,12 @@ mod scope; mod utils; mod visitor; +use crate::ModuleGraph; +use biome_js_semantic::ScopeId; use biome_js_syntax::AnyJsImportLike; -use biome_js_type_info::{BindingId, ImportSymbol, ResolvedTypeId, ScopeId, TypeData}; +use biome_js_type_info::{ + FormatTypeContext, ImportSymbol, ResolvedTypeId, TypeData, TypeReference, +}; use biome_jsdoc_comment::JsdocComment; use biome_resolver::ResolvedPath; use biome_rowan::{Text, TextRange}; @@ -16,11 +20,10 @@ use indexmap::IndexMap; use rust_lapper::Lapper; use rustc_hash::FxHashMap; use std::collections::BTreeSet; +use std::fmt::{Display, Formatter}; use std::{collections::BTreeMap, ops::Deref, sync::Arc}; -use crate::ModuleGraph; - -use scope::{JsScope, JsScopeData, TsBindingReference}; +use scope::JsScope; use crate::diagnostics::ModuleDiagnostic; pub(super) use binding::JsBindingData; @@ -28,6 +31,35 @@ pub use diagnostics::JsModuleInfoDiagnostic; pub use module_resolver::ModuleResolver; pub(crate) use visitor::JsModuleVisitor; +/// Type augmentation data for a binding from the semantic model. +/// +/// Stores type inference results and JSDoc comments that enrich the +/// base binding information from the semantic model. +#[derive(Debug, Clone)] +pub struct BindingTypeData { + /// The inferred type of this binding. + pub ty: TypeReference, + + /// JSDoc comment associated with this binding, if any. + pub jsdoc: Option, + + /// Ranges where this binding is exported. + pub export_ranges: Vec, +} + +impl Display for BindingTypeData { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let formatted = biome_formatter::format!(FormatTypeContext, [&self]) + .expect("Formatting not to throw any FormatErrors"); + f.write_str( + formatted + .print() + .expect("Expected a valid document") + .as_code(), + ) + } +} + /// Information restricted to a single module in the [ModuleGraph]. #[derive(Clone, Debug)] pub struct JsModuleInfo(pub(super) Arc); @@ -80,15 +112,23 @@ impl JsModuleInfo { pub fn global_scope(&self) -> JsScope { JsScope { info: self.0.clone(), - id: ScopeId::new(0), + scope: self.0.semantic_model.global_scope(), } } /// Returns the scope to be used for the given `range`. + /// + /// This finds the most specific scope that covers the given text range. + /// If the range is empty, returns the global scope. pub fn scope_for_range(&self, range: TextRange) -> JsScope { + // Guard against empty ranges - semantic_model.scope_for_range debug-asserts on empty ranges + if range.len() == 0.into() { + return self.global_scope(); + } + JsScope { info: self.0.clone(), - id: scope_id_for_range(&self.0.scope_by_range, range), + scope: self.0.semantic_model.scope_for_range(range), } } @@ -184,19 +224,20 @@ pub struct JsModuleInfoInner { /// assigning a name to them. pub blanket_reexports: Vec, - /// Collection of all the declarations in the module. - pub(crate) bindings: Vec, - - /// Parsed expressions, mapped from their range to their type ID. - pub(crate) expressions: FxHashMap, + /// Semantic model provided by the workspace service. + /// + /// Contains scope and binding information. This replaces the previous + /// duplicated scope/binding tracking that was built from semantic events. + pub semantic_model: std::sync::Arc, - /// All scopes in this module. + /// Type augmentation data: maps binding ranges to their type information and JSDoc. /// - /// The first entry is expected to be the global scope. - pub(crate) scopes: Vec, + /// This enriches the semantic model's bindings with type inference results + /// and documentation comments. + pub binding_type_data: FxHashMap, - /// Lookup tree to find scopes by text range. - pub(crate) scope_by_range: Lapper, + /// Parsed expressions, mapped from their range to their type ID. + pub(crate) expressions: FxHashMap, /// Collection of all types in the module. /// @@ -262,10 +303,12 @@ impl JsImportPath { static_assertions::assert_impl_all!(JsModuleInfo: Send, Sync); impl JsModuleInfoInner { - /// Returns one of the bindings by ID. + /// Returns type augmentation data for a binding by its range. + /// + /// This is the replacement for the old `binding()` method. #[inline] - pub fn binding(&self, binding_id: BindingId) -> &JsBindingData { - &self.bindings[binding_id.index()] + pub fn binding_type_data(&self, binding_range: TextRange) -> Option<&BindingTypeData> { + self.binding_type_data.get(&binding_range) } /// Attempts to find a binding by `name` in the scope with the given @@ -273,15 +316,22 @@ impl JsModuleInfoInner { /// /// Traverses upwards in scope if the binding is not found in the given /// scope. - fn find_binding_in_scope(&self, name: &str, scope_id: ScopeId) -> Option { - let mut scope = &self.scopes[scope_id.index()]; + /// + /// Returns the binding's text range for looking up type augmentation data. + fn find_binding_in_scope(&self, name: &str, scope_id: ScopeId) -> Option { + // Start from the specified scope and walk up the scope chain + let mut scope = self.semantic_model.scope_from_id(scope_id); + loop { - if let Some(binding_ref) = scope.bindings_by_name.get(name) { - return Some(*binding_ref); + // Check if this scope has a binding with the given name + if let Some(binding) = scope.get_binding(name) { + // Return the binding's range for type data lookup + return Some(binding.syntax().text_trimmed_range()); } - match &scope.parent { - Some(parent_id) => scope = &self.scopes[parent_id.index()], + // Move to parent scope + match scope.parent() { + Some(parent) => scope = parent, None => break, } } @@ -289,6 +339,29 @@ impl JsModuleInfoInner { None } + /// Checks if a binding with the given name in the specified scope is imported. + /// + /// Returns `true` if the binding is an import declaration, `false` otherwise. + fn is_binding_imported(&self, name: &str, scope_id: ScopeId) -> bool { + // Start from the specified scope and walk up the scope chain + let mut scope = self.semantic_model.scope_from_id(scope_id); + + loop { + // Check if this scope has a binding with the given name + if let Some(binding) = scope.get_binding(name) { + return binding.is_imported(); + } + + // Move to parent scope + match scope.parent() { + Some(parent) => scope = parent, + None => break, + } + } + + false + } + /// Returns the information about a given import by its syntax node. pub fn get_import_path_by_js_node(&self, node: &AnyJsImportLike) -> Option<&JsImportPath> { let specifier_text = node.inner_string_text()?; @@ -374,7 +447,10 @@ pub struct JsImport { /// which no binding exists, or namespaces defined by exports of another module. #[derive(Clone, Debug, PartialEq)] pub enum JsOwnExport { - Binding(BindingId), + /// An export that references a binding by its text range. + /// The range can be used to look up type augmentation data. + Binding(TextRange), + /// An export that directly references a resolved type. Type(ResolvedTypeId), } diff --git a/crates/biome_module_graph/src/js_module_info/binding.rs b/crates/biome_module_graph/src/js_module_info/binding.rs index 0d3e7696b71b..037dd00157ba 100644 --- a/crates/biome_module_graph/src/js_module_info/binding.rs +++ b/crates/biome_module_graph/src/js_module_info/binding.rs @@ -1,9 +1,10 @@ use std::sync::Arc; +use biome_js_semantic::ScopeId; use biome_js_syntax::{ AnyJsDeclaration, JsImport, JsSyntaxNode, JsVariableKind, TextRange, TsTypeParameter, }; -use biome_js_type_info::{BindingId, ScopeId, TypeReference}; +use biome_js_type_info::TypeReference; use biome_rowan::{AstNode, Text, TextSize}; use biome_jsdoc_comment::JsdocComment; @@ -37,11 +38,6 @@ pub struct JsBindingReference { } impl JsBindingReference { - #[inline(always)] - pub fn is_read(&self) -> bool { - matches!(self.kind, JsBindingReferenceKind::Read { .. }) - } - #[inline(always)] pub fn is_write(&self) -> bool { matches!(self.kind, JsBindingReferenceKind::Write { .. }) @@ -50,47 +46,77 @@ impl JsBindingReference { /// Provides access to all semantic data of a specific binding. pub struct JsBinding { - pub(crate) data: Arc, - pub(crate) id: BindingId, + data: Arc, + semantic_binding: biome_js_semantic::Binding, } impl std::fmt::Debug for JsBinding { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Binding").field("id", &self.id).finish() + let name = self + .semantic_binding + .tree() + .name_token() + .ok() + .map(|t| t.text_trimmed().to_string()); + f.debug_struct("JsBinding").field("name", &name).finish() } } impl JsBinding { + pub(crate) fn from_semantic_binding( + data: Arc, + semantic_binding: biome_js_semantic::Binding, + ) -> Self { + Self { + data, + semantic_binding, + } + } + /// Returns whether the binding is exported. pub fn is_exported(&self) -> bool { - let binding = self.data.binding(self.id); - !binding.export_ranges.is_empty() + // Check if there are export ranges in the type augmentation data + let binding_range = self.semantic_binding.syntax().text_trimmed_range(); + self.data + .binding_type_data + .get(&binding_range) + .is_some_and(|data| !data.export_ranges.is_empty()) } /// Returns whether the binding is imported. pub fn is_imported(&self) -> bool { - let binding = self.data.binding(self.id); - binding.declaration_kind.is_import_declaration() + self.semantic_binding.is_imported() } /// Returns the binding's name. pub fn name(&self) -> Text { - let binding = self.data.binding(self.id); - binding.name.clone() + self.semantic_binding + .tree() + .name_token() + .ok() + .map(|t| t.token_text_trimmed().into()) + .unwrap_or_default() } /// Returns the scope of this binding. pub fn scope(&self) -> JsScope { - let binding = self.data.binding(self.id); JsScope { info: self.data.clone(), - id: binding.scope_id, + scope: self.semantic_binding.scope(), } } - /// Returns a reference to the binding's type. - pub fn ty(&self) -> &TypeReference { - &self.data.binding(self.id).ty + /// Returns the binding's type. + /// + /// Returns an owned TypeReference since we may need to return + /// a default unknown type when no augmentation data exists. + pub fn ty(&self) -> TypeReference { + // Look up type augmentation data by binding range + let binding_range = self.semantic_binding.syntax().text_trimmed_range(); + self.data + .binding_type_data + .get(&binding_range) + .map_or_else(TypeReference::unknown, |data| data.ty.clone()) } } diff --git a/crates/biome_module_graph/src/js_module_info/collector.rs b/crates/biome_module_graph/src/js_module_info/collector.rs index f099ad114707..8836fb248703 100644 --- a/crates/biome_module_graph/src/js_module_info/collector.rs +++ b/crates/biome_module_graph/src/js_module_info/collector.rs @@ -1,6 +1,6 @@ use std::{borrow::Cow, collections::BTreeSet, sync::Arc}; -use biome_js_semantic::{SemanticEvent, SemanticEventExtractor}; +use biome_js_semantic::{BindingId, ScopeId, SemanticEvent, SemanticEventExtractor}; use biome_js_syntax::{ AnyJsCombinedSpecifier, AnyJsDeclaration, AnyJsExportDefaultDeclaration, AnyJsExpression, AnyJsImportClause, JsAssignmentExpression, JsForVariableDeclaration, JsFormalParameter, @@ -9,15 +9,15 @@ use biome_js_syntax::{ inner_string_text, }; use biome_js_type_info::{ - BindingId, FunctionParameter, GLOBAL_RESOLVER, GLOBAL_UNKNOWN_ID, GenericTypeParameter, - MAX_FLATTEN_DEPTH, Module, Namespace, Resolvable, ResolvedTypeData, ResolvedTypeId, ScopeId, - TypeData, TypeId, TypeImportQualifier, TypeMember, TypeMemberKind, TypeReference, - TypeReferenceQualifier, TypeResolver, TypeResolverLevel, TypeStore, + FunctionParameter, GLOBAL_RESOLVER, GLOBAL_UNKNOWN_ID, GenericTypeParameter, MAX_FLATTEN_DEPTH, + Module, Namespace, Resolvable, ResolvedTypeData, ResolvedTypeId, TypeData, TypeId, + TypeImportQualifier, TypeMember, TypeMemberKind, TypeReference, TypeReferenceQualifier, + TypeResolver, TypeResolverLevel, TypeStore, }; use biome_jsdoc_comment::JsdocComment; use biome_rowan::{AstNode, Text, TextRange, TextSize, TokenText}; use indexmap::IndexMap; -use rust_lapper::{Interval, Lapper}; +use rust_lapper::Interval; use rustc_hash::FxHashMap; use super::{ @@ -396,7 +396,7 @@ impl JsModuleInfoCollector { self.scopes.push(JsScopeData { range, - parent: parent_scope_id.map(|id| ScopeId::new(id.index())), + parent: parent_scope_id, children: Vec::new(), bindings: Vec::new(), bindings_by_name: FxHashMap::default(), @@ -550,17 +550,15 @@ impl JsModuleInfoCollector { } } - fn finalise(&mut self) -> (IndexMap, Lapper) { - let scope_by_range = Lapper::new( - self.scope_range_by_start - .iter() - .flat_map(|(_, scopes)| scopes.iter()) - .cloned() - .collect(), - ); - + fn finalise( + &mut self, + semantic_model: &biome_js_semantic::SemanticModel, + ) -> ( + IndexMap, + FxHashMap, + ) { if self.infer_types { - self.infer_all_types(&scope_by_range); + self.infer_all_types(semantic_model); self.resolve_all_and_downgrade_project_references(); // Purging before flattening will save us from duplicate work during @@ -571,15 +569,29 @@ impl JsModuleInfoCollector { } let exports = self.collect_exports(); + let binding_type_data = self.build_binding_type_data(semantic_model); - (exports, scope_by_range) + (exports, binding_type_data) } - fn infer_all_types(&mut self, scope_by_range: &Lapper) { + fn infer_all_types(&mut self, _semantic_model: &biome_js_semantic::SemanticModel) { + // NOTE: We still use the collector's temporary scopes here for type inference. + // The semantic_model is passed for future use, but currently we rely on the + // collector's scope_by_range that was built from semantic events. + // + // TODO: Refactor type inference to use semantic_model's scopes directly. + let scope_by_range = rust_lapper::Lapper::new( + self.scope_range_by_start + .iter() + .flat_map(|(_, scopes)| scopes.iter()) + .cloned() + .collect(), + ); + for index in 0..self.bindings.len() { let binding = &self.bindings[index]; if let Some(node) = self.binding_node_by_start.get(&binding.range.start()) { - let scope_id = scope_id_for_range(scope_by_range, binding.range); + let scope_id = scope_id_for_range(&scope_by_range, binding.range); let ty = self.infer_type(&node.clone(), binding.clone(), scope_id); self.bindings[index].ty = ty; } @@ -609,6 +621,18 @@ impl JsModuleInfoCollector { scope_id: ScopeId, ) -> TypeReference { let binding_name = &binding.name.clone(); + + // If this binding is an import, create a TypeReference::Import directly + if binding.declaration_kind.is_import_declaration() + && let Some(import) = self.static_imports.get(binding_name) + { + return TypeReference::from(TypeImportQualifier { + symbol: import.symbol.clone(), + resolved_path: import.resolved_path.clone(), + type_only: binding.declaration_kind.is_import_type_declaration(), + }); + } + for ancestor in node.ancestors() { if let Some(decl) = AnyJsDeclaration::cast_ref(&ancestor) { let ty = if let Some(typed_bindings) = decl @@ -972,7 +996,9 @@ impl JsModuleInfoCollector { | TsBindingReference::ValueType(binding_id) | TsBindingReference::TypeAndValueType(binding_id) | TsBindingReference::NamespaceAndValueType(binding_id) => { - JsOwnExport::Binding(*binding_id) + // Get the binding range instead of storing the BindingId + let binding_range = self.bindings[binding_id.index()].range; + JsOwnExport::Binding(binding_range) } }; @@ -1118,10 +1144,39 @@ impl TypeResolver for JsModuleInfoCollector { } } +impl JsModuleInfoCollector { + /// Build type augmentation data from the temporary bindings collected during traversal. + /// + /// Maps binding ranges to their type information and JSDoc comments. + fn build_binding_type_data( + &self, + _semantic_model: &biome_js_semantic::SemanticModel, + ) -> FxHashMap { + let mut binding_type_data = FxHashMap::default(); + + for binding in &self.bindings { + binding_type_data.insert( + binding.range, + super::BindingTypeData { + ty: binding.ty.clone(), + jsdoc: binding.jsdoc.clone(), + export_ranges: binding.export_ranges.clone(), + }, + ); + } + + binding_type_data + } +} + impl JsModuleInfo { - pub(super) fn new(mut collector: JsModuleInfoCollector, infer_types: bool) -> Self { + pub(super) fn new( + mut collector: JsModuleInfoCollector, + semantic_model: std::sync::Arc, + infer_types: bool, + ) -> Self { collector.infer_types = infer_types; - let (exports, scope_by_range) = collector.finalise(); + let (exports, binding_type_data) = collector.finalise(&semantic_model); Self(Arc::new(JsModuleInfoInner { static_imports: Imports(collector.static_imports), @@ -1129,10 +1184,9 @@ impl JsModuleInfo { dynamic_import_paths: collector.dynamic_import_paths, exports: Exports(exports), blanket_reexports: collector.blanket_reexports, - bindings: collector.bindings, + semantic_model, + binding_type_data, expressions: collector.parsed_expressions, - scopes: collector.scopes, - scope_by_range, types: collector.types.into(), diagnostics: collector.diagnostics.into_iter().map(Into::into).collect(), infer_types: collector.infer_types, diff --git a/crates/biome_module_graph/src/js_module_info/module_resolver.rs b/crates/biome_module_graph/src/js_module_info/module_resolver.rs index f0de32b3887b..6906bf47df03 100644 --- a/crates/biome_module_graph/src/js_module_info/module_resolver.rs +++ b/crates/biome_module_graph/src/js_module_info/module_resolver.rs @@ -13,7 +13,7 @@ use rustc_hash::{FxHashMap, FxHashSet}; use crate::{ JsExport, JsImportPath, JsOwnExport, ModuleGraph, - js_module_info::{JsModuleInfoInner, scope::TsBindingReference, utils::reached_too_many_types}, + js_module_info::{JsModuleInfoInner, utils::reached_too_many_types}, }; use super::{JsModuleInfo, JsModuleInfoDiagnostic}; @@ -136,8 +136,25 @@ impl ModuleResolver { .get("default") .and_then(JsExport::as_own_export) .map(|own_export| match own_export { - JsOwnExport::Binding(binding_id) => { - self.resolved_type_for_reference(&module.bindings[binding_id.index()].ty) + JsOwnExport::Binding(binding_range) => { + // Check if this binding is an import and resolve it + if let Some(import_qualifier) = + resolve_binding_as_import(module, *binding_range) + { + return self.resolve_import(&import_qualifier).map_or_else( + || self.resolved_type_for_id(GLOBAL_UNKNOWN_ID), + |resolved_id| self.resolved_type_for_id(resolved_id), + ); + } + + // Not an import, look up the binding's type from augmentation data + module + .binding_type_data(*binding_range) + .and_then(|data| self.resolve_reference(&data.ty)) + .map_or_else( + || self.resolved_type_for_id(GLOBAL_UNKNOWN_ID), + |resolved_id| self.resolved_type_for_id(resolved_id), + ) } JsOwnExport::Type(resolved_id) => self.resolved_type_for_id(*resolved_id), }) @@ -152,19 +169,26 @@ impl ModuleResolver { /// defined in the scope of the given `range`. pub fn resolved_type_of_named_value(self: &Arc, range: TextRange, name: &str) -> Type { let module = &self.modules[0]; + + // Get the scope at the specified range for correct lexical scoping let scope = module.scope_for_range(range); - let Some(resolved_id) = module - .find_binding_in_scope(name, scope.id) - .and_then(TsBindingReference::value_ty) - .and_then(|binding_id| match &module.binding(binding_id).ty { - TypeReference::Resolved(resolved_id) => Some(*resolved_id), - _ => None, - }) + let scope_id = scope.scope.id(); + + let Some(resolved_id) = + module + .find_binding_in_scope(name, scope_id) + .and_then(|binding_range| { + // Look up the binding's type data by its range + module + .binding_type_data(binding_range) + // Resolve the type reference, handling Resolved, Import, and Qualifier cases + .and_then(|data| self.resolve_reference(&data.ty)) + }) else { return self.resolved_type_for_id(GLOBAL_UNKNOWN_ID); }; - self.resolved_type_for_id(self.mapped_resolved_id(resolved_id)) + self.resolved_type_for_id(resolved_id) } fn find_module(&self, path: &ResolvedPath) -> Option { @@ -310,7 +334,7 @@ impl ModuleResolver { ResolveFromExportResult::Resolved(resolved) => return resolved, ResolveFromExportResult::FollowImport(import) => { module_id = self.find_module(&import.resolved_path)?; - symbol = Cow::Owned(import.symbol.clone()); + symbol = Cow::Owned(import.symbol); type_only = if import.type_only { true } else { type_only }; } } @@ -398,21 +422,41 @@ impl TypeResolver for ModuleResolver { fn resolve_type_of(&self, identifier: &Text, scope_id: ScopeId) -> Option { let module = &self.modules[0]; - let Some(binding_ref) = module.find_binding_in_scope(identifier, scope_id) else { + + // Convert scope_id to semantic ScopeId + let semantic_scope_id = biome_js_semantic::ScopeId::new(scope_id.index()); + + let Some(binding_range) = module.find_binding_in_scope(identifier, semantic_scope_id) + else { return GLOBAL_RESOLVER.resolve_type_of(identifier, scope_id); }; - let binding = module.binding(binding_ref.value_ty_or_ty()); - if binding.declaration_kind.is_import_declaration() { - module.static_imports.get(&binding.name).and_then(|import| { - self.resolve_import(&TypeImportQualifier { - symbol: import.symbol.clone(), - resolved_path: import.resolved_path.clone(), - type_only: binding.declaration_kind.is_import_type_declaration(), + // Check if the binding is actually an import declaration + // This prevents incorrectly treating local bindings that shadow imported names as imports + if module.is_binding_imported(identifier, semantic_scope_id) { + module + .static_imports + .get(identifier.text()) + .and_then(|import| { + // TODO: Determine if this is a type-only import + // JsImport doesn't store phase information directly + // We may need to look it up from static_import_paths + let type_only = module + .static_import_paths + .get(import.specifier.text()) + .is_some_and(|path| path.phase == crate::JsImportPhase::Type); + + self.resolve_import(&TypeImportQualifier { + symbol: import.symbol.clone(), + resolved_path: import.resolved_path.clone(), + type_only, + }) }) - }) } else { - self.resolve_reference(&binding.ty) + // Look up the binding's type from augmentation data + module + .binding_type_data(binding_range) + .and_then(|data| self.resolve_reference(&data.ty)) } } @@ -459,9 +503,50 @@ impl TypeResolver for ModuleResolver { } } -enum ResolveFromExportResult<'a> { +/// Attempts to resolve a binding to an import qualifier if the binding is imported. +/// +/// Returns `Some(TypeImportQualifier)` if the binding at `binding_range` is an imported +/// binding, `None` otherwise. +fn resolve_binding_as_import( + module: &JsModuleInfoInner, + binding_range: TextRange, +) -> Option { + // Find the binding in the semantic model to get its name + let binding = module + .semantic_model + .scope_from_id(ScopeId::GLOBAL) + .bindings() + .find(|b| b.syntax().text_trimmed_range() == binding_range)?; + + let name = binding.syntax().text_trimmed().into_text(); + + // Check if this binding is an import + if !module.is_binding_imported(name.text(), ScopeId::GLOBAL) { + return None; + } + + // Resolve the import directly from static_imports. + // Note: We don't need to check dynamic_import_paths because: + // - Dynamic imports (import('./foo')) return Promises and don't create direct bindings + // - CommonJS require() calls are expressions that return values, not import bindings + // - Only static imports (import { foo } from '...') create bindings that + // is_binding_imported() recognizes as imported + let import = module.static_imports.get(name.text())?; + let type_only = module + .static_import_paths + .get(import.specifier.text()) + .is_some_and(|path| path.phase == crate::JsImportPhase::Type); + + Some(TypeImportQualifier { + symbol: import.symbol.clone(), + resolved_path: import.resolved_path.clone(), + type_only, + }) +} + +enum ResolveFromExportResult { Resolved(Option), - FollowImport(&'a TypeImportQualifier), + FollowImport(TypeImportQualifier), } /// Resolves an export from the given `module` with the given `module_id`. @@ -470,23 +555,41 @@ enum ResolveFromExportResult<'a> { /// [`ResolvedTypeId`] is returned. If the export references another module, it /// returns the [`TypeImportQualifier`] to follow. #[inline] -fn resolve_from_export<'a>( +fn resolve_from_export( module_id: ModuleId, - module: &'a JsModuleInfoInner, + module: &JsModuleInfoInner, export: &JsOwnExport, -) -> ResolveFromExportResult<'a> { +) -> ResolveFromExportResult { let resolved = match export { - JsOwnExport::Binding(binding_id) => match &module.bindings[binding_id.index()].ty { - TypeReference::Qualifier(_qualifier) => { - // If it wasn't resolved before exporting, we can't help it - // anymore. - None - } - TypeReference::Resolved(resolved_id) => Some(resolved_id.with_module_id(module_id)), - TypeReference::Import(import) => { - return ResolveFromExportResult::FollowImport(import); + JsOwnExport::Binding(binding_range) => { + // Look up the binding's type data by its range + match module.binding_type_data(*binding_range) { + Some(data) => match &data.ty { + TypeReference::Qualifier(_qualifier) => { + // If it wasn't resolved before exporting, we can't help it + // anymore. + None + } + TypeReference::Resolved(resolved_id) => { + Some(resolved_id.with_module_id(module_id)) + } + TypeReference::Import(import) => { + return ResolveFromExportResult::FollowImport(*import.clone()); + } + }, + None => { + // If there's no binding_type_data, check if this binding is an import. + // Imported bindings don't have local type data - their types come from + // the imported module. + if let Some(import_qualifier) = + resolve_binding_as_import(module, *binding_range) + { + return ResolveFromExportResult::FollowImport(import_qualifier); + } + None + } } - }, + } JsOwnExport::Type(resolved_id) => Some(resolved_id.with_module_id(module_id)), }; diff --git a/crates/biome_module_graph/src/js_module_info/scope.rs b/crates/biome_module_graph/src/js_module_info/scope.rs index 3578d2db197b..f4a12e023b4e 100644 --- a/crates/biome_module_graph/src/js_module_info/scope.rs +++ b/crates/biome_module_graph/src/js_module_info/scope.rs @@ -1,7 +1,8 @@ -use std::{collections::VecDeque, iter::FusedIterator, sync::Arc}; +use std::{iter::FusedIterator, sync::Arc}; +use biome_js_semantic::{BindingId, ScopeId}; use biome_js_syntax::TextRange; -use biome_js_type_info::{BindingId, ScopeId, TypeReferenceQualifier}; +use biome_js_type_info::TypeReferenceQualifier; use biome_rowan::TokenText; use rustc_hash::FxHashMap; @@ -13,6 +14,7 @@ use super::{ #[derive(Debug)] pub struct JsScopeData { // The scope range + #[expect(dead_code, reason = "May be used in future for scope analysis")] pub range: TextRange, // The parent scope of this scope pub parent: Option, @@ -301,17 +303,6 @@ impl TsBindingReference { } } - /// Returns the value type binding. - pub fn value_ty(self) -> Option { - match self { - Self::ValueType(binding_id) - | Self::TypeAndValueType(binding_id) - | Self::NamespaceAndValueType(binding_id) => Some(binding_id), - Self::Merged { value_ty, .. } => value_ty, - Self::Type(_) => None, - } - } - /// Returns the value type binding, or the type binding if the value type /// binding is unknown. pub fn value_ty_or_ty(self) -> BindingId { @@ -337,12 +328,12 @@ impl TsBindingReference { #[derive(Clone, Debug)] pub struct JsScope { pub(crate) info: Arc, - pub(crate) id: ScopeId, + pub(crate) scope: biome_js_semantic::Scope, } impl PartialEq for JsScope { fn eq(&self, other: &Self) -> bool { - self.id == other.id && Arc::ptr_eq(&self.info, &other.info) + self.scope == other.scope && Arc::ptr_eq(&self.info, &other.info) } } @@ -350,35 +341,34 @@ impl Eq for JsScope {} impl JsScope { pub fn is_global_scope(&self) -> bool { - self.id.index() == 0 + self.scope.is_global_scope() } /// Returns all parents of this scope. Starting with the current /// [JsScope]. pub fn ancestors(&self) -> impl Iterator + use<> { - std::iter::successors(Some(self.clone()), |scope| scope.parent()) + let info = self.info.clone(); + self.scope.ancestors().map(move |scope| Self { + info: info.clone(), + scope, + }) } /// Returns all descendents of this scope in breadth-first order. Starting /// with the current [JsScope]. pub fn descendents(&self) -> impl Iterator + use<> { - let mut q = VecDeque::new(); - q.push_back(self.id); - - ScopeDescendentsIter { - info: self.info.clone(), - q, - } + let info = self.info.clone(); + self.scope.descendents().map(move |scope| Self { + info: info.clone(), + scope, + }) } /// Returns this scope parent. pub fn parent(&self) -> Option { - debug_assert!((self.id.index()) < self.info.scopes.len()); - - let parent = self.info.scopes[self.id.index()].parent?; - Some(Self { + self.scope.parent().map(|scope| Self { info: self.info.clone(), - id: parent, + scope, }) } @@ -388,8 +378,7 @@ impl JsScope { pub fn bindings(&self) -> ScopeBindingsIter { ScopeBindingsIter { info: self.info.clone(), - scope_id: self.id, - binding_index: 0, + semantic_bindings: self.scope.bindings(), } } @@ -402,74 +391,37 @@ impl JsScope { /// assert!(scope.is_ancestor_of(scope)); /// ``` pub fn is_ancestor_of(&self, other: &Self) -> bool { - other.ancestors().any(|s| s == *self) + self.scope.is_ancestor_of(&other.scope) } pub fn range(&self) -> TextRange { - self.info.scopes[self.id.index()].range - } -} - -/// Iterates all descendent scopes of the specified scope in breadth-first -/// order. -pub struct ScopeDescendentsIter { - info: Arc, - q: VecDeque, -} - -impl Iterator for ScopeDescendentsIter { - type Item = JsScope; - - fn next(&mut self) -> Option { - if let Some(id) = self.q.pop_front() { - let scope = &self.info.scopes[id.index()]; - self.q.extend(scope.children.iter()); - Some(JsScope { - info: self.info.clone(), - id, - }) - } else { - None - } + self.scope.range() } } -impl FusedIterator for ScopeDescendentsIter {} - /// Iterates all bindings that were bound in a given scope. /// /// It **does not** return bindings of parent scopes. -#[derive(Debug)] pub struct ScopeBindingsIter { info: Arc, - scope_id: ScopeId, - binding_index: u32, + semantic_bindings: biome_js_semantic::ScopeBindingsIter, } impl Iterator for ScopeBindingsIter { type Item = JsBinding; fn next(&mut self) -> Option { - debug_assert!(self.scope_id.index() < self.info.scopes.len()); - - let id = *self.info.scopes[self.scope_id.index()] - .bindings - .get(self.binding_index as usize)?; - - self.binding_index += 1; - - Some(JsBinding { - data: self.info.clone(), - id, - }) + let semantic_binding = self.semantic_bindings.next()?; + Some(JsBinding::from_semantic_binding( + self.info.clone(), + semantic_binding, + )) } } impl ExactSizeIterator for ScopeBindingsIter { fn len(&self) -> usize { - debug_assert!(self.scope_id.index() < self.info.scopes.len()); - - self.info.scopes[self.scope_id.index()].bindings.len() + self.semantic_bindings.len() } } diff --git a/crates/biome_module_graph/src/js_module_info/visitor.rs b/crates/biome_module_graph/src/js_module_info/visitor.rs index 1ebea296422c..e5f8a39fdca9 100644 --- a/crates/biome_module_graph/src/js_module_info/visitor.rs +++ b/crates/biome_module_graph/src/js_module_info/visitor.rs @@ -1,3 +1,4 @@ +use biome_js_semantic::ScopeId; use biome_js_syntax::{ AnyJsArrayBindingPatternElement, AnyJsBinding, AnyJsBindingPattern, AnyJsDeclarationClause, AnyJsExportClause, AnyJsExportDefaultDeclaration, AnyJsExpression, AnyJsImportClause, @@ -5,7 +6,7 @@ use biome_js_syntax::{ AnyTsModuleName, JsExportFromClause, JsExportNamedFromClause, JsExportNamedSpecifierList, JsIdentifierBinding, JsVariableDeclaratorList, TsExportAssignmentClause, unescape_js_string, }; -use biome_js_type_info::{ImportSymbol, ScopeId, TypeData, TypeReference, TypeResolver}; +use biome_js_type_info::{ImportSymbol, TypeData, TypeReference, TypeResolver}; use biome_jsdoc_comment::JsdocComment; use biome_resolver::{ResolveOptions, resolve}; use biome_rowan::{AstNode, TokenText, WalkEvent}; @@ -30,6 +31,7 @@ pub(crate) struct JsModuleVisitor<'a> { root: AnyJsRoot, directory: &'a Utf8Path, fs_proxy: &'a ModuleGraphFsProxy<'a>, + semantic_model: std::sync::Arc, infer_types: bool, } @@ -38,12 +40,14 @@ impl<'a> JsModuleVisitor<'a> { root: AnyJsRoot, directory: &'a Utf8Path, fs_proxy: &'a ModuleGraphFsProxy, + semantic_model: std::sync::Arc, infer_types: bool, ) -> Self { Self { root, directory, fs_proxy, + semantic_model, infer_types, } } @@ -69,7 +73,7 @@ impl<'a> JsModuleVisitor<'a> { } } - JsModuleInfo::new(collector, self.infer_types) + JsModuleInfo::new(collector, self.semantic_model, self.infer_types) } fn visit_import(&self, node: AnyJsImportLike, collector: &mut JsModuleInfoCollector) { diff --git a/crates/biome_module_graph/src/lib.rs b/crates/biome_module_graph/src/lib.rs index 3b6b9fdf2960..a48b1e5343c9 100644 --- a/crates/biome_module_graph/src/lib.rs +++ b/crates/biome_module_graph/src/lib.rs @@ -12,8 +12,8 @@ pub use biome_resolver::ResolvedPath; pub use css_module_info::{CssImport, CssImports, CssModuleInfo}; pub use diagnostics::ModuleDiagnostic; pub use js_module_info::{ - JsExport, JsImport, JsImportPath, JsImportPhase, JsModuleInfo, JsModuleInfoDiagnostic, - JsOwnExport, JsReexport, ModuleResolver, SerializedJsModuleInfo, + BindingTypeData, JsExport, JsImport, JsImportPath, JsImportPhase, JsModuleInfo, + JsModuleInfoDiagnostic, JsOwnExport, JsReexport, ModuleResolver, SerializedJsModuleInfo, }; pub use module_graph::{ ModuleDependencies, ModuleGraph, ModuleInfo, SUPPORTED_EXTENSIONS, SerializedModuleInfo, diff --git a/crates/biome_module_graph/src/module_graph.rs b/crates/biome_module_graph/src/module_graph.rs index b994e6976e41..770432cc9dd9 100644 --- a/crates/biome_module_graph/src/module_graph.rs +++ b/crates/biome_module_graph/src/module_graph.rs @@ -81,12 +81,16 @@ impl ModuleGraph { &self, fs: &dyn FsWithResolverProxy, project_layout: &ProjectLayout, - added_or_updated_paths: &[(&BiomePath, AnyJsRoot)], + added_or_updated_paths: &[( + &BiomePath, + AnyJsRoot, + std::sync::Arc, + )], enable_type_inference: bool, ) -> (ModuleDependencies, Vec) { // Make sure all directories are registered for the added/updated paths. let path_info = self.path_info.pin(); - for (path, _) in added_or_updated_paths { + for (path, _, _) in added_or_updated_paths { let mut parent = path.parent(); while let Some(path) = parent { let mut inserted = false; @@ -108,10 +112,15 @@ impl ModuleGraph { // Traverse all the added and updated paths and insert their module // info. let modules = self.data.pin(); - for (path, root) in added_or_updated_paths { + for (path, root, semantic_model) in added_or_updated_paths { let directory = path.parent().unwrap_or(path); - let visitor = - JsModuleVisitor::new(root.clone(), directory, &fs_proxy, enable_type_inference); + let visitor = JsModuleVisitor::new( + root.clone(), + directory, + &fs_proxy, + semantic_model.clone(), + enable_type_inference, + ); let module_info = visitor.collect_info(); for import_path in module_info.all_import_paths() { @@ -243,9 +252,9 @@ impl ModuleGraph { find_exported_symbol_with_seen_paths(&data, module, symbol_name, &mut seen_paths).and_then( |(module, export)| match export { - JsOwnExport::Binding(binding_id) => { - module.bindings[binding_id.index()].jsdoc.clone() - } + JsOwnExport::Binding(binding_range) => module + .binding_type_data(*binding_range) + .and_then(|data| data.jsdoc.clone()), JsOwnExport::Type(_) => None, }, ) diff --git a/crates/biome_module_graph/tests/snap/mod.rs b/crates/biome_module_graph/tests/snap/mod.rs index 54a3d580d3fb..03b772e057d5 100644 --- a/crates/biome_module_graph/tests/snap/mod.rs +++ b/crates/biome_module_graph/tests/snap/mod.rs @@ -1,5 +1,3 @@ -use std::collections::BTreeSet; - use biome_fs::MemoryFileSystem; use biome_js_formatter::context::JsFormatOptions; use biome_js_formatter::format_node; @@ -10,6 +8,7 @@ use biome_resolver::ResolvedPath; use biome_rowan::AstNode; use biome_test_utils::{dump_registered_module_types, dump_registered_types}; use camino::Utf8PathBuf; +use std::collections::BTreeSet; pub struct ModuleGraphSnapshot<'a> { module_graph: &'a ModuleGraph, @@ -94,23 +93,33 @@ impl<'a> ModuleGraphSnapshot<'a> { content.push_str(&data.to_string()); content.push_str("\n```\n\n"); - let exported_binding_ids: BTreeSet<_> = data + let exported_binding_ranges: BTreeSet<_> = data .exports .values() .filter_map(JsExport::as_own_export) .filter_map(|export| match export { - JsOwnExport::Binding(binding_id) => Some(*binding_id), + JsOwnExport::Binding(binding_range) => Some(*binding_range), JsOwnExport::Type(_) => None, }) .collect(); - if !exported_binding_ids.is_empty() { + if !exported_binding_ranges.is_empty() { content.push_str("## Exported Bindings\n\n"); content.push_str("```"); - for binding_id in exported_binding_ids { - content.push_str(&format!( - "\n{binding_id:?} => {}\n", - data.binding(binding_id) - )); + for binding_range in exported_binding_ranges { + if let Some(type_data) = data.binding_type_data(binding_range) { + // Get the binding name from the semantic model + let binding_name = data + .semantic_model + .all_bindings() + .find(|b| b.syntax().text_trimmed_range() == binding_range) + .and_then(|b| b.tree().name_token().ok()) + .map_or_else(|| "".to_string(), |b| b.to_string()); + + content.push_str(&format!( + "\n{} => {}\n", + binding_name, type_data + )); + } } content.push_str("```\n\n"); } diff --git a/crates/biome_module_graph/tests/snapshots/test_export_default_function_declaration.snap b/crates/biome_module_graph/tests/snapshots/test_export_default_function_declaration.snap index ad618acfcaba..d35560467d0f 100644 --- a/crates/biome_module_graph/tests/snapshots/test_export_default_function_declaration.snap +++ b/crates/biome_module_graph/tests/snapshots/test_export_default_function_declaration.snap @@ -20,7 +20,7 @@ export default function Component(): JSX.Element {} ``` Exports { "default" => { - ExportOwnExport => JsOwnExport::Binding(0) + ExportOwnExport => JsOwnExport::Binding(130..139) } } Imports { @@ -31,14 +31,13 @@ Imports { ## Exported Bindings ``` -BindingId(0) => JsBindingData { - Name: Component, - Type: Module(0) TypeId(1), - JSDoc comment: JsDoc( +Component => BindingTypeData { + Types Module(0) TypeId(1), + JsDoc( @public @returns {JSX.Element} ), - Declaration kind: Unknown + Exported Ranges: (130..139) } ``` diff --git a/crates/biome_module_graph/tests/snapshots/test_export_default_imported_binding.snap b/crates/biome_module_graph/tests/snapshots/test_export_default_imported_binding.snap new file mode 100644 index 000000000000..efc2bc76ecd0 --- /dev/null +++ b/crates/biome_module_graph/tests/snapshots/test_export_default_imported_binding.snap @@ -0,0 +1,108 @@ +--- +source: crates/biome_module_graph/tests/snap/mod.rs +expression: content +--- + +# `/src/index.ts` (Not imported by resolver) + +## Source + +```ts +import { foo } from "./foo.ts"; + +export default foo; +``` + +## Module Info + +``` +Exports { + "default" => { + ExportOwnExport => JsOwnExport::Binding(22..25) + } +} +Imports { + "foo" => { + Specifier: "./foo.ts" + Resolved path: "/src/foo.ts" + Import Symbol: foo + } +} +``` + +## Exported Bindings + +``` +foo => BindingTypeData { + Types Import Symbol: foo from "/src/foo.ts", + Exported Ranges: (73..76)(73..76) +} +``` + +## Registered types + +``` +Module TypeId(0) => Import Symbol: foo from "/src/foo.ts" +``` + +# `/src/foo.ts` (Module 1) + +## Source + +```ts +/** + * @returns {number} + */ +export function foo(): number { + return 42; +} +``` + +## Module Info + +``` +Exports { + "foo" => { + ExportOwnExport => JsOwnExport::Binding(94..97) + } +} +Imports { + No imports +} +``` + +## Exported Bindings + +``` +foo => BindingTypeData { + Types Module(0) TypeId(1), + JsDoc( + @returns {number} + ), + Exported Ranges: (94..97) +} +``` + +## Registered types + +``` +Module TypeId(0) => number: 42 + +Module TypeId(1) => sync Function "foo" { + accepts: { + params: [] + type_args: [] + } + returns: number +} +``` + +# Module Resolver + +## Registered types + +``` +Full TypeId(0) => namespace for ModuleId(1) + +Full TypeId(1) => Module(1) TypeId(1) +``` diff --git a/crates/biome_module_graph/tests/snapshots/test_export_referenced_function.snap b/crates/biome_module_graph/tests/snapshots/test_export_referenced_function.snap index b2f603cca9db..f87aaafe7e25 100644 --- a/crates/biome_module_graph/tests/snapshots/test_export_referenced_function.snap +++ b/crates/biome_module_graph/tests/snapshots/test_export_referenced_function.snap @@ -21,7 +21,7 @@ export { foo }; ``` Exports { "foo" => { - ExportOwnExport => JsOwnExport::Binding(0) + ExportOwnExport => JsOwnExport::Binding(87..90) } } Imports { @@ -32,13 +32,12 @@ Imports { ## Exported Bindings ``` -BindingId(0) => JsBindingData { - Name: foo, - Type: Module(0) TypeId(1), - JSDoc comment: JsDoc( +foo => BindingTypeData { + Types Module(0) TypeId(1), + JsDoc( @returns {string} ), - Declaration kind: HoistedValue + Exported Ranges: (118..121) } ``` diff --git a/crates/biome_module_graph/tests/snapshots/test_resolve_export_type_referencing_imported_type.snap b/crates/biome_module_graph/tests/snapshots/test_resolve_export_type_referencing_imported_type.snap index ed0342ec5435..073257e894e3 100644 --- a/crates/biome_module_graph/tests/snapshots/test_resolve_export_type_referencing_imported_type.snap +++ b/crates/biome_module_graph/tests/snapshots/test_resolve_export_type_referencing_imported_type.snap @@ -22,7 +22,7 @@ export { returnPromiseResult }; ``` Exports { "returnPromiseResult" => { - ExportOwnExport => JsOwnExport::Binding(1) + ExportOwnExport => JsOwnExport::Binding(77..96) } } Imports { @@ -37,10 +37,9 @@ Imports { ## Exported Bindings ``` -BindingId(1) => JsBindingData { - Name: returnPromiseResult, - Type: Module(0) TypeId(7), - Declaration kind: HoistedValue +returnPromiseResult => BindingTypeData { + Types Module(0) TypeId(7), + Exported Ranges: (215..234) } ``` @@ -94,7 +93,7 @@ export type PromisedResult = Promise<{ result: true | false }>; ``` Exports { "PromisedResult" => { - ExportOwnExport => JsOwnExport::Binding(0) + ExportOwnExport => JsOwnExport::Binding(12..26) } } Imports { @@ -105,10 +104,9 @@ Imports { ## Exported Bindings ``` -BindingId(0) => JsBindingData { - Name: PromisedResult, - Type: Module(0) TypeId(4), - Declaration kind: Type +PromisedResult => BindingTypeData { + Types Module(0) TypeId(4), + Exported Ranges: (12..26) } ``` diff --git a/crates/biome_module_graph/tests/snapshots/test_resolve_export_types.snap b/crates/biome_module_graph/tests/snapshots/test_resolve_export_types.snap index ec5584bd7e22..a16d74ab8ece 100644 --- a/crates/biome_module_graph/tests/snapshots/test_resolve_export_types.snap +++ b/crates/biome_module_graph/tests/snapshots/test_resolve_export_types.snap @@ -44,10 +44,10 @@ export const superComputer = new DeepThought(); ``` Exports { "theAnswer" => { - ExportOwnExport => JsOwnExport::Binding(0) + ExportOwnExport => JsOwnExport::Binding(26..35) } "superComputer" => { - ExportOwnExport => JsOwnExport::Binding(3) + ExportOwnExport => JsOwnExport::Binding(1077..1090) } } Imports { @@ -58,16 +58,14 @@ Imports { ## Exported Bindings ``` -BindingId(0) => JsBindingData { - Name: theAnswer, - Type: Module(0) TypeId(0), - Declaration kind: Value +theAnswer => BindingTypeData { + Types Module(0) TypeId(0), + Exported Ranges: (26..35) } -BindingId(3) => JsBindingData { - Name: superComputer, - Type: Module(0) TypeId(5), - Declaration kind: Value +superComputer => BindingTypeData { + Types Module(0) TypeId(5), + Exported Ranges: (1077..1090) } ``` diff --git a/crates/biome_module_graph/tests/snapshots/test_resolve_exports.snap b/crates/biome_module_graph/tests/snapshots/test_resolve_exports.snap index ecefa5438958..ccdffe919ff1 100644 --- a/crates/biome_module_graph/tests/snapshots/test_resolve_exports.snap +++ b/crates/biome_module_graph/tests/snapshots/test_resolve_exports.snap @@ -62,34 +62,34 @@ export * as renamed2 from "./renamed-reexports"; ``` Exports { "foo" => { - ExportOwnExport => JsOwnExport::Binding(0) + ExportOwnExport => JsOwnExport::Binding(87..90) } "qux" => { - ExportOwnExport => JsOwnExport::Binding(4) + ExportOwnExport => JsOwnExport::Binding(389..392) } "bar" => { - ExportOwnExport => JsOwnExport::Binding(1) + ExportOwnExport => JsOwnExport::Binding(187..190) } "quz" => { - ExportOwnExport => JsOwnExport::Binding(2) + ExportOwnExport => JsOwnExport::Binding(250..253) } "baz" => { - ExportOwnExport => JsOwnExport::Binding(3) + ExportOwnExport => JsOwnExport::Binding(363..366) } "a" => { - ExportOwnExport => JsOwnExport::Binding(5) + ExportOwnExport => JsOwnExport::Binding(426..427) } "b" => { - ExportOwnExport => JsOwnExport::Binding(6) + ExportOwnExport => JsOwnExport::Binding(429..430) } "d" => { - ExportOwnExport => JsOwnExport::Binding(7) + ExportOwnExport => JsOwnExport::Binding(436..437) } "e" => { - ExportOwnExport => JsOwnExport::Binding(8) + ExportOwnExport => JsOwnExport::Binding(439..440) } "default" => { - ExportOwnExport => JsOwnExport::Binding(11) + ExportOwnExport => JsOwnExport::Binding(909..918) } "oh\nno" => { ExportReexport => Reexport( @@ -117,77 +117,67 @@ Imports { ## Exported Bindings ``` -BindingId(0) => JsBindingData { - Name: foo, - Type: Module(0) TypeId(10), - JSDoc comment: JsDoc( +foo => BindingTypeData { + Types Module(0) TypeId(10), + JsDoc( @returns {string} ), - Declaration kind: HoistedValue + Exported Ranges: (118..121) } -BindingId(1) => JsBindingData { - Name: bar, - Type: Module(0) TypeId(11), - JSDoc comment: JsDoc( +bar => BindingTypeData { + Types Module(0) TypeId(11), + JsDoc( @package ), - Declaration kind: HoistedValue + Exported Ranges: (187..190) } -BindingId(2) => JsBindingData { - Name: quz, - Type: Module(0) TypeId(0), - JSDoc comment: JsDoc( +quz => BindingTypeData { + Types Module(0) TypeId(0), + JsDoc( @private ), - Declaration kind: Value + Exported Ranges: (250..253) } -BindingId(3) => JsBindingData { - Name: baz, - Type: Module(0) TypeId(13), - Declaration kind: HoistedValue +baz => BindingTypeData { + Types Module(0) TypeId(13), + Exported Ranges: (363..366) } -BindingId(4) => JsBindingData { - Name: qux, - Type: Module(0) TypeId(1), - Declaration kind: HoistedValue +qux => BindingTypeData { + Types Module(0) TypeId(1), + Exported Ranges: (123..126) } -BindingId(5) => JsBindingData { - Name: a, - Type: Module(0) TypeId(4), - Declaration kind: Value +a => BindingTypeData { + Types Module(0) TypeId(4), + Exported Ranges: (426..427) } -BindingId(6) => JsBindingData { - Name: b, - Type: Module(0) TypeId(5), - Declaration kind: Value +b => BindingTypeData { + Types Module(0) TypeId(5), + Exported Ranges: (429..430) } -BindingId(7) => JsBindingData { - Name: d, - Type: Module(0) TypeId(7), - Declaration kind: Value +d => BindingTypeData { + Types Module(0) TypeId(7), + Exported Ranges: (436..437) } -BindingId(8) => JsBindingData { - Name: e, - Type: Module(0) TypeId(8), - Declaration kind: Value +e => BindingTypeData { + Types Module(0) TypeId(8), + Exported Ranges: (439..440) } -BindingId(11) => JsBindingData { - Name: Component, - Type: Module(0) TypeId(17), - JSDoc comment: JsDoc( +Component => BindingTypeData { + Types Module(0) TypeId(17), + JsDoc( @public @returns {JSX.Element} ), - Declaration kind: Unknown + Exported Ranges: (909..918) } ``` @@ -303,7 +293,7 @@ export function ohNo() {} ``` Exports { "ohNo" => { - ExportOwnExport => JsOwnExport::Binding(0) + ExportOwnExport => JsOwnExport::Binding(29..33) } } Imports { @@ -314,10 +304,9 @@ Imports { ## Exported Bindings ``` -BindingId(0) => JsBindingData { - Name: ohNo, - Type: Module(0) TypeId(1), - Declaration kind: HoistedValue +ohNo => BindingTypeData { + Types Module(0) TypeId(1), + Exported Ranges: (29..33) } ``` diff --git a/crates/biome_module_graph/tests/snapshots/test_resolve_generic_return_value.snap b/crates/biome_module_graph/tests/snapshots/test_resolve_generic_return_value.snap index fdd1f1b05056..fe1bcf6b9736 100644 --- a/crates/biome_module_graph/tests/snapshots/test_resolve_generic_return_value.snap +++ b/crates/biome_module_graph/tests/snapshots/test_resolve_generic_return_value.snap @@ -22,13 +22,13 @@ export const promise = makePromiseCb(); ``` Exports { "makePromise" => { - ExportOwnExport => JsOwnExport::Binding(4) + ExportOwnExport => JsOwnExport::Binding(105..116) } "makePromiseCb" => { - ExportOwnExport => JsOwnExport::Binding(5) + ExportOwnExport => JsOwnExport::Binding(168..181) } "promise" => { - ExportOwnExport => JsOwnExport::Binding(6) + ExportOwnExport => JsOwnExport::Binding(224..231) } } Imports { @@ -39,22 +39,19 @@ Imports { ## Exported Bindings ``` -BindingId(4) => JsBindingData { - Name: makePromise, - Type: Module(0) TypeId(6), - Declaration kind: Value +makePromise => BindingTypeData { + Types Module(0) TypeId(6), + Exported Ranges: (105..116) } -BindingId(5) => JsBindingData { - Name: makePromiseCb, - Type: Module(0) TypeId(6), - Declaration kind: Value +makePromiseCb => BindingTypeData { + Types Module(0) TypeId(6), + Exported Ranges: (168..181) } -BindingId(6) => JsBindingData { - Name: promise, - Type: Module(0) TypeId(7), - Declaration kind: Value +promise => BindingTypeData { + Types Module(0) TypeId(7), + Exported Ranges: (224..231) } ``` diff --git a/crates/biome_module_graph/tests/snapshots/test_resolve_generic_return_value_with_multiple_modules.snap b/crates/biome_module_graph/tests/snapshots/test_resolve_generic_return_value_with_multiple_modules.snap index f84847ea12c1..cada1cc01bd4 100644 --- a/crates/biome_module_graph/tests/snapshots/test_resolve_generic_return_value_with_multiple_modules.snap +++ b/crates/biome_module_graph/tests/snapshots/test_resolve_generic_return_value_with_multiple_modules.snap @@ -16,7 +16,7 @@ export type Bar = { bar: "bar" }; ``` Exports { "Bar" => { - ExportOwnExport => JsOwnExport::Binding(0) + ExportOwnExport => JsOwnExport::Binding(21..24) } } Imports { @@ -27,10 +27,9 @@ Imports { ## Exported Bindings ``` -BindingId(0) => JsBindingData { - Name: Bar, - Type: Module(0) TypeId(1), - Declaration kind: Type +Bar => BindingTypeData { + Types Module(0) TypeId(1), + Exported Ranges: (21..24) } ``` @@ -116,7 +115,7 @@ export function foo(foo: T, bar: Bar): T; ``` Exports { "foo" => { - ExportOwnExport => JsOwnExport::Binding(1) + ExportOwnExport => JsOwnExport::Binding(71..74) } } Imports { @@ -131,10 +130,9 @@ Imports { ## Exported Bindings ``` -BindingId(1) => JsBindingData { - Name: foo, - Type: Module(0) TypeId(3), - Declaration kind: HoistedValue +foo => BindingTypeData { + Types Module(0) TypeId(3), + Exported Ranges: } ``` diff --git a/crates/biome_module_graph/tests/snapshots/test_resolve_import_as_namespace.snap b/crates/biome_module_graph/tests/snapshots/test_resolve_import_as_namespace.snap index b85caa6d0079..8a5165b8370d 100644 --- a/crates/biome_module_graph/tests/snapshots/test_resolve_import_as_namespace.snap +++ b/crates/biome_module_graph/tests/snapshots/test_resolve_import_as_namespace.snap @@ -53,7 +53,7 @@ export function foo(): number { ``` Exports { "foo" => { - ExportOwnExport => JsOwnExport::Binding(0) + ExportOwnExport => JsOwnExport::Binding(25..28) } } Imports { @@ -64,10 +64,9 @@ Imports { ## Exported Bindings ``` -BindingId(0) => JsBindingData { - Name: foo, - Type: Module(0) TypeId(1), - Declaration kind: HoistedValue +foo => BindingTypeData { + Types Module(0) TypeId(1), + Exported Ranges: (25..28) } ``` diff --git a/crates/biome_module_graph/tests/snapshots/test_resolve_merged_types.snap b/crates/biome_module_graph/tests/snapshots/test_resolve_merged_types.snap index 616409f52b83..9fef6be6809b 100644 --- a/crates/biome_module_graph/tests/snapshots/test_resolve_merged_types.snap +++ b/crates/biome_module_graph/tests/snapshots/test_resolve_merged_types.snap @@ -27,10 +27,10 @@ export { A, B }; ``` Exports { "Union" => { - ExportOwnExport => JsOwnExport::Binding(3) + ExportOwnExport => JsOwnExport::Binding(54..59) } "Union2" => { - ExportOwnExport => JsOwnExport::Binding(7) + ExportOwnExport => JsOwnExport::Binding(131..137) } "A" => { ExportOwnExport => JsOwnExport::Type(Module(0) TypeId(2)) @@ -47,16 +47,14 @@ Imports { ## Exported Bindings ``` -BindingId(3) => JsBindingData { - Name: Union, - Type: Module(0) TypeId(5), - Declaration kind: Type +Union => BindingTypeData { + Types Module(0) TypeId(5), + Exported Ranges: (54..59) } -BindingId(7) => JsBindingData { - Name: Union2, - Type: Module(0) TypeId(9), - Declaration kind: Type +Union2 => BindingTypeData { + Types Module(0) TypeId(9), + Exported Ranges: (131..137) } ``` diff --git a/crates/biome_module_graph/tests/snapshots/test_resolve_multiple_reexports.snap b/crates/biome_module_graph/tests/snapshots/test_resolve_multiple_reexports.snap index f38c9decbb6c..8f4735d1473f 100644 --- a/crates/biome_module_graph/tests/snapshots/test_resolve_multiple_reexports.snap +++ b/crates/biome_module_graph/tests/snapshots/test_resolve_multiple_reexports.snap @@ -18,7 +18,7 @@ export function bar(): string { ``` Exports { "bar" => { - ExportOwnExport => JsOwnExport::Binding(0) + ExportOwnExport => JsOwnExport::Binding(25..28) } } Imports { @@ -29,10 +29,9 @@ Imports { ## Exported Bindings ``` -BindingId(0) => JsBindingData { - Name: bar, - Type: Module(0) TypeId(1), - Declaration kind: HoistedValue +bar => BindingTypeData { + Types Module(0) TypeId(1), + Exported Ranges: (25..28) } ``` @@ -111,7 +110,7 @@ export function foo(): number { ``` Exports { "foo" => { - ExportOwnExport => JsOwnExport::Binding(0) + ExportOwnExport => JsOwnExport::Binding(25..28) } } Imports { @@ -122,10 +121,9 @@ Imports { ## Exported Bindings ``` -BindingId(0) => JsBindingData { - Name: foo, - Type: Module(0) TypeId(1), - Declaration kind: HoistedValue +foo => BindingTypeData { + Types Module(0) TypeId(1), + Exported Ranges: (25..28) } ``` diff --git a/crates/biome_module_graph/tests/snapshots/test_resolve_nested_function_call_with_namespace_in_return_type.snap b/crates/biome_module_graph/tests/snapshots/test_resolve_nested_function_call_with_namespace_in_return_type.snap index 9afe943deac5..f060c955e3cf 100644 --- a/crates/biome_module_graph/tests/snapshots/test_resolve_nested_function_call_with_namespace_in_return_type.snap +++ b/crates/biome_module_graph/tests/snapshots/test_resolve_nested_function_call_with_namespace_in_return_type.snap @@ -53,7 +53,7 @@ export function foo(): Type {} ``` Exports { "foo" => { - ExportOwnExport => JsOwnExport::Binding(0) + ExportOwnExport => JsOwnExport::Binding(25..28) } } Imports { @@ -64,10 +64,9 @@ Imports { ## Exported Bindings ``` -BindingId(0) => JsBindingData { - Name: foo, - Type: Module(0) TypeId(1), - Declaration kind: HoistedValue +foo => BindingTypeData { + Types Module(0) TypeId(1), + Exported Ranges: (25..28) } ``` diff --git a/crates/biome_module_graph/tests/snapshots/test_resolve_promise_export.snap b/crates/biome_module_graph/tests/snapshots/test_resolve_promise_export.snap index 878f26e62809..39b64d3d356e 100644 --- a/crates/biome_module_graph/tests/snapshots/test_resolve_promise_export.snap +++ b/crates/biome_module_graph/tests/snapshots/test_resolve_promise_export.snap @@ -20,7 +20,7 @@ export const promise = returnsPromise(); ``` Exports { "promise" => { - ExportOwnExport => JsOwnExport::Binding(1) + ExportOwnExport => JsOwnExport::Binding(119..126) } } Imports { @@ -31,10 +31,9 @@ Imports { ## Exported Bindings ``` -BindingId(1) => JsBindingData { - Name: promise, - Type: Module(0) TypeId(2), - Declaration kind: Value +promise => BindingTypeData { + Types Module(0) TypeId(2), + Exported Ranges: (119..126) } ``` diff --git a/crates/biome_module_graph/tests/snapshots/test_resolve_promise_from_imported_function_returning_imported_promise_type.snap b/crates/biome_module_graph/tests/snapshots/test_resolve_promise_from_imported_function_returning_imported_promise_type.snap index 58388cef516c..206c44e77e70 100644 --- a/crates/biome_module_graph/tests/snapshots/test_resolve_promise_from_imported_function_returning_imported_promise_type.snap +++ b/crates/biome_module_graph/tests/snapshots/test_resolve_promise_from_imported_function_returning_imported_promise_type.snap @@ -49,7 +49,7 @@ export type PromisedResult = Promise<{ result: true | false }>; ``` Exports { "PromisedResult" => { - ExportOwnExport => JsOwnExport::Binding(0) + ExportOwnExport => JsOwnExport::Binding(12..26) } } Imports { @@ -60,10 +60,9 @@ Imports { ## Exported Bindings ``` -BindingId(0) => JsBindingData { - Name: PromisedResult, - Type: Module(0) TypeId(4), - Declaration kind: Type +PromisedResult => BindingTypeData { + Types Module(0) TypeId(4), + Exported Ranges: (12..26) } ``` @@ -103,7 +102,7 @@ export { returnPromiseResult }; ``` Exports { "returnPromiseResult" => { - ExportOwnExport => JsOwnExport::Binding(1) + ExportOwnExport => JsOwnExport::Binding(77..96) } } Imports { @@ -118,10 +117,9 @@ Imports { ## Exported Bindings ``` -BindingId(1) => JsBindingData { - Name: returnPromiseResult, - Type: Module(0) TypeId(7), - Declaration kind: HoistedValue +returnPromiseResult => BindingTypeData { + Types Module(0) TypeId(7), + Exported Ranges: (215..234) } ``` diff --git a/crates/biome_module_graph/tests/snapshots/test_resolve_promise_from_imported_function_returning_reexported_promise_type.snap b/crates/biome_module_graph/tests/snapshots/test_resolve_promise_from_imported_function_returning_reexported_promise_type.snap index 99290da016d1..6dd991d95d9e 100644 --- a/crates/biome_module_graph/tests/snapshots/test_resolve_promise_from_imported_function_returning_reexported_promise_type.snap +++ b/crates/biome_module_graph/tests/snapshots/test_resolve_promise_from_imported_function_returning_reexported_promise_type.snap @@ -75,7 +75,7 @@ export type PromisedResult = Promise<{ result: true | false }>; ``` Exports { "PromisedResult" => { - ExportOwnExport => JsOwnExport::Binding(0) + ExportOwnExport => JsOwnExport::Binding(12..26) } } Imports { @@ -86,10 +86,9 @@ Imports { ## Exported Bindings ``` -BindingId(0) => JsBindingData { - Name: PromisedResult, - Type: Module(0) TypeId(4), - Declaration kind: Type +PromisedResult => BindingTypeData { + Types Module(0) TypeId(4), + Exported Ranges: (12..26) } ``` @@ -129,7 +128,7 @@ export { returnPromiseResult }; ``` Exports { "returnPromiseResult" => { - ExportOwnExport => JsOwnExport::Binding(1) + ExportOwnExport => JsOwnExport::Binding(71..90) } } Imports { @@ -144,10 +143,9 @@ Imports { ## Exported Bindings ``` -BindingId(1) => JsBindingData { - Name: returnPromiseResult, - Type: Module(0) TypeId(7), - Declaration kind: HoistedValue +returnPromiseResult => BindingTypeData { + Types Module(0) TypeId(7), + Exported Ranges: (209..228) } ``` diff --git a/crates/biome_module_graph/tests/snapshots/test_resolve_recursive_looking_country_info.snap b/crates/biome_module_graph/tests/snapshots/test_resolve_recursive_looking_country_info.snap index 9e7d7f25640c..a055dc4d65b6 100644 --- a/crates/biome_module_graph/tests/snapshots/test_resolve_recursive_looking_country_info.snap +++ b/crates/biome_module_graph/tests/snapshots/test_resolve_recursive_looking_country_info.snap @@ -74,16 +74,16 @@ Exports { ExportOwnExport => JsOwnExport::Type(Module(0) TypeId(20)) } "subdivision" => { - ExportOwnExport => JsOwnExport::Binding(12) + ExportOwnExport => JsOwnExport::Binding(1138..1149) } "country" => { - ExportOwnExport => JsOwnExport::Binding(15) + ExportOwnExport => JsOwnExport::Binding(1266..1273) } "data" => { - ExportOwnExport => JsOwnExport::Binding(17) + ExportOwnExport => JsOwnExport::Binding(1336..1340) } "codes" => { - ExportOwnExport => JsOwnExport::Binding(18) + ExportOwnExport => JsOwnExport::Binding(1414..1419) } } Imports { @@ -94,28 +94,24 @@ Imports { ## Exported Bindings ``` -BindingId(12) => JsBindingData { - Name: subdivision, - Type: Module(0) TypeId(15), - Declaration kind: HoistedValue +subdivision => BindingTypeData { + Types Module(0) TypeId(15), + Exported Ranges: } -BindingId(15) => JsBindingData { - Name: country, - Type: Module(0) TypeId(18), - Declaration kind: HoistedValue +country => BindingTypeData { + Types Module(0) TypeId(18), + Exported Ranges: } -BindingId(17) => JsBindingData { - Name: data, - Type: Module(0) TypeId(1), - Declaration kind: Value +data => BindingTypeData { + Types Module(0) TypeId(1), + Exported Ranges: (1336..1340) } -BindingId(18) => JsBindingData { - Name: codes, - Type: Module(0) TypeId(2), - Declaration kind: Value +codes => BindingTypeData { + Types Module(0) TypeId(2), + Exported Ranges: (1414..1419) } ``` diff --git a/crates/biome_module_graph/tests/snapshots/test_resolve_single_reexport.snap b/crates/biome_module_graph/tests/snapshots/test_resolve_single_reexport.snap index 7b3bbe305559..0bd1a36cb849 100644 --- a/crates/biome_module_graph/tests/snapshots/test_resolve_single_reexport.snap +++ b/crates/biome_module_graph/tests/snapshots/test_resolve_single_reexport.snap @@ -71,7 +71,7 @@ export function foo(): number { ``` Exports { "foo" => { - ExportOwnExport => JsOwnExport::Binding(0) + ExportOwnExport => JsOwnExport::Binding(25..28) } } Imports { @@ -82,10 +82,9 @@ Imports { ## Exported Bindings ``` -BindingId(0) => JsBindingData { - Name: foo, - Type: Module(0) TypeId(1), - Declaration kind: HoistedValue +foo => BindingTypeData { + Types Module(0) TypeId(1), + Exported Ranges: (25..28) } ``` diff --git a/crates/biome_module_graph/tests/snapshots/test_resolve_type_of_this_in_class_export.snap b/crates/biome_module_graph/tests/snapshots/test_resolve_type_of_this_in_class_export.snap index b9618d953d24..9560d0369bbf 100644 --- a/crates/biome_module_graph/tests/snapshots/test_resolve_type_of_this_in_class_export.snap +++ b/crates/biome_module_graph/tests/snapshots/test_resolve_type_of_this_in_class_export.snap @@ -55,7 +55,7 @@ const foo7 = obj.inObject(); ``` Exports { "default" => { - ExportOwnExport => JsOwnExport::Binding(0) + ExportOwnExport => JsOwnExport::Binding(21..24) } } Imports { @@ -66,10 +66,9 @@ Imports { ## Exported Bindings ``` -BindingId(0) => JsBindingData { - Name: Foo, - Type: Module(0) TypeId(23), - Declaration kind: Unknown +Foo => BindingTypeData { + Types Module(0) TypeId(23), + Exported Ranges: (21..24) } ``` diff --git a/crates/biome_module_graph/tests/spec_tests.rs b/crates/biome_module_graph/tests/spec_tests.rs index 59f24fcac61e..4aa25433dad6 100644 --- a/crates/biome_module_graph/tests/spec_tests.rs +++ b/crates/biome_module_graph/tests/spec_tests.rs @@ -9,7 +9,8 @@ use std::sync::Arc; use crate::snap::ModuleGraphSnapshot; use biome_deserialize::json::deserialize_from_json_str; use biome_fs::{BiomePath, FileSystem, MemoryFileSystem, OsFileSystem, normalize_path}; -use biome_js_type_info::{ScopeId, TypeData, TypeResolver}; +use biome_js_semantic::ScopeId; +use biome_js_type_info::{TypeData, TypeResolver}; use biome_jsdoc_comment::JsdocComment; use biome_json_parser::{JsonParserOptions, parse_json}; use biome_json_value::{JsonObject, JsonString}; @@ -373,6 +374,59 @@ fn test_export_default_function_declaration() { snapshot.assert_snapshot("test_export_default_function_declaration"); } +#[test] +fn test_export_default_imported_binding() { + let fs = MemoryFileSystem::default(); + fs.insert( + "/src/foo.ts".into(), + r#" + /** + * @returns {number} + */ + export function foo(): number { + return 42; + } + "#, + ); + fs.insert( + "/src/index.ts".into(), + r#" + import { foo } from "./foo.ts"; + + export default foo; + "#, + ); + + let added_paths = [ + BiomePath::new("/src/foo.ts"), + BiomePath::new("/src/index.ts"), + ]; + let added_paths = get_added_js_paths(&fs, &added_paths); + + let module_graph = Arc::new(ModuleGraph::default()); + module_graph.update_graph_for_js_paths(&fs, &ProjectLayout::default(), &added_paths, true); + + let index_module = module_graph + .js_module_info_for_path(Utf8Path::new("/src/index.ts")) + .expect("module must exist"); + let resolver = Arc::new(ModuleResolver::for_module( + index_module, + module_graph.clone(), + )); + + // Test that the default export's type is correctly resolved as a function returning number + let default_export_ty = resolver + .resolved_type_of_default_export() + .expect("default export must exist"); + assert!( + default_export_ty.is_function(), + "Default export should be a function, got: {default_export_ty:?}" + ); + + let snapshot = ModuleGraphSnapshot::new(module_graph.as_ref(), &fs).with_resolver(&resolver); + snapshot.assert_snapshot("test_export_default_imported_binding"); +} + #[test] fn test_export_const_type_declaration_with_namespace() { let fs = MemoryFileSystem::default(); diff --git a/crates/biome_service/src/workspace/server.rs b/crates/biome_service/src/workspace/server.rs index 59344faa6396..d427170aa8f2 100644 --- a/crates/biome_service/src/workspace/server.rs +++ b/crates/biome_service/src/workspace/server.rs @@ -1013,13 +1013,24 @@ impl WorkspaceServer { match update_kind { UpdateKind::AddedOrChanged(_, root, services) => { // NOTE: add a new else if branch to handle other language roots - if let Some(js_root) = SendNode::into_language_root::(root.clone()) { - self.module_graph.update_graph_for_js_paths( - self.fs.as_ref(), - &self.project_layout, - &[(path, js_root)], - infer_types, - ) + if let (Some(js_root), Some(services)) = ( + SendNode::into_language_root::(root.clone()), + services.as_js_services(), + ) { + // Module graph requires a semantic model to operate. + // If the semantic model is not available (e.g., due to parse errors), + // we skip module graph updates for this file. + if let Some(semantic_model) = services.semantic_model.clone() { + self.module_graph.update_graph_for_js_paths( + self.fs.as_ref(), + &self.project_layout, + &[(path, js_root, Arc::new(semantic_model))], + infer_types, + ) + } else { + // No semantic model available - return empty result + Default::default() + } } else if let (Some(css_root), Some(services)) = ( SendNode::into_language_root::(root.clone()), services.as_css_services(), diff --git a/crates/biome_test_utils/Cargo.toml b/crates/biome_test_utils/Cargo.toml index 0cf84a141437..2766c347b1cb 100644 --- a/crates/biome_test_utils/Cargo.toml +++ b/crates/biome_test_utils/Cargo.toml @@ -25,6 +25,7 @@ biome_formatter = { workspace = true } biome_fs = { workspace = true } biome_js_analyze = { workspace = true } biome_js_parser = { workspace = true } +biome_js_semantic = { workspace = true } biome_js_type_info = { workspace = true } biome_json_parser = { workspace = true } biome_module_graph = { workspace = true } diff --git a/crates/biome_test_utils/src/lib.rs b/crates/biome_test_utils/src/lib.rs index 0b535e8d2d7a..53fdb2228cc2 100644 --- a/crates/biome_test_utils/src/lib.rs +++ b/crates/biome_test_utils/src/lib.rs @@ -260,7 +260,11 @@ pub fn module_graph_for_test_file( pub fn get_added_js_paths<'a>( fs: &dyn FileSystem, paths: &'a [BiomePath], -) -> Vec<(&'a BiomePath, AnyJsRoot)> { +) -> Vec<( + &'a BiomePath, + AnyJsRoot, + std::sync::Arc, +)> { paths .iter() .filter_map(|path| { @@ -280,7 +284,14 @@ pub fn get_added_js_paths<'a>( ); parsed.try_tree() })?; - Some((path, root)) + + // Build semantic model for the parsed root + let semantic_model = biome_js_semantic::semantic_model( + &root, + biome_js_semantic::SemanticModelOptions::default(), + ); + + Some((path, root, std::sync::Arc::new(semantic_model))) }) .collect() } diff --git a/crates/biome_text_size/src/range.rs b/crates/biome_text_size/src/range.rs index 3a4994880841..2e6eaa6c40b2 100644 --- a/crates/biome_text_size/src/range.rs +++ b/crates/biome_text_size/src/range.rs @@ -27,6 +27,12 @@ impl fmt::Debug for TextRange { } } +impl fmt::Display for TextRange { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&self, f) + } +} + impl PartialOrd for TextRange { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) diff --git a/crates/biome_text_size/src/size.rs b/crates/biome_text_size/src/size.rs index 50a89d4d3ca1..d148424b5fb7 100644 --- a/crates/biome_text_size/src/size.rs +++ b/crates/biome_text_size/src/size.rs @@ -1,3 +1,4 @@ +use std::fmt::Formatter; use { crate::TextLen, std::{ @@ -31,6 +32,12 @@ impl fmt::Debug for TextSize { } } +impl fmt::Display for TextSize { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(self, f) + } +} + impl TextSize { /// The text size of some primitive text-like object. ///