Skip to content

Commit 38d2098

Browse files
authored
feat(resolver): resolve extension alias (#7565)
1 parent 74d3c56 commit 38d2098

File tree

18 files changed

+290
-8
lines changed

18 files changed

+290
-8
lines changed

.changeset/petite-waves-love.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
The resolver can now correctly resolve `.ts`, `.tsx`, `.d.ts`, `.js` files by `.js` extension if exists, based on [the file extension substitution in TypeScript](https://www.typescriptlang.org/docs/handbook/modules/reference.html#file-extension-substitution).
6+
7+
For example, the linter can now detect the floating promise in the following situation, if you have enabled the `noFloatingPromises` rule.
8+
9+
**`foo.ts`**
10+
```ts
11+
export async function doSomething(): Promise<void> {}
12+
```
13+
14+
**`bar.ts`**
15+
```ts
16+
import { doSomething } from "./foo.js"; // doesn't exist actually, but it is resolved to `foo.ts`
17+
18+
doSomething(); // floating promise!
19+
```

.changeset/silent-insects-flow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
The [`useImportExtensions`](https://biomejs.dev/linter/rules/use-import-extensions/) rule now correctly detects imports with an invalid extension. For example, importing `.ts` file with `.js` extension is flagged by default. If you are using TypeScript with neither the `allowImportingTsExtensions` option nor the `rewriteRelativeImportExtensions` option, it's recommended to turn on the `forceJsExtensions` option of the rule.

crates/biome_js_analyze/src/lint/correctness/use_import_extensions.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,6 @@ fn get_extensionless_import(
229229
(Some(_), _) if path.file_name()?.starts_with(resolved_path.file_name()?) => {
230230
return None; // For cases like `./foo.css` -> `./foo.css.ts`
231231
}
232-
(None, Some(_)) => return None,
233232
_ => {}
234233
}
235234

@@ -250,8 +249,17 @@ fn get_extensionless_import(
250249
resolved_extension?
251250
};
252251

252+
// Check if the existing extension matches the desired one.
253+
if existing_extension.is_some_and(|ext| ext == extension) {
254+
return None;
255+
}
256+
253257
let is_index_file = resolved_stem.is_some_and(|stem| stem == "index");
254-
let import_path_ends_with_index = path.file_name().is_some_and(|name| name == "index");
258+
let import_path_ends_with_index = path.file_name().is_some_and(|name| name == "index")
259+
|| path
260+
.with_extension("")
261+
.file_name()
262+
.is_some_and(|name| name == "index");
255263

256264
let new_path = if is_index_file && !import_path_ends_with_index {
257265
let mut path_parts = path.as_str().split('/');

crates/biome_js_analyze/tests/specs/correctness/useImportExtensions/invalid.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,8 @@ require("./sub/foo")
2424

2525
import "./sub/styles.css"
2626
import "./sub/component.svg.svelte";
27-
import "./sub/component.svg.svelte?query=string&query2#hash";
27+
import "./sub/component.svg.svelte?query=string&query2#hash";
28+
29+
// Invalid extension
30+
import "./sub/foo.js";
31+
import "./sub/bar/index.js";

crates/biome_js_analyze/tests/specs/correctness/useImportExtensions/invalid.ts.snap

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ require("./sub/foo")
3131
import "./sub/styles.css"
3232
import "./sub/component.svg.svelte";
3333
import "./sub/component.svg.svelte?query=string&query2#hash";
34+
35+
// Invalid extension
36+
import "./sub/foo.js";
37+
import "./sub/bar/index.js";
38+
3439
```
3540

3641
# Diagnostics
@@ -445,6 +450,7 @@ invalid.ts:26:8 lint/correctness/useImportExtensions FIXABLE ━━━━━
445450
> 26 │ import "./sub/component.svg.svelte";
446451
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
447452
27 │ import "./sub/component.svg.svelte?query=string&query2#hash";
453+
28 │
448454
449455
i Explicit import improves compatibility with browsers and makes file resolution in tooling faster.
450456
@@ -455,6 +461,7 @@ invalid.ts:26:8 lint/correctness/useImportExtensions FIXABLE ━━━━━
455461
26 │ - import·"./sub/component.svg.svelte";
456462
26 │ + import·"./sub/component.svg.svelte.ts";
457463
27 27 │ import "./sub/component.svg.svelte?query=string&query2#hash";
464+
28 28 │
458465
459466
460467
```
@@ -468,8 +475,59 @@ invalid.ts:27:8 lint/correctness/useImportExtensions ━━━━━━━━━
468475
26 │ import "./sub/component.svg.svelte";
469476
> 27 │ import "./sub/component.svg.svelte?query=string&query2#hash";
470477
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
478+
28 │
479+
29 │ // Invalid extension
471480
472481
i Explicit import improves compatibility with browsers and makes file resolution in tooling faster.
473482
474483
475484
```
485+
486+
```
487+
invalid.ts:30:8 lint/correctness/useImportExtensions FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
488+
489+
! Add a file extension for relative imports.
490+
491+
29 │ // Invalid extension
492+
> 30 │ import "./sub/foo.js";
493+
│ ^^^^^^^^^^^^^^
494+
31 │ import "./sub/bar/index.js";
495+
32 │
496+
497+
i Explicit import improves compatibility with browsers and makes file resolution in tooling faster.
498+
499+
i Safe fix: Add import extension .ts.
500+
501+
28 28 │
502+
29 29 │ // Invalid extension
503+
30 │ - import·"./sub/foo.js";
504+
30 │ + import·"./sub/foo.ts";
505+
31 31 │ import "./sub/bar/index.js";
506+
32 32 │
507+
508+
509+
```
510+
511+
```
512+
invalid.ts:31:8 lint/correctness/useImportExtensions FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
513+
514+
! Add a file extension for relative imports.
515+
516+
29 │ // Invalid extension
517+
30 │ import "./sub/foo.js";
518+
> 31 │ import "./sub/bar/index.js";
519+
│ ^^^^^^^^^^^^^^^^^^^^
520+
32 │
521+
522+
i Explicit import improves compatibility with browsers and makes file resolution in tooling faster.
523+
524+
i Safe fix: Add import extension .ts.
525+
526+
29 29 │ // Invalid extension
527+
30 30 │ import "./sub/foo.js";
528+
31 │ - import·"./sub/bar/index.js";
529+
31 │ + import·"./sub/bar/index.ts";
530+
32 32 │
531+
532+
533+
```

crates/biome_js_analyze/tests/specs/correctness/useImportExtensions/invalidWithForcedJsExtensions.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,8 @@ require("./sub/foo")
2323

2424
import "./sub/styles.css"
2525
import "./sub/component.svg.svelte";
26-
import "./sub/component.svg.svelte?query=string&query2#hash";
26+
import "./sub/component.svg.svelte?query=string&query2#hash";
27+
28+
// Invalid extension
29+
import "./sub/foo.ts";
30+
import "./sub/bar/index.ts";

crates/biome_js_analyze/tests/specs/correctness/useImportExtensions/invalidWithForcedJsExtensions.ts.snap

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ require("./sub/foo")
3030
import "./sub/styles.css"
3131
import "./sub/component.svg.svelte";
3232
import "./sub/component.svg.svelte?query=string&query2#hash";
33+
34+
// Invalid extension
35+
import "./sub/foo.ts";
36+
import "./sub/bar/index.ts";
37+
3338
```
3439

3540
# Diagnostics
@@ -418,6 +423,7 @@ invalidWithForcedJsExtensions.ts:25:8 lint/correctness/useImportExtensions FIXA
418423
> 25 │ import "./sub/component.svg.svelte";
419424
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
420425
26 │ import "./sub/component.svg.svelte?query=string&query2#hash";
426+
27 │
421427
422428
i Explicit import improves compatibility with browsers and makes file resolution in tooling faster.
423429
@@ -428,6 +434,7 @@ invalidWithForcedJsExtensions.ts:25:8 lint/correctness/useImportExtensions FIXA
428434
25 │ - import·"./sub/component.svg.svelte";
429435
25 │ + import·"./sub/component.svg.svelte.js";
430436
26 26 │ import "./sub/component.svg.svelte?query=string&query2#hash";
437+
27 27 │
431438
432439
433440
```
@@ -441,8 +448,59 @@ invalidWithForcedJsExtensions.ts:26:8 lint/correctness/useImportExtensions ━
441448
25 │ import "./sub/component.svg.svelte";
442449
> 26 │ import "./sub/component.svg.svelte?query=string&query2#hash";
443450
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
451+
27 │
452+
28 │ // Invalid extension
444453
445454
i Explicit import improves compatibility with browsers and makes file resolution in tooling faster.
446455
447456
448457
```
458+
459+
```
460+
invalidWithForcedJsExtensions.ts:29:8 lint/correctness/useImportExtensions FIXABLE ━━━━━━━━━━━━━━━
461+
462+
! Add a file extension for relative imports.
463+
464+
28 │ // Invalid extension
465+
> 29 │ import "./sub/foo.ts";
466+
│ ^^^^^^^^^^^^^^
467+
30 │ import "./sub/bar/index.ts";
468+
31 │
469+
470+
i Explicit import improves compatibility with browsers and makes file resolution in tooling faster.
471+
472+
i Safe fix: Add import extension .js.
473+
474+
27 27 │
475+
28 28 │ // Invalid extension
476+
29 │ - import·"./sub/foo.ts";
477+
29 │ + import·"./sub/foo.js";
478+
30 30 │ import "./sub/bar/index.ts";
479+
31 31 │
480+
481+
482+
```
483+
484+
```
485+
invalidWithForcedJsExtensions.ts:30:8 lint/correctness/useImportExtensions FIXABLE ━━━━━━━━━━━━━━━
486+
487+
! Add a file extension for relative imports.
488+
489+
28 │ // Invalid extension
490+
29 │ import "./sub/foo.ts";
491+
> 30 │ import "./sub/bar/index.ts";
492+
│ ^^^^^^^^^^^^^^^^^^^^
493+
31 │
494+
495+
i Explicit import improves compatibility with browsers and makes file resolution in tooling faster.
496+
497+
i Safe fix: Add import extension .js.
498+
499+
28 28 │ // Invalid extension
500+
29 29 │ import "./sub/foo.ts";
501+
30 │ - import·"./sub/bar/index.ts";
502+
30 │ + import·"./sub/bar/index.js";
503+
31 31 │
504+
505+
506+
```

crates/biome_module_graph/src/js_module_info/visitor.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@ use crate::{
1818

1919
use super::{ResolvedPath, collector::JsModuleInfoCollector};
2020

21+
/// Extensions to try to resolve based on the extension in the import specifier.
22+
/// ref: https://www.typescriptlang.org/docs/handbook/modules/reference.html#the-moduleresolution-compiler-option
23+
const EXTENSION_ALIASES: &[(&str, &[&str])] = &[
24+
("js", &["ts", "tsx", "d.ts", "js", "jsx"]),
25+
("mjs", &["mts", "d.mts", "mjs"]),
26+
("cjs", &["cts", "d.cts", "cjs"]),
27+
];
28+
2129
pub(crate) struct JsModuleVisitor<'a> {
2230
root: AnyJsRoot,
2331
directory: &'a Utf8Path,
@@ -401,7 +409,7 @@ impl<'a> JsModuleVisitor<'a> {
401409
if let Ok(binding) = node.pattern() {
402410
self.visit_binding_pattern(
403411
binding,
404-
collector,
412+
collector,
405413
);
406414
}
407415
}
@@ -450,6 +458,7 @@ impl<'a> JsModuleVisitor<'a> {
450458
condition_names: &["types", "import", "default"],
451459
default_files: &["index"],
452460
extensions: SUPPORTED_EXTENSIONS,
461+
extension_aliases: EXTENSION_ALIASES,
453462
resolve_node_builtins: true,
454463
resolve_types: true,
455464
..Default::default()

0 commit comments

Comments
 (0)