Skip to content

Commit 6a57198

Browse files
committed
feat(minifier): allow compressing computed constructor/prototype keys precisely (#9594)
1 parent 0ab76bf commit 6a57198

File tree

2 files changed

+158
-56
lines changed

2 files changed

+158
-56
lines changed

crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs

Lines changed: 108 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,12 @@ impl<'a> PeepholeOptimizations {
7878
prop: &mut MethodDefinition<'a>,
7979
ctx: Ctx<'a, '_>,
8080
) {
81+
let property_key_parent: ClassPropertyKeyParent = prop.into();
82+
if let PropertyKey::StringLiteral(str) = &prop.key {
83+
if property_key_parent.should_keep_as_computed_property(&str.value) {
84+
return;
85+
}
86+
}
8187
self.try_compress_property_key(&mut prop.key, &mut prop.computed, ctx);
8288
}
8389

@@ -86,6 +92,12 @@ impl<'a> PeepholeOptimizations {
8692
prop: &mut PropertyDefinition<'a>,
8793
ctx: Ctx<'a, '_>,
8894
) {
95+
let property_key_parent: ClassPropertyKeyParent = prop.into();
96+
if let PropertyKey::StringLiteral(str) = &prop.key {
97+
if property_key_parent.should_keep_as_computed_property(&str.value) {
98+
return;
99+
}
100+
}
89101
self.try_compress_property_key(&mut prop.key, &mut prop.computed, ctx);
90102
}
91103

@@ -94,6 +106,12 @@ impl<'a> PeepholeOptimizations {
94106
prop: &mut AccessorProperty<'a>,
95107
ctx: Ctx<'a, '_>,
96108
) {
109+
let property_key_parent: ClassPropertyKeyParent = prop.into();
110+
if let PropertyKey::StringLiteral(str) = &prop.key {
111+
if property_key_parent.should_keep_as_computed_property(&str.value) {
112+
return;
113+
}
114+
}
97115
self.try_compress_property_key(&mut prop.key, &mut prop.computed, ctx);
98116
}
99117

@@ -847,9 +865,7 @@ impl<'a> PeepholeOptimizations {
847865
};
848866
let PropertyKey::StringLiteral(s) = key else { return };
849867
let value = s.value.as_str();
850-
// Uncaught SyntaxError: Classes may not have a field named 'constructor'
851-
// Uncaught SyntaxError: Class constructor may not be a private method
852-
if matches!(value, "__proto__" | "prototype" | "constructor" | "#constructor") {
868+
if value == "__proto__" {
853869
return;
854870
}
855871
if is_identifier_name(value) {
@@ -1107,6 +1123,68 @@ impl<'a> LatePeepholeOptimizations {
11071123
}
11081124
}
11091125

