Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
58 changes: 55 additions & 3 deletions src/js_printer/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1242,6 +1242,11 @@ pub enum ExprFlag {
HasNonOptionalChainParent,
ExprResultIsUnused,
IsFollowedByOf,
/// The expression is the direct operand of a `delete` whose source form was
/// an identifier or property access. A print-time rewrite that would
/// surface a bare identifier here (e.g. cross-module enum inlining to
/// `NaN`/`Infinity`) must keep it a value, not a Reference.
Comment thread
robobun marked this conversation as resolved.
Outdated
IsDeleteTarget,
}

pub(crate) type ExprFlagSet = enumset::EnumSet<ExprFlag>;
Expand Down Expand Up @@ -3255,6 +3260,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 +3270,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 +3312,11 @@ pub(crate) mod __gated_printer {
}
}
ExprData::EIndex(e) => {
let is_delete_target = flags.contains(ExprFlag::IsDeleteTarget);
// The delete target is this index expression itself, never its target
// or index subexpressions.
Comment thread
robobun marked this conversation as resolved.
Outdated
flags.remove(ExprFlag::IsDeleteTarget);

let mut wrap = false;
if e.optional_chain.is_none() {
flags.insert(ExprFlag::HasNonOptionalChainParent);
Expand All @@ -3316,7 +3327,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 +4024,23 @@ 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,
)
{
// The operand was a property access / identifier in source, so
// the `(0, ...)` re-wrap above is skipped. Cross-module enum
// inlining in the EDot/EIndex print arms can still replace that
// access with `NaN`/`Infinity` (a strict-mode `delete <id>`
// SyntaxError). Tell the operand it is the delete target so the
// inline path can wrap itself. Gating on the parse-time flag keeps
// this from reaching compound operands (EIf/EBinary) that forward
// `flags` to children which are not themselves delete targets.
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 +6191,23 @@ pub(crate) mod __gated_printer {
inlined: js_ast::InlinedEnumValueDecoded,
comment: &[u8],
level: Level,
is_delete_target: bool,
) {
// `delete E.N` sets WAS_ORIGINALLY_DELETE_OF_IDENTIFIER_OR_PROPERTY_ACCESS, so the
// EUnary arm does not re-wrap. Inlining a non-finite number here would then print
// `delete NaN` / `delete Infinity`: a strict-mode SyntaxError. Wrap in `(0, ...)`
// so the operand stays a value; `delete <value>` evaluates to `true`, matching the
// source semantics (enum members are configurable).
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 +6233,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