Skip to content

Commit 57c15e6

Browse files
fireairforcearendjrdyc3
authored
feat(parser): support import source (#7019)
Co-authored-by: Arend van Beelen jr. <[email protected]> Co-authored-by: Carson McManus <[email protected]>
1 parent 2ec91e3 commit 57c15e6

38 files changed

+878
-556
lines changed

.changeset/tangy-chicken-start.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Added support in the JS parser for `import source`(a [stage3 proposal](https://github.com/tc39/proposal-source-phase-imports)). The syntax looks like:
6+
7+
```ts
8+
import source foo from "<specifier>";
9+
```

crates/biome_grit_patterns/src/grit_node_patterns.rs

Lines changed: 105 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use grit_pattern_matcher::pattern::{
1010
ResolvedPattern, State,
1111
};
1212
use grit_util::error::GritResult;
13-
use grit_util::{AnalysisLogs, Language};
13+
use grit_util::{AnalysisLogs, AstNode, Language};
1414

1515
/// Check if two syntax kinds are compatible for import pattern matching
1616
fn are_import_kinds_compatible(
@@ -32,9 +32,20 @@ fn are_import_kinds_compatible(
3232
(JS_IMPORT_DEFAULT_CLAUSE, JS_IMPORT_NAMED_CLAUSE) => true,
3333
// Named import pattern can match default import
3434
(JS_IMPORT_NAMED_CLAUSE, JS_IMPORT_DEFAULT_CLAUSE) => true,
35-
// Default import specifier can match named import specifiers
36-
(JS_DEFAULT_IMPORT_SPECIFIER, JS_NAMED_IMPORT_SPECIFIERS) => true,
37-
(JS_NAMED_IMPORT_SPECIFIERS, JS_DEFAULT_IMPORT_SPECIFIER) => true,
35+
// Default import specifier can match individual named specifiers
36+
(JS_DEFAULT_IMPORT_SPECIFIER, JS_SHORTHAND_NAMED_IMPORT_SPECIFIER) => true,
37+
(JS_DEFAULT_IMPORT_SPECIFIER, JS_NAMED_IMPORT_SPECIFIER) => true,
38+
(JS_DEFAULT_IMPORT_SPECIFIER, JS_NAMESPACE_IMPORT_SPECIFIER) => true,
39+
// Named specifiers can match default specifier
40+
(JS_SHORTHAND_NAMED_IMPORT_SPECIFIER, JS_DEFAULT_IMPORT_SPECIFIER) => true,
41+
(JS_NAMED_IMPORT_SPECIFIER, JS_DEFAULT_IMPORT_SPECIFIER) => true,
42+
(JS_NAMESPACE_IMPORT_SPECIFIER, JS_DEFAULT_IMPORT_SPECIFIER) => true,
43+
// Import clauses can match each other
44+
(JS_IMPORT_DEFAULT_CLAUSE, JS_IMPORT_DEFAULT_CLAUSE) => true,
45+
(JS_IMPORT_NAMED_CLAUSE, JS_IMPORT_NAMED_CLAUSE) => true,
46+
(JS_IMPORT_NAMESPACE_CLAUSE, JS_IMPORT_NAMESPACE_CLAUSE) => true,
47+
(JS_IMPORT_COMBINED_CLAUSE, JS_IMPORT_COMBINED_CLAUSE) => true,
48+
(JS_IMPORT_BARE_CLAUSE, JS_IMPORT_BARE_CLAUSE) => true,
3849
_ => false,
3950
}
4051
}
@@ -86,9 +97,16 @@ impl Matcher<GritQueryContext> for GritNodePattern {
8697
);
8798
}
8899

89-
if node.kind() != self.kind && !are_import_kinds_compatible(self.kind, node.kind()) {
100+
// Check if we need to handle import structure transformation
101+
let needs_import_transformation = self.is_import_transformation_needed(node.kind());
102+
103+
if node.kind() != self.kind
104+
&& !are_import_kinds_compatible(self.kind, node.kind())
105+
&& !needs_import_transformation
106+
{
90107
return Ok(false);
91108
}
109+
92110
if self.args.is_empty() {
93111
return Ok(true);
94112
}
@@ -97,7 +115,6 @@ impl Matcher<GritQueryContext> for GritNodePattern {
97115
let Some(range) = context.language().comment_text_range(&node) else {
98116
return Ok(false);
99117
};
100-
101118
return self.args[0].pattern.execute(
102119
&ResolvedPattern::from_range_binding(range, node.text()),
103120
init_state,
@@ -114,15 +131,70 @@ impl Matcher<GritQueryContext> for GritNodePattern {
114131
{
115132
let mut cur_state = running_state.clone();
116133

117-
let res = pattern.execute(
118-
&match node.child_by_slot_index(*slot_index) {
134+
// Get child binding with import transformation if needed
135+
let child_binding = if needs_import_transformation {
136+
use biome_js_syntax::JsSyntaxKind::*;
137+
let Some(pattern_js_kind) = self.kind.as_js_kind() else {
138+
return Ok(false);
139+
};
140+
let Some(node_js_kind) = node.kind().as_js_kind() else {
141+
return Ok(false);
142+
};
143+
144+
// Helper function
145+
let get_child = |source: u32| {
146+
node.child_by_slot_index(source).map_or(
147+
GritResolvedPattern::from_empty_binding(node.clone(), *slot_index),
148+
GritResolvedPattern::from_node_binding,
149+
)
150+
};
151+
152+
// Import slot mapping table
153+
let source_slot = match (pattern_js_kind, node_js_kind, slot_index) {
154+
// type_token, phase_token
155+
(JS_IMPORT_DEFAULT_CLAUSE, JS_IMPORT_NAMED_CLAUSE, 0 | 1) => None,
156+
// default_specifier -> first named specifier
157+
(JS_IMPORT_DEFAULT_CLAUSE, JS_IMPORT_NAMED_CLAUSE, 2) => Some(1),
158+
// from_token, source, assertion
159+
(JS_IMPORT_DEFAULT_CLAUSE, JS_IMPORT_NAMED_CLAUSE, 3..=5) => {
160+
Some(slot_index - 1)
161+
}
162+
// type_token
163+
(JS_IMPORT_NAMED_CLAUSE, JS_IMPORT_DEFAULT_CLAUSE, 0) => Some(0),
164+
// named_specifiers -> default specifier
165+
(JS_IMPORT_NAMED_CLAUSE, JS_IMPORT_DEFAULT_CLAUSE, 1) => Some(2),
166+
// from_token, source, assertion
167+
(JS_IMPORT_NAMED_CLAUSE, JS_IMPORT_DEFAULT_CLAUSE, 2..=4) => {
168+
Some(slot_index + 1)
169+
}
170+
// normal case
171+
_ => Some(*slot_index),
172+
};
173+
174+
match source_slot {
175+
None => GritResolvedPattern::from_empty_binding(node.clone(), *slot_index),
176+
Some(1)
177+
if pattern_js_kind == JS_IMPORT_DEFAULT_CLAUSE
178+
&& node_js_kind == JS_IMPORT_NAMED_CLAUSE =>
179+
{
180+
// Special case: default_specifier -> first named specifier
181+
node.child_by_slot_index(1)
182+
.and_then(|specifiers| specifiers.children().next())
183+
.map_or(
184+
GritResolvedPattern::from_empty_binding(node.clone(), *slot_index),
185+
GritResolvedPattern::from_node_binding,
186+
)
187+
}
188+
Some(source) => get_child(source),
189+
}
190+
} else {
191+
match node.child_by_slot_index(*slot_index) {
119192
Some(child) => GritResolvedPattern::from_node_binding(child),
120193
None => GritResolvedPattern::from_empty_binding(node.clone(), *slot_index),
121-
},
122-
&mut cur_state,
123-
context,
124-
logs,
125-
);
194+
}
195+
};
196+
197+
let res = pattern.execute(&child_binding, &mut cur_state, context, logs);
126198
if res? {
127199
running_state = cur_state;
128200
} else {
@@ -140,6 +212,26 @@ impl PatternName for GritNodePattern {
140212
}
141213
}
142214

215+
impl GritNodePattern {
216+
/// Check if this pattern needs import structure transformation
217+
fn is_import_transformation_needed(&self, node_kind: GritTargetSyntaxKind) -> bool {
218+
use biome_js_syntax::JsSyntaxKind::*;
219+
220+
let Some(pattern_js_kind) = self.kind.as_js_kind() else {
221+
return false;
222+
};
223+
let Some(node_js_kind) = node_kind.as_js_kind() else {
224+
return false;
225+
};
226+
227+
matches!(
228+
(pattern_js_kind, node_js_kind),
229+
(JS_IMPORT_DEFAULT_CLAUSE, JS_IMPORT_NAMED_CLAUSE)
230+
| (JS_IMPORT_NAMED_CLAUSE, JS_IMPORT_DEFAULT_CLAUSE)
231+
)
232+
}
233+
}
234+
143235
#[derive(Clone, Debug)]
144236
pub struct GritNodePatternArg {
145237
pub slot_index: u32,

crates/biome_js_factory/src/generated/node_factory.rs

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_js_factory/src/generated/syntax_factory.rs

Lines changed: 8 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_js_formatter/src/js/module/import_default_clause.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ impl FormatNodeRule<JsImportDefaultClause> for FormatJsImportDefaultClause {
1111
fn fmt_fields(&self, node: &JsImportDefaultClause, f: &mut JsFormatter) -> FormatResult<()> {
1212
let JsImportDefaultClauseFields {
1313
type_token,
14+
phase_token,
1415
default_specifier,
1516
from_token,
1617
source,
@@ -21,6 +22,10 @@ impl FormatNodeRule<JsImportDefaultClause> for FormatJsImportDefaultClause {
2122
write!(f, [type_token.format(), space()])?;
2223
}
2324

25+
if let Some(phase_token) = phase_token {
26+
write!(f, [phase_token.format(), space()])?;
27+
}
28+
2429
write![
2530
f,
2631
[

crates/biome_js_formatter/tests/specs/js/module/import/default_import.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,6 @@ import foo from "foo.json" with {
99
"json" };
1010
import foo2 from "foo.json" with { "type": "json", type: "html", "type": "js" };
1111
import a, * as b from "foo"
12+
import source x from "x";
13+
/* 0 */import /* 1 */ source /* 2 */ x /* 3 */ from /* 4 */ "x" /* 5 */;
14+
import source s from "x" with { attr: "val" };

crates/biome_js_formatter/tests/specs/js/module/import/default_import.js.snap

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ import foo from "foo.json" with {
1616
"json" };
1717
import foo2 from "foo.json" with { "type": "json", type: "html", "type": "js" };
1818
import a, * as b from "foo"
19-
19+
import source x from "x";
20+
/* 0 */import /* 1 */ source /* 2 */ x /* 3 */ from /* 4 */ "x" /* 5 */;
21+
import source s from "x" with { attr: "val" };
2022
```
2123

2224

@@ -52,4 +54,7 @@ import foo from "foo.json" with { type: "json" };
5254
import foo from "foo.json" with { type: "json" };
5355
import foo2 from "foo.json" with { type: "json", type: "html", "type": "js" };
5456
import a, * as b from "foo";
57+
import source x from "x";
58+
/* 0 */ import /* 1 */ source /* 2 */ x /* 3 */ from /* 4 */ "x" /* 5 */;
59+
import source s from "x" with { attr: "val" };
5560
```

crates/biome_js_formatter/tests/specs/prettier/js/babel-plugins/source-phase-imports.js.snap

Lines changed: 9 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -16,87 +16,26 @@ import.source("x");
1616
```diff
1717
--- Prettier
1818
+++ Biome
19-
@@ -1,2 +1,5 @@
20-
-import source fooSource from "foo";
21-
+import source
22-
+fooSource;
23-
+from;
24-
+("foo");
25-
import.source("x");
19+
@@ -1,2 +1,3 @@
20+
import source fooSource from "foo";
21+
-import.source("x");
22+
+import.
23+
+source("x");
2624
```
2725

2826
# Output
2927

3028
```js
31-
import source
32-
fooSource;
33-
from;
34-
("foo");
35-
import.source("x");
29+
import source fooSource from "foo";
30+
import.
31+
source("x");
3632
```
3733

3834
# Errors
3935
```
40-
source-phase-imports.js:1:15 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
41-
42-
× expected `from` but instead found `fooSource`
43-
44-
> 1 │ import source fooSource from "foo";
45-
│ ^^^^^^^^^
46-
2 │ import.source("x");
47-
3 │
48-
49-
i Remove fooSource
50-
51-
source-phase-imports.js:1:25 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
52-
53-
× Expected a semicolon or an implicit semicolon after a statement, but found none
54-
55-
> 1 │ import source fooSource from "foo";
56-
│ ^^^^
57-
2 │ import.source("x");
58-
3 │
59-
60-
i An explicit or implicit semicolon is expected here...
61-
62-
> 1 │ import source fooSource from "foo";
63-
│ ^^^^
64-
2 │ import.source("x");
65-
3 │
66-
67-
i ...Which is required to end this statement
68-
69-
> 1 │ import source fooSource from "foo";
70-
│ ^^^^^^^^^^^^^^
71-
2 │ import.source("x");
72-
3 │
73-
74-
source-phase-imports.js:1:30 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
75-
76-
× Expected a semicolon or an implicit semicolon after a statement, but found none
77-
78-
> 1 │ import source fooSource from "foo";
79-
│ ^^^^^
80-
2 │ import.source("x");
81-
3 │
82-
83-
i An explicit or implicit semicolon is expected here...
84-
85-
> 1 │ import source fooSource from "foo";
86-
│ ^^^^^
87-
2 │ import.source("x");
88-
3 │
89-
90-
i ...Which is required to end this statement
91-
92-
> 1 │ import source fooSource from "foo";
93-
│ ^^^^^^^^^^
94-
2 │ import.source("x");
95-
3 │
96-
9736
source-phase-imports.js:2:8 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
9837
99-
× Expected `meta` following an import keyword, but found `source`
38+
× Expected `meta` following an import keyword, but found none
10039
10140
1 │ import source fooSource from "foo";
10241
> 2 │ import.source("x");

0 commit comments

Comments
 (0)