1126+
struct ClassPropertyKeyParent {
1127+
pub ty: ClassPropertyKeyParentType,
1128+
/// Whether the property is static.
1129+
pub r#static: bool,
1130+
}
1131+
1132+
impl ClassPropertyKeyParent {
1133+
/// Whether the key should be kept as a computed property to avoid early errors.
1134+
///
1135+
/// <https://tc39.es/ecma262/2024/multipage/ecmascript-language-functions-and-classes.html#sec-static-semantics-classelementkind>
1136+
/// <https://tc39.es/ecma262/2024/multipage/ecmascript-language-functions-and-classes.html#sec-class-definitions-static-semantics-early-errors>
1137+
/// <https://arai-a.github.io/ecma262-compare/?pr=2417&id=sec-class-definitions-static-semantics-early-errors>
1138+
fn should_keep_as_computed_property(&self, key: &str) -> bool {
1139+
match key {
1140+
"prototype" => self.r#static,
1141+
"constructor" => match self.ty {
1142+
// Uncaught SyntaxError: Class constructor may not be an accessor
1143+
ClassPropertyKeyParentType::MethodDefinition => !self.r#static,
1144+
// Uncaught SyntaxError: Classes may not have a field named 'constructor'
1145+
// Uncaught SyntaxError: Class constructor may not be a private method
1146+
ClassPropertyKeyParentType::AccessorProperty
1147+
| ClassPropertyKeyParentType::PropertyDefinition => true,
1148+
},
1149+
"#constructor" => true,
1150+
_ => false,
1151+
}
1152+
}
1153+
}
1154+
1155+
enum ClassPropertyKeyParentType {
1156+
PropertyDefinition,
1157+
AccessorProperty,
1158+
MethodDefinition,
1159+
}
1160+
1161+
impl From<&PropertyDefinition<'_>> for ClassPropertyKeyParent {
1162+
fn from(prop: &PropertyDefinition<'_>) -> Self {
1163+
Self { ty: ClassPropertyKeyParentType::PropertyDefinition, r#static: prop.r#static }
1164+
}
1165+
}
1166+
1167+
impl From<&AccessorProperty<'_>> for ClassPropertyKeyParent {
1168+
fn from(accessor: &AccessorProperty<'_>) -> Self {
1169+
Self { ty: ClassPropertyKeyParentType::AccessorProperty, r#static: accessor.r#static }
1170+
}
1171+
}
1172+
1173+
impl From<&MethodDefinition<'_>> for ClassPropertyKeyParent {
1174+
fn from(method: &MethodDefinition<'_>) -> Self {
1175+
Self { ty: ClassPropertyKeyParentType::MethodDefinition, r#static: method.r#static }
1176+
}
1177+
}
1178+
1179+
impl<T> From<&mut T> for ClassPropertyKeyParent
1180+
where
1181+
ClassPropertyKeyParent: for<'a> std::convert::From<&'a T>,
1182+
{
1183+
fn from(prop: &mut T) -> Self {
1184+
(&*prop).into()
1185+
}
1186+
}
1187+
11101188
/// Port from <https://github.com/google/closure-compiler/blob/v20240609/test/com/google/javascript/jscomp/PeepholeSubstituteAlternateSyntaxTest.java>
11111189
#[cfg(test)]
11121190
mod test {
@@ -1607,10 +1685,34 @@ mod test {
16071685
);
16081686

16091687
test("class C { ['-1']() {} }", "class C { '-1'() {} }");
1610-
test_same("class C { ['prototype']() {} }");
16111688
test_same("class C { ['__proto__']() {} }");
1612-
test_same("class C { ['constructor']() {} }");
1613-
test_same("class C { ['#constructor']() {} }");
1689+
1690+
// <https://tc39.es/ecma262/2024/multipage/ecmascript-language-functions-and-classes.html#sec-static-semantics-classelementkind>
1691+
// <https://tc39.es/ecma262/2024/multipage/ecmascript-language-functions-and-classes.html#sec-class-definitions-static-semantics-early-errors>
1692+
// <https://arai-a.github.io/ecma262-compare/?pr=2417&id=sec-class-definitions-static-semantics-early-errors>
1693+
test_same("class C { static ['prototype']() {} }"); // class C { static prototype() {} } is an early error
1694+
test_same("class C { static ['prototype'] = 0 }"); // class C { prototype = 0 } is an early error
1695+
test_same("class C { static accessor ['prototype'] = 0 }"); // class C { accessor prototype = 0 } is an early error
1696+
test("class C { ['prototype']() {} }", "class C { prototype() {} }");
1697+
test("class C { ['prototype'] = 0 }", "class C { prototype = 0 }");
1698+
test("class C { accessor ['prototype'] = 0 }", "class C { accessor prototype = 0 }");
1699+
test_same("class C { ['constructor'] = 0 }"); // class C { constructor = 0 } is an early error
1700+
test_same("class C { accessor ['constructor'] = 0 }"); // class C { accessor constructor = 0 } is an early error
1701+
test_same("class C { static ['constructor'] = 0 }"); // class C { static constructor = 0 } is an early error
1702+
test_same("class C { static accessor ['constructor'] = 0 }"); // class C { static accessor constructor = 0 } is an early error
1703+
test_same("class C { ['constructor']() {} }"); // computed `constructor` is not treated as a constructor
1704+
test_same("class C { *['constructor']() {} }"); // class C { *constructor() {} } is an early error
1705+
test_same("class C { async ['constructor']() {} }"); // class C { async constructor() {} } is an early error
1706+
test_same("class C { async *['constructor']() {} }"); // class C { async *constructor() {} } is an early error
1707+
test_same("class C { get ['constructor']() {} }"); // class C { get constructor() {} } is an early error
1708+
test_same("class C { set ['constructor'](v) {} }"); // class C { set constructor(v) {} } is an early error
1709+
test("class C { static ['constructor']() {} }", "class C { static constructor() {} }");
1710+
test_same("class C { ['#constructor'] = 0 }"); // class C { #constructor = 0 } is an early error
1711+
test_same("class C { accessor ['#constructor'] = 0 }"); // class C { accessor #constructor = 0 } is an early error
1712+
test_same("class C { ['#constructor']() {} }"); // class C { #constructor() {} } is an early error
1713+
test_same("class C { static ['#constructor'] = 0 }"); // class C { static #constructor = 0 } is an early error
1714+
test_same("class C { static accessor ['#constructor'] = 0 }"); // class C { static accessor #constructor = 0 } is an early error
1715+
test_same("class C { static ['#constructor']() {} }"); // class C { static #constructor() {} } is an early error
16141716
}
16151717

