Skip to content

Commit 146c77c

Browse files
authored
fix(es/parser): Support literal computed property names in enums (#11163)
**Related issue:** - Closes #11160
1 parent 5a4088d commit 146c77c

File tree

9 files changed

+304
-6
lines changed

9 files changed

+304
-6
lines changed

.changeset/stale-moons-itch.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
swc_core: patch
3+
swc_ecma_lexer: patch
4+
---
5+
6+
fix(es/parser): Support literal computed property names in enums
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"jsc": {
3+
"parser": {
4+
"syntax": "typescript",
5+
"tsx": false
6+
},
7+
"target": "es5",
8+
"loose": false,
9+
"minify": {
10+
"compress": false,
11+
"mangle": false
12+
}
13+
},
14+
"module": {
15+
"type": "es6"
16+
},
17+
"minify": false,
18+
"isModule": true
19+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
enum CHAR {
2+
["\t"] = 0x09,
3+
["\n"] = 0x0A,
4+
["\v"] = 0x0B,
5+
["\f"] = 0x0C,
6+
["\r"] = 0x0D,
7+
[" "] = 0x20,
8+
9+
["-"] = 0x2D,
10+
["["] = 0x5B
11+
}
12+
13+
const c = "\n"
14+
15+
console.log(c.charCodeAt(0) === CHAR["\n"]);
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
enum CHAR {
2+
foo = 1, // ✅ Regular enum member
3+
4+
'\t' = 0x09, // ✅ String literal
5+
"\n" = 0x0A, // ✅ String literal
6+
7+
['\v'] = 0x0B, // ✅ String literal within brackets
8+
["\f"] = 0x0C, // ✅ String literal within brackets
9+
[`\r`] = 0x0D, // ✅ Template literal (no substitution) within brackets
10+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
var CHAR = /*#__PURE__*/ function(CHAR) {
2+
CHAR[CHAR[" "] = 9] = " ";
3+
CHAR[CHAR["\n"] = 10] = "\n";
4+
CHAR[CHAR["\v"] = 11] = "\v";
5+
CHAR[CHAR["\f"] = 12] = "\f";
6+
CHAR[CHAR["\r"] = 13] = "\r";
7+
CHAR[CHAR[" "] = 32] = " ";
8+
CHAR[CHAR["-"] = 45] = "-";
9+
CHAR[CHAR["["] = 91] = "[";
10+
return CHAR;
11+
}(CHAR || {});
12+
var c = "\n";
13+
console.log(c.charCodeAt(0) === 10);
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
var CHAR = /*#__PURE__*/ function(CHAR) {
2+
CHAR[CHAR["foo"] = 1] = "foo";
3+
CHAR[CHAR[" "] = 9] = " ";
4+
CHAR[CHAR["\n"] = 10] = "\n";
5+
CHAR[CHAR["\v"] = 11] = "\v";
6+
CHAR[CHAR["\f"] = 12] = "\f";
7+
CHAR[CHAR["\r"] = 13] = "\r";
8+
return CHAR;
9+
}(CHAR || {});

crates/swc_ecma_lexer/src/common/parser/typescript.rs

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::fmt::Write;
1+
use std::{fmt::Write, mem};
22

33
use either::Either;
44
use swc_atoms::{atom, Atom};
@@ -946,8 +946,10 @@ fn parse_ts_enum_member<'a, P: Parser<'a>>(p: &mut P) -> PResult<TsEnumMember> {
946946
debug_assert!(p.input().syntax().typescript());
947947

948948
let start = p.cur_pos();
949-
// Computed property names are grammar errors in an enum, so accept just string
950-
// literal or identifier.
949+
// TypeScript allows computed property names with literal expressions in enums.
950+
// Non-literal computed properties (like ["a" + "b"]) are rejected.
951+
// We normalize literal computed properties (["\t"] → "\t") to keep AST simple.
952+
// See https://github.com/swc-project/swc/issues/11160
951953
let cur = p.input().cur();
952954
let id = if cur.is_str() {
953955
TsEnumMemberId::Str(parse_str_lit(p))
@@ -971,10 +973,35 @@ fn parse_ts_enum_member<'a, P: Parser<'a>>(p: &mut P) -> PResult<TsEnumMember> {
971973
})
972974
} else if cur.is_lbracket() {
973975
p.assert_and_bump(&P::Token::LBRACKET);
974-
let _ = p.parse_expr()?;
975-
p.emit_err(p.span(start), SyntaxError::TS1164);
976+
let expr = p.parse_expr()?;
976977
p.assert_and_bump(&P::Token::RBRACKET);
977-
TsEnumMemberId::Ident(Ident::new_no_ctxt(atom!(""), p.span(start)))
978+
let bracket_span = p.span(start);
979+
980+
match *expr {
981+
Expr::Lit(Lit::Str(str_lit)) => {
982+
// String literal: ["\t"] → "\t"
983+
TsEnumMemberId::Str(str_lit)
984+
}
985+
Expr::Tpl(mut tpl) if tpl.exprs.is_empty() => {
986+
// Template literal without substitution: [`hello`] → "hello"
987+
988+
let tpl = mem::take(tpl.quasis.first_mut().unwrap());
989+
990+
let span = tpl.span;
991+
let value = tpl.cooked.unwrap();
992+
993+
TsEnumMemberId::Str(Str {
994+
span,
995+
value,
996+
raw: None,
997+
})
998+
}
999+
_ => {
1000+
// Non-literal expression: report error
1001+
p.emit_err(bracket_span, SyntaxError::TS1164);
1002+
TsEnumMemberId::Ident(Ident::new_no_ctxt(atom!(""), bracket_span))
1003+
}
1004+
}
9781005
} else if cur.is_error() {
9791006
let err = p.input_mut().expect_error_token_and_bump();
9801007
return Err(err);
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export enum CHAR {
2+
foo = 1, // ✅ Regular enum member
3+
4+
'\t' = 0x09, // ✅ String literal
5+
"\n" = 0x0A, // ✅ String literal
6+
7+
['\v'] = 0x0B, // ✅ String literal within brackets
8+
["\f"] = 0x0C, // ✅ String literal within brackets
9+
[`\r`] = 0x0D, // ✅ Template literal (no substitution) within brackets
10+
}
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
{
2+
"type": "Module",
3+
"span": {
4+
"start": 1,
5+
"end": 348
6+
},
7+
"body": [
8+
{
9+
"type": "ExportDeclaration",
10+
"span": {
11+
"start": 1,
12+
"end": 348
13+
},
14+
"declaration": {
15+
"type": "TsEnumDeclaration",
16+
"span": {
17+
"start": 8,
18+
"end": 348
19+
},
20+
"declare": false,
21+
"isConst": false,
22+
"id": {
23+
"type": "Identifier",
24+
"span": {
25+
"start": 13,
26+
"end": 17
27+
},
28+
"ctxt": 0,
29+
"value": "CHAR",
30+
"optional": false
31+
},
32+
"members": [
33+
{
34+
"type": "TsEnumMember",
35+
"span": {
36+
"start": 24,
37+
"end": 31
38+
},
39+
"id": {
40+
"type": "Identifier",
41+
"span": {
42+
"start": 24,
43+
"end": 27
44+
},
45+
"ctxt": 0,
46+
"value": "foo",
47+
"optional": false
48+
},
49+
"init": {
50+
"type": "NumericLiteral",
51+
"span": {
52+
"start": 30,
53+
"end": 31
54+
},
55+
"value": 1.0,
56+
"raw": "1"
57+
}
58+
},
59+
{
60+
"type": "TsEnumMember",
61+
"span": {
62+
"start": 72,
63+
"end": 83
64+
},
65+
"id": {
66+
"type": "StringLiteral",
67+
"span": {
68+
"start": 72,
69+
"end": 76
70+
},
71+
"value": "\t",
72+
"raw": "'\\t'"
73+
},
74+
"init": {
75+
"type": "NumericLiteral",
76+
"span": {
77+
"start": 79,
78+
"end": 83
79+
},
80+
"value": 9.0,
81+
"raw": "0x09"
82+
}
83+
},
84+
{
85+
"type": "TsEnumMember",
86+
"span": {
87+
"start": 114,
88+
"end": 125
89+
},
90+
"id": {
91+
"type": "StringLiteral",
92+
"span": {
93+
"start": 114,
94+
"end": 118
95+
},
96+
"value": "\n",
97+
"raw": "\"\\n\""
98+
},
99+
"init": {
100+
"type": "NumericLiteral",
101+
"span": {
102+
"start": 121,
103+
"end": 125
104+
},
105+
"value": 10.0,
106+
"raw": "0x0A"
107+
}
108+
},
109+
{
110+
"type": "TsEnumMember",
111+
"span": {
112+
"start": 157,
113+
"end": 170
114+
},
115+
"id": {
116+
"type": "StringLiteral",
117+
"span": {
118+
"start": 158,
119+
"end": 162
120+
},
121+
"value": "\u000b",
122+
"raw": "'\\v'"
123+
},
124+
"init": {
125+
"type": "NumericLiteral",
126+
"span": {
127+
"start": 166,
128+
"end": 170
129+
},
130+
"value": 11.0,
131+
"raw": "0x0B"
132+
}
133+
},
134+
{
135+
"type": "TsEnumMember",
136+
"span": {
137+
"start": 215,
138+
"end": 228
139+
},
140+
"id": {
141+
"type": "StringLiteral",
142+
"span": {
143+
"start": 216,
144+
"end": 220
145+
},
146+
"value": "\f",
147+
"raw": "\"\\f\""
148+
},
149+
"init": {
150+
"type": "NumericLiteral",
151+
"span": {
152+
"start": 224,
153+
"end": 228
154+
},
155+
"value": 12.0,
156+
"raw": "0x0C"
157+
}
158+
},
159+
{
160+
"type": "TsEnumMember",
161+
"span": {
162+
"start": 273,
163+
"end": 286
164+
},
165+
"id": {
166+
"type": "StringLiteral",
167+
"span": {
168+
"start": 275,
169+
"end": 277
170+
},
171+
"value": "\r",
172+
"raw": null
173+
},
174+
"init": {
175+
"type": "NumericLiteral",
176+
"span": {
177+
"start": 282,
178+
"end": 286
179+
},
180+
"value": 13.0,
181+
"raw": "0x0D"
182+
}
183+
}
184+
]
185+
}
186+
}
187+
],
188+
"interpreter": null
189+
}

0 commit comments

Comments
 (0)