diff --git a/src/js_printer/lib.rs b/src/js_printer/lib.rs index ea6bc77a3ae1..ba07040bd4c6 100644 --- a/src/js_printer/lib.rs +++ b/src/js_printer/lib.rs @@ -1242,6 +1242,7 @@ pub enum ExprFlag { HasNonOptionalChainParent, ExprResultIsUnused, IsFollowedByOf, + IsDeleteTarget, } pub(crate) type ExprFlagSet = enumset::EnumSet; @@ -3255,6 +3256,7 @@ pub(crate) mod __gated_printer { } ExprData::EDot(e) => { let is_optional_chain = e.optional_chain == Some(js_ast::OptionalChain::Start); + let is_delete_target = flags.contains(ExprFlag::IsDeleteTarget); let mut wrap = false; if e.optional_chain.is_none() { @@ -3264,7 +3266,7 @@ pub(crate) mod __gated_printer { if let Some(inlined) = self.try_to_get_imported_enum_value(e.target, &e.name) { - self.print_inlined_enum(inlined, &e.name, level); + self.print_inlined_enum(inlined, &e.name, level, is_delete_target); return; } } else { @@ -3306,6 +3308,9 @@ pub(crate) mod __gated_printer { } } ExprData::EIndex(e) => { + let is_delete_target = flags.contains(ExprFlag::IsDeleteTarget); + flags.remove(ExprFlag::IsDeleteTarget); + let mut wrap = false; if e.optional_chain.is_none() { flags.insert(ExprFlag::HasNonOptionalChainParent); @@ -3316,7 +3321,12 @@ pub(crate) mod __gated_printer { if let Some(value) = self.try_to_get_imported_enum_value(e.target, str.slice8()) { - self.print_inlined_enum(value, str.slice8(), level); + self.print_inlined_enum( + value, + str.slice8(), + level, + is_delete_target, + ); return; } } @@ -4008,7 +4018,15 @@ pub(crate) mod __gated_printer { self.print_expr(e.value, Level::Prefix.sub(1), ExprFlag::none()); self.print(b")"); } else { - self.print_expr(e.value, Level::Prefix.sub(1), ExprFlag::none()); + let mut value_flags = ExprFlag::none(); + if e.op == Op::Code::UnDelete + && e.flags.contains( + E::UnaryFlags::WAS_ORIGINALLY_DELETE_OF_IDENTIFIER_OR_PROPERTY_ACCESS, + ) + { + value_flags.insert(ExprFlag::IsDeleteTarget); + } + self.print_expr(e.value, Level::Prefix.sub(1), value_flags); } } @@ -6159,7 +6177,19 @@ pub(crate) mod __gated_printer { inlined: js_ast::InlinedEnumValueDecoded, comment: &[u8], level: Level, + is_delete_target: bool, ) { + // "delete NaN" / "delete Infinity" is a strict-mode SyntaxError + let wrap = is_delete_target + && matches!(inlined, js_ast::InlinedEnumValueDecoded::Number(n) if !n.is_finite()); + let level = if wrap { + self.print(b"(0,"); + self.print_space(); + Level::Comma + } else { + level + }; + match inlined { js_ast::InlinedEnumValueDecoded::Number(num) => self.print_number(num, level), // TODO: extract printString @@ -6185,6 +6215,10 @@ pub(crate) mod __gated_printer { self.print(b" */"); } } + + if wrap { + self.print(b")"); + } } pub(crate) fn print_decl_stmt( diff --git a/test/bundler/esbuild/ts.test.ts b/test/bundler/esbuild/ts.test.ts index d026d000f1dc..90e3faedc1eb 100644 --- a/test/bundler/esbuild/ts.test.ts +++ b/test/bundler/esbuild/ts.test.ts @@ -2236,6 +2236,64 @@ describe("bundler", () => { ]); }, }); + itBundled("ts/EnumCrossModuleInliningDeleteTarget", { + files: { + "/entry.ts": /* ts */ ` + import { E } from './enums' + console.log(JSON.stringify([ + delete E.N, + delete E.I, + delete E.NI, + delete E.F, + delete E.S, + delete E["N"], + delete E["I"], + ])) + `, + "/enums.ts": /* ts */ ` + export enum E { + N = 0 / 0, + I = 1 / 0, + NI = -1 / 0, + F = 42, + S = "s", + } + `, + }, + minifySyntax: false, // intentionally disabled. enum inlining always happens + run: { stdout: "[true,true,true,true,true,true,true]" }, + onAfterBundle(api) { + const out = api.readFile("/out.js"); + // The bundle is an ES module: `delete NaN` / `delete Infinity` would be a + // SyntaxError. Non-finite values must be wrapped; finite/string values need not be. + expect(out).not.toContain("delete NaN"); + expect(out).not.toContain("delete Infinity"); + expect(out).toContain("delete (0, NaN /* N */)"); + expect(out).toContain("delete (0, Infinity /* I */)"); + expect(out).toContain("delete (0, -Infinity /* NI */)"); + expect(out).toContain("delete 42 /* F */"); + expect(out).toContain('delete "s" /* S */'); + }, + }); + itBundled("ts/EnumCrossModuleInliningDeleteTargetMinified", { + files: { + "/entry.ts": /* ts */ ` + import { E } from './enums' + console.log(JSON.stringify([delete E.N, delete E.I, delete E["N"]])) + `, + "/enums.ts": /* ts */ ` + export enum E { N = 0 / 0, I = 1 / 0 } + `, + }, + minifyWhitespace: true, + minifySyntax: true, + run: { stdout: "[true,true,true]" }, + onAfterBundle(api) { + const out = api.readFile("/out.js"); + expect(out).not.toContain("delete NaN"); + expect(out).toContain("delete(0,NaN)"); + }, + }); itBundled("ts/EnumExportClause", { files: { "/entry.ts": /* ts */ `