16161718
#[test]

crates/oxc_minifier/tests/peephole/esbuild.rs

Lines changed: 50 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -70,56 +70,56 @@ fn js_parser_test() {
7070
"function f() { x() } var f; function f() { y() }",
7171
"function f() { x();}var f;function f() { y();}",
7272
);
73-
// test("class Foo { ['constructor'] = 0 }", "class Foo { ['constructor'] = 0;}");
74-
// test("class Foo { ['constructor']() {} }", "class Foo { ['constructor']() { }}");
75-
// test("class Foo { *['constructor']() {} }", "class Foo { *['constructor']() { }}");
76-
// test("class Foo { get ['constructor']() {} }", "class Foo { get ['constructor']() { }}");
77-
// test("class Foo { set ['constructor'](x) {} }", "class Foo { set ['constructor'](x) { }}");
78-
// test("class Foo { async ['constructor']() {} }", "class Foo { async ['constructor']() { }}");
79-
// test("class Foo { static ['constructor'] = 0 }", "class Foo { static ['constructor'] = 0;}");
80-
// test("class Foo { static ['constructor']() {} }", "class Foo { static constructor() { }}");
81-
// test("class Foo { static *['constructor']() {} }", "class Foo { static *constructor() { }}");
82-
// test(
83-
// "class Foo { static get ['constructor']() {} }",
84-
// "class Foo { static get constructor() { }}",
85-
// );
86-
// test(
87-
// "class Foo { static set ['constructor'](x) {} }",
88-
// "class Foo { static set constructor(x) { }}",
89-
// );
90-
// test(
91-
// "class Foo { static async ['constructor']() {} }",
92-
// "class Foo { static async constructor() { }}",
93-
// );
94-
// test("class Foo { ['prototype'] = 0 }", "class Foo { prototype = 0;}");
95-
// test("class Foo { ['prototype']() {} }", "class Foo { prototype() { }}");
96-
// test("class Foo { *['prototype']() {} }", "class Foo { *prototype() { }}");
97-
// test("class Foo { get ['prototype']() {} }", "class Foo { get prototype() { }}");
98-
// test("class Foo { set ['prototype'](x) {} }", "class Foo { set prototype(x) { }}");
99-
// test("class Foo { async ['prototype']() {} }", "class Foo { async prototype() { }}");
100-
// test("class Foo { static ['prototype'] = 0 }", "class Foo { static ['prototype'] = 0;}");
101-
// test("class Foo { static ['prototype']() {} }", "class Foo { static ['prototype']() { }}");
102-
// test("class Foo { static *['prototype']() {} }", "class Foo { static *['prototype']() { }}");
103-
// test(
104-
// "class Foo { static get ['prototype']() {} }",
105-
// "class Foo { static get ['prototype']() { }}",
106-
// );
107-
// test(
108-
// "class Foo { static set ['prototype'](x) {} }",
109-
// "class Foo { static set ['prototype'](x) { }}",
110-
// );
111-
// test(
112-
// "class Foo { static async ['prototype']() {} }",
113-
// "class Foo { static async ['prototype']() { }}",
114-
// );
115-
// test(
116-
// "class Foo { constructor() {} ['constructor']() {} }",
117-
// "class Foo { constructor() { } ['constructor']() { }}",
118-
// );
119-
// test(
120-
// "class Foo { static constructor() {} static ['constructor']() {} }",
121-
// "class Foo { static constructor() { } static constructor() { }}",
122-
// );
73+
test("class Foo { ['constructor'] = 0 }", "class Foo { ['constructor'] = 0;}");
74+
test("class Foo { ['constructor']() {} }", "class Foo { ['constructor']() { }}");
75+
test("class Foo { *['constructor']() {} }", "class Foo { *['constructor']() { }}");
76+
test("class Foo { get ['constructor']() {} }", "class Foo { get ['constructor']() { }}");
77+
test("class Foo { set ['constructor'](x) {} }", "class Foo { set ['constructor'](x) { }}");
78+
test("class Foo { async ['constructor']() {} }", "class Foo { async ['constructor']() { }}");
79+
test("class Foo { static ['constructor'] = 0 }", "class Foo { static ['constructor'] = 0;}");
80+
test("class Foo { static ['constructor']() {} }", "class Foo { static constructor() { }}");
81+
test("class Foo { static *['constructor']() {} }", "class Foo { static *constructor() { }}");
82+
test(
83+
"class Foo { static get ['constructor']() {} }",
84+
"class Foo { static get constructor() { }}",
85+
);
86+
test(
87+
"class Foo { static set ['constructor'](x) {} }",
88+
"class Foo { static set constructor(x) { }}",
89+
);
90+
test(
91+
"class Foo { static async ['constructor']() {} }",
92+
"class Foo { static async constructor() { }}",
93+
);
94+
test("class Foo { ['prototype'] = 0 }", "class Foo { prototype = 0;}");
95+
test("class Foo { ['prototype']() {} }", "class Foo { prototype() { }}");
96+
test("class Foo { *['prototype']() {} }", "class Foo { *prototype() { }}");
97+
test("class Foo { get ['prototype']() {} }", "class Foo { get prototype() { }}");
98+
test("class Foo { set ['prototype'](x) {} }", "class Foo { set prototype(x) { }}");
99+
test("class Foo { async ['prototype']() {} }", "class Foo { async prototype() { }}");
100+
test("class Foo { static ['prototype'] = 0 }", "class Foo { static ['prototype'] = 0;}");
101+
test("class Foo { static ['prototype']() {} }", "class Foo { static ['prototype']() { }}");
102+
test("class Foo { static *['prototype']() {} }", "class Foo { static *['prototype']() { }}");
103+
test(
104+
"class Foo { static get ['prototype']() {} }",
105+
"class Foo { static get ['prototype']() { }}",
106+
);
107+
test(
108+
"class Foo { static set ['prototype'](x) {} }",
109+
"class Foo { static set ['prototype'](x) { }}",
110+
);
111+
test(
112+
"class Foo { static async ['prototype']() {} }",
113+
"class Foo { static async ['prototype']() { }}",
114+
);
115+
test(
116+
"class Foo { constructor() {} ['constructor']() {} }",
117+
"class Foo { constructor() { } ['constructor']() { }}",
118+
);
119+
test(
120+
"class Foo { static constructor() {} static ['constructor']() {} }",
121+
"class Foo { static constructor() { } static constructor() { }}",
122+
);
123123
test("class x { '0' = y }", "class x { 0 = y;}");
124124
test("class x { '123' = y }", "class x { 123 = y;}");
125125
test("class x { ['-123'] = y }", "class x { '-123' = y;}");

0 commit comments

Comments
 (0)