Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 40 additions & 3 deletions src/js_printer/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1242,6 +1242,7 @@ pub enum ExprFlag {
HasNonOptionalChainParent,
ExprResultIsUnused,
IsFollowedByOf,
IsDeleteTarget,
}

pub(crate) type ExprFlagSet = enumset::EnumSet<ExprFlag>;
Expand Down Expand Up @@ -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() {
Expand All @@ -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 {
Expand Down Expand Up @@ -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);
Expand All @@ -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;
}
}
Expand Down Expand Up @@ -4008,7 +4018,17 @@ 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,
)
{
// Gate on the parse-time flag so this never reaches compound
// operands (EIf/EBinary) that forward `flags` to non-target children.
Comment thread
robobun marked this conversation as resolved.
Outdated
value_flags.insert(ExprFlag::IsDeleteTarget);
}
self.print_expr(e.value, Level::Prefix.sub(1), value_flags);
}
}

Expand Down Expand Up @@ -6159,7 +6179,20 @@ 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; wrap so the
// inlined value stays a value, not a Reference.
Comment thread
robobun marked this conversation as resolved.
Outdated
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
Expand All @@ -6185,6 +6218,10 @@ pub(crate) mod __gated_printer {
self.print(b" */");
}
}

if wrap {
self.print(b")");
}
}

pub(crate) fn print_decl_stmt(
Expand Down
63 changes: 63 additions & 0 deletions test/bundler/esbuild/ts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2236,6 +2236,69 @@ describe("bundler", () => {
]);
},
});
// Cross-module enum inlining happens at print time after
// WAS_ORIGINALLY_DELETE_OF_IDENTIFIER_OR_PROPERTY_ACCESS has already bypassed the
// `(0, ...)` re-wrap in the EUnary print arm. When the inlined value prints as `NaN`
// or `Infinity` that yields `delete NaN`, a strict-mode SyntaxError in the ESM bundle.
// Finite numbers and strings are fine (`delete 42` / `delete "s"` are valid and true).
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
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 */ `
Expand Down
Loading