1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
#![warn(unused_must_use)]
use crate::lexer::T;
use crate::p::P;
use crate::parser::{JSXTag, options};
use bun_ast::expr::Data as ExprData;
use bun_ast::flags;
use bun_ast::op::Level;
use bun_ast::{E, Expr, ExprNodeIndex, ExprNodeList, G};
use bun_collections::VecExt;
use bun_core::err;
// Zig: `pub fn ParseJSXElement(comptime ...) type { return struct { ... } }`
// — file-split mixin pattern. Round-C lowered `const JSX: JSXTransformType` → `J: JsxT`
// (sealed trait + ZST), so this becomes a direct `impl` on `P` instead of a wrapper struct.
impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_ONLY> {
pub fn parse_jsx_element(&mut self, loc: bun_ast::Loc) -> Result<Expr, bun_core::Error> {
let p = self;
// Nested child elements (`<a><b><c>...`) recurse back into this function,
// so guard the stack the same way the other recursive parse entry points do.
if !p.stack_check.is_safe_to_recurse() {
return Err(err!("StackOverflow"));
}
if SCAN_ONLY {
p.needs_jsx_import = true;
}
let tag = JSXTag::parse(p)?;
// The tag may have TypeScript type arguments: "<Foo<T>/>"
if TYPESCRIPT {
// Pass a flag to the type argument skipper because we need to call
let _ = p.skip_type_script_type_arguments::<true>()?;
}
let mut previous_string_with_backslash_loc = bun_ast::Loc::default();
let mut properties = bun_alloc::AstAlloc::vec();
let mut key_prop_i: i32 = -1;
let mut flags = flags::JSXElementBitset::empty();
let mut start_tag: Option<ExprNodeIndex> = None;
let mut can_be_inlined = false;
// Fragments don't have props
// Fragments of the form "React.Fragment" are not parsed as fragments.
if let Some(t) = tag.data.as_expr() {
start_tag = Some(t);
can_be_inlined = p.options.features.jsx_optimization_inline;
let mut spread_loc: bun_ast::Loc = bun_ast::Loc::EMPTY;
let mut props: Vec<G::Property> = Vec::new();
let mut first_spread_prop_i: i32 = -1;
let mut i: i32 = 0;
'parse_attributes: loop {
match p.lexer.token {
T::TIdentifier => {
// PORT NOTE: `defer i += 1` inlined at each exit point of this arm.
// Parse the prop name
let key_range = p.lexer.range();
let prop_name_literal = p.lexer.identifier;
let special_prop = E::JSXSpecialProp::from_bytes(prop_name_literal)
.unwrap_or(E::JSXSpecialProp::Any);
p.lexer.next_inside_jsx_element()?;
if special_prop == E::JSXSpecialProp::Key {
// <ListItem key>
if p.lexer.token != T::TEquals {
// Unlike Babel, we're going to just warn here and move on.
p.log().add_warning(
Some(p.source),
key_range.loc,
b"\"key\" prop ignored. Must be a string, number or symbol.",
);
continue;
}
key_prop_i = i;
}
let prop_name =
p.new_expr(E::EString::init(prop_name_literal), key_range.loc);
// Parse the value
let value: Expr = if p.lexer.token != T::TEquals {
// Implicitly true value
// <button selected>
p.new_expr(
E::Boolean { value: true },
bun_ast::Loc {
start: key_range.loc.start + key_range.len,
},
)
} else {
can_be_inlined = false;
p.parse_jsx_prop_value_identifier(
&mut previous_string_with_backslash_loc,
)?
};
props.push(G::Property {
key: Some(prop_name),
value: Some(value),
..Default::default()
});
i += 1; // defer i += 1
}
T::TOpenBrace => {
// PORT NOTE: `defer i += 1` inlined at end of this arm.
// Use Next() not ExpectInsideJSXElement() so we can parse "..."
p.lexer.next()?;
match p.lexer.token {
T::TDotDotDot => {
p.lexer.next()?;
can_be_inlined = false;
if first_spread_prop_i == -1 {
first_spread_prop_i = i;
}
spread_loc = p.lexer.loc();
let value = p.parse_expr(Level::Comma)?;
props.push(G::Property {
value: Some(value),
kind: G::PropertyKind::Spread,
..Default::default()
});
}
// This implements
// <div {foo} />
// ->
// <div foo={foo} />
T::TIdentifier => {
can_be_inlined = false;
// we need to figure out what the key they mean is
// to do that, we must determine the key name
let expr = p.parse_expr(Level::Lowest)?;
let key = 'brk: {
match &expr.data {
ExprData::EImportIdentifier(ident) => {
break 'brk p.new_expr(
E::EString::init(p.load_name_from_ref(ident.ref_)),
expr.loc,
);
}
ExprData::ECommonjsExportIdentifier(ident) => {
break 'brk p.new_expr(
E::EString::init(p.load_name_from_ref(ident.ref_)),
expr.loc,
);
}
ExprData::EIdentifier(ident) => {
break 'brk p.new_expr(
E::EString::init(p.load_name_from_ref(ident.ref_)),
expr.loc,
);
}
ExprData::EDot(dot) => {
break 'brk p.new_expr(
E::EString::init(&dot.name),
dot.name_loc,
);
}
ExprData::EIndex(index) => {
if matches!(index.index.data, ExprData::EString(_)) {
break 'brk index.index;
}
}
_ => {}
}
// If we get here, it's invalid
p.log().add_error(
Some(p.source),
expr.loc,
b"Invalid JSX prop shorthand, must be identifier, dot or string",
);
return Err(err!("SyntaxError"));
};
props.push(G::Property {
value: Some(expr),
key: Some(key),
kind: G::PropertyKind::Normal,
..Default::default()
});
}
// This implements
// <div {"foo"} />
// <div {'foo'} />
// ->
// <div foo="foo" />
// note: template literals are not supported, operations on strings are not supported either
T::TStringLiteral => {
let key_loc = p.lexer.loc();
let key_str = p.lexer.to_e_string()?;
let key = p.new_expr(key_str, key_loc);
p.lexer.next()?;
props.push(G::Property {
value: Some(key),
key: Some(key),
kind: G::PropertyKind::Normal,
..Default::default()
});
}
_ => p.lexer.unexpected()?,
}
p.lexer.next_inside_jsx_element()?;
i += 1; // defer i += 1
}
_ => {
break 'parse_attributes;
}
}
}
let is_key_after_spread =
key_prop_i > -1 && first_spread_prop_i > -1 && key_prop_i > first_spread_prop_i;
if is_key_after_spread {
flags.insert(flags::JSXElement::IsKeyAfterSpread);
}
properties = G::PropertyList::move_from_list(props);
if is_key_after_spread
&& p.options.jsx.runtime == options::JSXRuntime::Automatic
&& !p.has_classic_runtime_warned
{
p.log().add_warning(
Some(p.source),
spread_loc,
b"\"key\" prop after a {...spread} is deprecated in JSX. Falling back to classic runtime.",
);
p.has_classic_runtime_warned = true;
}
}
// People sometimes try to use the output of "JSON.stringify()" as a JSX
// attribute when automatically-generating JSX code. Doing so is incorrect
// because JSX strings work like XML instead of like JS (since JSX is XML-in-
// JS). Specifically, using a backslash before a quote does not cause it to
// be escaped:
//
// JSX ends the "content" attribute here and sets "content" to 'some so-called \\'
// v
// <Button content="some so-called \"button text\"" />
// ^
// There is no "=" after the JSX attribute "text", so we expect a ">"
//
// This code special-cases this error to provide a less obscure error message.
if p.lexer.token == T::TSyntaxError
&& p.lexer.raw() == b"\\"
&& previous_string_with_backslash_loc.start > 0
{
let r = p.lexer.range();
// Not dealing with this right now.
p.log().add_range_error(
Some(p.source),
r,
b"Invalid JSX escape - use XML entity codes quotes or pass a JavaScript string instead",
);
return Err(err!("SyntaxError"));
}
// A slash here is a self-closing element
if p.lexer.token == T::TSlash {
let close_tag_loc = p.lexer.loc();
// Use NextInsideJSXElement() not Next() so we can parse ">>" as ">"
p.lexer.next_inside_jsx_element()?;
if p.lexer.token != T::TGreaterThan {
p.lexer.expected(T::TGreaterThan)?;
}
return Ok(p.new_expr(
E::JSXElement {
tag: start_tag,
properties,
key_prop_index: key_prop_i,
flags,
close_tag_loc,
..Default::default()
},
loc,
));
}
// Use ExpectJSXElementChild() so we parse child strings
p.lexer.expect_jsx_element_child(T::TGreaterThan)?;
let mut children: Vec<Expr> = Vec::new();
// var last_element_i: usize = 0;
loop {
match p.lexer.token {
T::TStringLiteral => {
let e_string = p.lexer.to_e_string()?;
children.push(p.new_expr(e_string, loc));
p.lexer.next_jsx_element_child()?;
}
T::TOpenBrace => {
// Use Next() instead of NextJSXElementChild() here since the next token is an expression
p.lexer.next()?;
let is_spread = p.lexer.token == T::TDotDotDot;
if is_spread {
p.lexer.next()?;
}
// The expression is optional, and may be absent
if p.lexer.token != T::TCloseBrace {
if can_be_inlined {
can_be_inlined = false;
}
let mut item = p.parse_expr(Level::Lowest)?;
if is_spread {
item = p.new_expr(E::Spread { value: item }, loc);
}
children.push(item);
}
// Use ExpectJSXElementChild() so we parse child strings
p.lexer.expect_jsx_element_child(T::TCloseBrace)?;
}
T::TLessThan => {
let less_than_loc = p.lexer.loc();
p.lexer.next_inside_jsx_element()?;
if p.lexer.token != T::TSlash {
// This is a child element
let child = Self::parse_jsx_element(p, less_than_loc)?;
children.push(child);
// PERF(port): was `catch unreachable` on append — Vec::push is infallible
// The call to parseJSXElement() above doesn't consume the last
// TGreaterThan because the caller knows what Next() function to call.
// Use NextJSXElementChild() here since the next token is an element
// child.
p.lexer.next_jsx_element_child()?;
continue;
}
// This is the closing element
p.lexer.next_inside_jsx_element()?;
let end_tag = JSXTag::parse(p)?;
if end_tag.name != tag.name {
// TODO(port): arena param dropped from Zig signature.
p.log().add_range_error_fmt_with_note(
Some(p.source),
end_tag.range,
format_args!(
"Expected closing JSX tag to match opening tag \"\\<{}\\>\"",
bstr::BStr::new(tag.name)
),
format_args!("Opening tag here:"),
tag.range,
);
return Err(err!("SyntaxError"));
}
if p.lexer.token != T::TGreaterThan {
p.lexer.expected(T::TGreaterThan)?;
}
return Ok(p.new_expr(
E::JSXElement {
tag: end_tag.data.as_expr(),
children: ExprNodeList::move_from_list(children),
properties,
key_prop_index: key_prop_i,
flags,
close_tag_loc: end_tag.range.loc,
..Default::default()
},
loc,
));
}
_ => {
p.lexer.unexpected()?;
return Err(err!("SyntaxError"));
}
}
}
}
}
// ported from: src/js_parser/ast/parseJSXElement.zig