Skip to content

Commit fe82c4c

Browse files
authored
fix(es/parser): Rescan >= for JSX closing tag (#10693)
**Related issue:** - Closes #10692
1 parent a066b76 commit fe82c4c

File tree

18 files changed

+1002
-14
lines changed

18 files changed

+1002
-14
lines changed

.changeset/funny-kangaroos-sort.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
swc_ecma_lexer: patch
3+
swc_core: patch
4+
swc_ecma_parser: patch
5+
---
6+
7+
fix(es/parser): Rescan `>=` for JSX closing tag

crates/swc_ecma_parser/src/lexer/token.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1469,6 +1469,17 @@ impl Token {
14691469
_ => false,
14701470
}
14711471
}
1472+
1473+
pub(crate) fn should_rescan_into_gt_in_jsx(self) -> bool {
1474+
matches!(
1475+
self,
1476+
Token::GtEq
1477+
| Token::RShift
1478+
| Token::RShiftEq
1479+
| Token::ZeroFillRShift
1480+
| Token::ZeroFillRShiftEq
1481+
)
1482+
}
14721483
}
14731484

14741485
#[derive(Clone, Copy, Debug)]

crates/swc_ecma_parser/src/parser/expr.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,9 @@ impl<I: Tokens> Parser<I> {
5353
.map(Box::new)
5454
};
5555
} else if self.input().syntax().jsx()
56-
&& self
57-
.input_mut()
58-
.peek()
59-
.is_some_and(|peek| (*peek).is_word() || peek == &Token::Gt)
56+
&& self.input_mut().peek().is_some_and(|peek| {
57+
(*peek).is_word() || peek == &Token::Gt || peek.should_rescan_into_gt_in_jsx()
58+
})
6059
{
6160
fn into_expr(e: Either<JSXFragment, JSXElement>) -> Box<Expr> {
6261
match e {

crates/swc_ecma_parser/src/parser/input.rs

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -119,23 +119,20 @@ impl<I: Tokens> Buffer<I> {
119119
}
120120

121121
pub fn rescan_jsx_open_el_terminal_token(&mut self) {
122+
if !self
123+
.cur()
124+
.is_some_and(|token| token.should_rescan_into_gt_in_jsx())
125+
{
126+
return;
127+
}
122128
// rescan `>=`, `>>`, `>>=`, `>>>`, `>>>=` into `>`
123129
let cur = match self.cur.as_ref() {
124130
Some(cur) => cur,
125131
None => {
126132
return self.scan_jsx_open_el_terminal_token();
127133
}
128134
};
129-
if !matches!(
130-
cur.token,
131-
Token::GtEq
132-
| Token::RShift
133-
| Token::RShiftEq
134-
| Token::ZeroFillRShift
135-
| Token::ZeroFillRShiftEq
136-
) {
137-
return;
138-
}
135+
139136
let start = cur.span.lo;
140137
if let Some(t) = self.iter_mut().rescan_jsx_open_el_terminal_token(start) {
141138
self.set_cur(t);

crates/swc_ecma_parser/src/parser/jsx/mod.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,13 @@ impl<I: Tokens> Parser<I> {
107107
let start = self.cur_pos();
108108
self.expect(&Token::LessSlash)?;
109109
let tagname = self.parse_jsx_element_name()?;
110+
111+
// Handle JSX closing tag followed by '=': '</tag>='
112+
// When lexer sees '>=' it combines into GtEq, but JSX only needs '>'
113+
// Use rescan_jsx_open_el_terminal_token to split >= back into >
114+
self.input_mut().rescan_jsx_open_el_terminal_token();
110115
self.expect_without_advance(&Token::Gt)?;
116+
111117
if in_expr_context {
112118
self.bump();
113119
} else {
@@ -138,7 +144,13 @@ impl<I: Tokens> Parser<I> {
138144
fn parse_jsx_closing_fragment(&mut self, in_expr_context: bool) -> PResult<JSXClosingFragment> {
139145
let start = self.cur_pos();
140146
self.expect(&Token::LessSlash)?;
147+
148+
// Handle JSX closing fragment followed by '=': '</>=
149+
// When lexer sees '>=' it combines into GtEq, but JSX only needs '>'
150+
// Use rescan_jsx_open_el_terminal_token to split >= back into >
151+
self.input_mut().rescan_jsx_open_el_terminal_token();
141152
self.expect_without_advance(&Token::Gt)?;
153+
142154
if in_expr_context {
143155
self.bump();
144156
} else {
@@ -307,6 +319,12 @@ impl<I: Tokens> Parser<I> {
307319
let ctx = self.ctx() & !Context::ShouldNotLexLtOrGtAsType;
308320
self.with_ctx(ctx).parse_with(|p| {
309321
p.expect(&Token::Lt)?;
322+
323+
// Handle JSX fragment opening followed by '=': '<>='
324+
// When lexer sees '>=' it combines into GtEq, but JSX fragment only needs '>'
325+
// Use rescan_jsx_open_el_terminal_token to split >= back into >
326+
p.input_mut().rescan_jsx_open_el_terminal_token();
327+
310328
if p.input_mut().cur().is_some_and(|cur| cur == &Token::Gt) {
311329
// <>xxxxxx</>
312330
p.input_mut().scan_jsx_token(true);
@@ -369,7 +387,13 @@ impl<I: Tokens> Parser<I> {
369387
} else {
370388
// <xxxxx/>
371389
p.expect(&Token::Slash)?;
390+
391+
// Handle JSX self-closing tag followed by '=': '<tag/>='
392+
// When lexer sees '>=' it combines into GtEq, but JSX only needs '>'
393+
// Use rescan_jsx_open_el_terminal_token to split >= back into >
394+
p.input_mut().rescan_jsx_open_el_terminal_token();
372395
p.expect_without_advance(&Token::Gt)?;
396+
373397
if in_expr_context {
374398
p.bump();
375399
} else {

crates/swc_ecma_parser/tests/jsx.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ where
4646
}
4747

4848
#[testing::fixture("tests/jsx/basic/**/*.js")]
49+
#[testing::fixture("tests/jsx/basic/**/*.jsx")]
4950
fn references(entry: PathBuf) {
5051
run_test(false, |cm, handler| {
5152
let input = read_to_string(&entry).unwrap();
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<>
2+
<span>x</span>=
3+
</>;
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
{
2+
"type": "Module",
3+
"span": {
4+
"start": 1,
5+
"end": 28
6+
},
7+
"body": [
8+
{
9+
"type": "ExpressionStatement",
10+
"span": {
11+
"start": 1,
12+
"end": 28
13+
},
14+
"expression": {
15+
"type": "JSXFragment",
16+
"span": {
17+
"start": 1,
18+
"end": 27
19+
},
20+
"opening": {
21+
"type": "JSXOpeningFragment",
22+
"span": {
23+
"start": 1,
24+
"end": 3
25+
}
26+
},
27+
"children": [
28+
{
29+
"type": "JSXText",
30+
"span": {
31+
"start": 3,
32+
"end": 8
33+
},
34+
"value": "\n ",
35+
"raw": "\n "
36+
},
37+
{
38+
"type": "JSXElement",
39+
"span": {
40+
"start": 8,
41+
"end": 22
42+
},
43+
"opening": {
44+
"type": "JSXOpeningElement",
45+
"name": {
46+
"type": "Identifier",
47+
"span": {
48+
"start": 9,
49+
"end": 13
50+
},
51+
"ctxt": 0,
52+
"value": "span",
53+
"optional": false
54+
},
55+
"span": {
56+
"start": 8,
57+
"end": 14
58+
},
59+
"attributes": [],
60+
"selfClosing": false,
61+
"typeArguments": null
62+
},
63+
"children": [
64+
{
65+
"type": "JSXText",
66+
"span": {
67+
"start": 14,
68+
"end": 15
69+
},
70+
"value": "x",
71+
"raw": "x"
72+
}
73+
],
74+
"closing": {
75+
"type": "JSXClosingElement",
76+
"span": {
77+
"start": 15,
78+
"end": 22
79+
},
80+
"name": {
81+
"type": "Identifier",
82+
"span": {
83+
"start": 17,
84+
"end": 21
85+
},
86+
"ctxt": 0,
87+
"value": "span",
88+
"optional": false
89+
}
90+
}
91+
},
92+
{
93+
"type": "JSXText",
94+
"span": {
95+
"start": 22,
96+
"end": 24
97+
},
98+
"value": "=\n",
99+
"raw": "=\n"
100+
}
101+
],
102+
"closing": {
103+
"type": "JSXClosingFragment",
104+
"span": {
105+
"start": 24,
106+
"end": 27
107+
}
108+
}
109+
}
110+
}
111+
],
112+
"interpreter": null
113+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<>
2+
<span>=x</span>
3+
</>;
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
{
2+
"type": "Module",
3+
"span": {
4+
"start": 1,
5+
"end": 28
6+
},
7+
"body": [
8+
{
9+
"type": "ExpressionStatement",
10+
"span": {
11+
"start": 1,
12+
"end": 28
13+
},
14+
"expression": {
15+
"type": "JSXFragment",
16+
"span": {
17+
"start": 1,
18+
"end": 27
19+
},
20+
"opening": {
21+
"type": "JSXOpeningFragment",
22+
"span": {
23+
"start": 1,
24+
"end": 3
25+
}
26+
},
27+
"children": [
28+
{
29+
"type": "JSXText",
30+
"span": {
31+
"start": 3,
32+
"end": 8
33+
},
34+
"value": "\n ",
35+
"raw": "\n "
36+
},
37+
{
38+
"type": "JSXElement",
39+
"span": {
40+
"start": 8,
41+
"end": 23
42+
},
43+
"opening": {
44+
"type": "JSXOpeningElement",
45+
"name": {
46+
"type": "Identifier",
47+
"span": {
48+
"start": 9,
49+
"end": 13
50+
},
51+
"ctxt": 0,
52+
"value": "span",
53+
"optional": false
54+
},
55+
"span": {
56+
"start": 8,
57+
"end": 14
58+
},
59+
"attributes": [],
60+
"selfClosing": false,
61+
"typeArguments": null
62+
},
63+
"children": [
64+
{
65+
"type": "JSXText",
66+
"span": {
67+
"start": 14,
68+
"end": 16
69+
},
70+
"value": "=x",
71+
"raw": "=x"
72+
}
73+
],
74+
"closing": {
75+
"type": "JSXClosingElement",
76+
"span": {
77+
"start": 16,
78+
"end": 23
79+
},
80+
"name": {
81+
"type": "Identifier",
82+
"span": {
83+
"start": 18,
84+
"end": 22
85+
},
86+
"ctxt": 0,
87+
"value": "span",
88+
"optional": false
89+
}
90+
}
91+
},
92+
{
93+
"type": "JSXText",
94+
"span": {
95+
"start": 23,
96+
"end": 24
97+
},
98+
"value": "\n",
99+
"raw": "\n"
100+
}
101+
],
102+
"closing": {
103+
"type": "JSXClosingFragment",
104+
"span": {
105+
"start": 24,
106+
"end": 27
107+
}
108+
}
109+
}
110+
}
111+
],
112+
"interpreter": null
113+
}

0 commit comments

Comments
 (0)