Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
4 changes: 4 additions & 0 deletions src/js_parser/parse/parse_entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1012,6 +1012,10 @@ impl<'a> Parser<'a> {
}
// An enum above can only see the constants declared before it.
js_ast::StmtData::SLocal(local) => p.record_ts_enum_constants(&local),
// The enums inside are visited with the namespace's part, after this loop.
js_ast::StmtData::SNamespace(namespace) => {
p.compute_enum_values_inside_namespace(namespace.stmts.slice())
}
_ => {}
}
}
Expand Down
4 changes: 4 additions & 0 deletions src/js_parser/visit/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1536,6 +1536,10 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
}
// An enum above can only see the constants declared before it.
StmtData::SLocal(local) => p.record_ts_enum_constants(&local),
// The enums inside are visited with the namespace, after this loop.
StmtData::SNamespace(namespace) => {
p.compute_enum_values_inside_namespace(namespace.stmts.slice())
}
_ => {}
}
}
Expand Down
91 changes: 89 additions & 2 deletions src/js_parser/visit/visit_stmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,21 @@ fn ts_namespace_member_value(data: js_ast::ts::Data) -> Option<TSConstantValue>
}
}

/// The enum, namespace, or member that `ref_` names; a sibling block's member goes through `namespace_alias`.
fn ts_member_data_of_symbol(
symbols: &[js_ast::Symbol],
members: &bun_collections::HashMap<Ref, js_ast::ts::Data>,
ref_: Ref,
) -> Option<js_ast::ts::Data> {
if let Some(alias) = &symbols[ref_.inner_index() as usize].namespace_alias {
let js_ast::ts::Data::Namespace(map) = *members.get(&alias.namespace_ref)? else {
return None;
};
return Some((*map).get(alias.alias.slice())?.data);
}
members.get(&ref_).copied()
}

/// An unbound `Infinity` or `NaN`, which tsc folds too.
fn ts_global_constant(name: &[u8]) -> Option<TSConstantValue> {
match name {
Expand Down Expand Up @@ -2366,7 +2381,11 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
if let Some(&value) = self.ts_enum_constants.get(&result.r#ref) {
return Some(value);
}
ts_namespace_member_value(*self.ref_to_ts_namespace_member.get(&result.r#ref)?)
ts_namespace_member_value(ts_member_data_of_symbol(
&self.symbols,
&self.ref_to_ts_namespace_member,
result.r#ref,
)?)
}
js_ast::ExprData::EDot(dot) => {
if dot.optional_chain.is_some() {
Expand Down Expand Up @@ -2423,7 +2442,11 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
if result.is_inside_with_scope || result.r#ref.is_empty() {
return None;
}
let mut data = *self.ref_to_ts_namespace_member.get(&result.r#ref)?;
let mut data = ts_member_data_of_symbol(
&self.symbols,
&self.ref_to_ts_namespace_member,
result.r#ref,
)?;
// The innermost member was pushed last.
for member_name in member_names.iter().rev() {
let js_ast::ts::Data::Namespace(map) = data else {
Expand All @@ -2437,6 +2460,70 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
}
}

/// Stores the constant members of the enums in an unvisited namespace body, for the enums `visit_stmts` pre-visits.
pub(crate) fn compute_enum_values_inside_namespace(&mut self, stmts: &[Stmt]) {
for stmt in stmts {
match stmt.data {
StmtData::SEnum(data) => self.compute_enum_values_ahead_of_visit(stmt.loc, &data),
StmtData::SNamespace(namespace) => {
if self.stack_check.is_safe_to_recurse() {
self.compute_enum_values_inside_namespace(namespace.stmts.slice());
}
}
_ => {}
}
}
}

fn compute_enum_values_ahead_of_visit(&mut self, loc: bun_ast::Loc, data: &S::Enum) {
// The first scope recorded for an enum statement is its own.
let scope = super::scopes_for_enum_at(&self.scopes_in_order_for_enum, loc)[0].scope_ref();
let Some(ts_namespace) = scope.ts_namespace else {
return;
};
let mut exported_members = ts_namespace.exported_members;

// Identifiers in an initializer resolve from inside the enum.
let old_scope = self.current_scope;
self.current_scope = scope;

// An initializer can reference the members before it by name.
let mut members_so_far: smallvec::SmallVec<[Ref; 16]> = smallvec::SmallVec::new();
let mut next_numeric_value: Option<f64> = Some(0.0);
for value in data.values.slice() {
let known = match value.value {
Some(initializer) => match self.eval_ts_constant_expression(&initializer) {
Some(TSConstantValue::Number(number)) => {
Some(js_ast::ts::Data::EnumNumber(number))
}
Some(TSConstantValue::String(str_)) => Some(js_ast::ts::Data::EnumString(str_)),
None => None,
},
None => next_numeric_value.map(js_ast::ts::Data::EnumNumber),
};
next_numeric_value = match known {
Some(js_ast::ts::Data::EnumNumber(number)) => Some(number + 1.0),
_ => None,
};
if let Some(known) = known {
exported_members
.get_ptr_mut(value.name.slice())
.unwrap()
.data = known;
if value.ref_.is_valid() {
self.ref_to_ts_namespace_member.insert(value.ref_, known);
Comment thread
robobun marked this conversation as resolved.
members_so_far.push(value.ref_);
}
}
}
// `s_enum` adds these back one member at a time, so that a later member stays a property access.
for ref_ in members_so_far {
self.ref_to_ts_namespace_member.remove(&ref_);
}

self.current_scope = old_scope;
}

fn s_enum(
p: &mut Self,
stmts: &mut StmtList<'a>,
Expand Down
3 changes: 2 additions & 1 deletion src/jsc/RuntimeTranspilerCache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ bun_core::declare_scope!(cache, visible);
/// Version 28: the define table and `--drop` entries participate in the features hash.
/// Version 29: `new Array(x, ...spread)` is no longer folded into an array literal.
/// Version 30: TypeScript enum initializers fold references to `const` variables.
const EXPECTED_VERSION: u32 = 30;
/// Version 31: TypeScript enum initializers fold members of enums nested in an earlier namespace.
const EXPECTED_VERSION: u32 = 31;

/// Source files smaller than this are not written to / read from the on-disk
/// transpiler cache. Originally 50 KiB, which excluded almost every file in a
Expand Down
44 changes: 44 additions & 0 deletions test/bundler/esbuild/ts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2589,4 +2589,48 @@ describe("bundler", () => {
`,
},
});
itBundled("ts/EnumReferencesEnumInsideEarlierNamespace", {
files: {
"/entry.ts": /* ts */ `
namespace NS {
export enum Inner { X = 3, S = "s" }
export namespace Deep {
enum Hidden { H = 100 }
export enum Flags { A = 1 << 0, B = 1 << 4, AB = A | B, H = Hidden.H }
}
export enum Merged { P = 200 }
export enum Merged { Q = P + 1, R }
}
enum Outer { A = NS.Inner.X, B, C = NS.Deep.Flags.AB, D, E = NS["Deep"].Flags["H"], F = NS.Merged.R, G, S = NS.Inner.S }
namespace Later {
export namespace L { export enum Z { Q = 7 } }
export enum D { A = L.Z.Q, B }
}
console.log(JSON.stringify([Outer, Later.D]));
export { Outer };
`,
"/other.ts": /* ts */ `
import { Outer } from "./entry";
console.log(JSON.stringify([Outer.B, Outer.G, Outer.S]));
`,
},
entryPoints: ["/entry.ts", "/other.ts"],
run: [
{
file: "/out/entry.js",
stdout:
'[{"3":"A","4":"B","17":"C","18":"D","100":"E","202":"F","203":"G","A":3,"B":4,"C":17,"D":18,"E":100,"F":202,"G":203,"S":"s"},{"7":"A","8":"B","A":7,"B":8}]',
},
{
file: "/out/other.js",
stdout:
'[{"3":"A","4":"B","17":"C","18":"D","100":"E","202":"F","203":"G","A":3,"B":4,"C":17,"D":18,"E":100,"F":202,"G":203,"S":"s"},{"7":"A","8":"B","A":7,"B":8}]\n[4,203,"s"]',
},
],
onAfterBundle(api) {
// The values are known at bundle time, so the uses in the other module are inlined.
const other = api.readFile("/out/other.js");
expect(other).not.toMatch(/Outer\.[BGS]/);
},
});
});
101 changes: 101 additions & 0 deletions test/bundler/transpiler/transpiler.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2231,6 +2231,107 @@ export default class {
expect(stdout).toBe('["m2","_m2"]\n');
expect(exitCode).toBe(0);
});

// The enums of a statement list are visited before its other statements, so
// an enum nested in a namespace is visited after the enums that follow the
// namespace. Its constant members must still be known to them, like in tsc.
describe("enum initializers that reference an enum inside an earlier namespace", () => {
const exp = ts.expectPrinted_;

it("numeric member continues auto-increment", () => {
exp(
"namespace M { export enum Z { Q = 3 } }\nenum D { A = M.Z.Q, B }",
'var M;\n((M) => {\n let Z;\n ((Z) => {\n Z[Z["Q"] = 3] = "Q";\n })(Z = M.Z ||= {});\n})(M ||= {});\nvar D;\n((D) => {\n D[D["A"] = 3] = "A";\n D[D["B"] = 4] = "B";\n})(D ||= {})',
);
});

it("nested namespaces, index access, string members", () => {
exp(
'namespace A { export namespace B { export enum Z { Q = 1 << 2, S = "s" } } }\nenum D { X = A.B.Z.Q | 1, Y, Z = A.B.Z.S, W = A["B"].Z["Q"] }',
'var A;\n((A) => {\n let B;\n ((B) => {\n let Z;\n ((Z) => {\n Z[Z["Q"] = 4] = "Q";\n Z["S"] = "s";\n })(Z = B.Z ||= {});\n })(B = A.B ||= {});\n})(A ||= {});\nvar D;\n((D) => {\n D[D["X"] = 5] = "X";\n D[D["Y"] = 6] = "Y";\n D["Z"] = "s";\n D[D["W"] = 4] = "W";\n})(D ||= {})',
);
});

it("constant expressions over earlier members, unary operators, template literals", () => {
exp(
"namespace M { export enum Z { A = 2, N = -(A ** 3), C = ~A, T = `v${N}` } }\nenum D { N = M.Z.N, C = M.Z.C, T = M.Z.T }",
'var M;\n((M) => {\n let Z;\n ((Z) => {\n Z[Z["A"] = 2] = "A";\n Z[Z["N"] = -8] = "N";\n Z[Z["C"] = -3] = "C";\n Z["T"] = "v-8";\n })(Z = M.Z ||= {});\n})(M ||= {});\nvar D;\n((D) => {\n D[D["N"] = -8] = "N";\n D[D["C"] = -3] = "C";\n D["T"] = "v-8";\n})(D ||= {})',
);
});

it("an enum inside a namespace sees an enum inside a nested namespace before it", () => {
exp(
"namespace O {\n export namespace I { export enum Z { Q = 7 } }\n export enum D { A = I.Z.Q, B }\n}",
'var O;\n((O) => {\n let I;\n ((I) => {\n let Z;\n ((Z) => {\n Z[Z["Q"] = 7] = "Q";\n })(Z = I.Z ||= {});\n })(I = O.I ||= {});\n let D;\n ((D) => {\n D[D["A"] = 7] = "A";\n D[D["B"] = 8] = "B";\n })(D = O.D ||= {});\n})(O ||= {})',
);
});

it("names resolve from the scope of the nested enum", () => {
// The inner `G` shadows the outer one inside `M`.
exp(
"enum G { X = 1 }\nnamespace M { enum G { X = 50 } export enum Z { A = G.X, B } }\nenum D { V = M.Z.B }",
'var G;\n((G) => {\n G[G["X"] = 1] = "X";\n})(G ||= {});\nvar M;\n((M) => {\n let G;\n ((G) => {\n G[G["X"] = 50] = "X";\n })(G ||= {});\n let Z;\n ((Z) => {\n Z[Z["A"] = 50] = "A";\n Z[Z["B"] = 51] = "B";\n })(Z = M.Z ||= {});\n})(M ||= {});\nvar D;\n((D) => {\n D[D["V"] = 51] = "V";\n})(D ||= {})',
);
});

it("members that are not constant are left alone", () => {
exp(
"namespace M { export enum Z { Q = f(), R = 2, S } }\nenum D { A = M.Z.R, B = M.Z.S, C = M.Z.Q }",
'var M;\n((M) => {\n let Z;\n ((Z) => {\n Z[Z["Q"] = f()] = "Q";\n Z[Z["R"] = 2] = "R";\n Z[Z["S"] = 3] = "S";\n })(Z = M.Z ||= {});\n})(M ||= {});\nvar D;\n((D) => {\n D[D["A"] = 2] = "A";\n D[D["B"] = 3] = "B";\n D[D["C"] = M.Z.Q] = "C";\n})(D ||= {})',
);
});

it("a namespace after the enum is not consulted", () => {
// tsc reports an error for a reference to an enum member declared later.
exp(
"enum D { A = M.Z.Q, B }\nnamespace M { export enum Z { Q = 3 } }",
'var D;\n((D) => {\n D[D["A"] = M.Z.Q] = "A";\n D[D["B"] = undefined] = "B";\n})(D ||= {});\nvar M;\n((M) => {\n let Z;\n ((Z) => {\n Z[Z["Q"] = 3] = "Q";\n })(Z = M.Z ||= {});\n})(M ||= {})',
);
});

it("a member that references a later member stays a property access, as at top level", () => {
exp(
"namespace M { export enum Z { A = B, B = 5, C } }\nenum D { X = M.Z.C, Y = M.Z.A }",
'var M;\n((M) => {\n let Z;\n ((Z) => {\n Z[Z["A"] = Z.B] = "A";\n Z[Z["B"] = 5] = "B";\n Z[Z["C"] = 6] = "C";\n })(Z = M.Z ||= {});\n})(M ||= {});\nvar D;\n((D) => {\n D[D["X"] = 6] = "X";\n D[D["Y"] = M.Z.A] = "Y";\n})(D ||= {})',
);
});

it("a const declared before the namespace", () => {
exp(
"const k = 2;\nnamespace M { export enum Z { Q = k * 10, R } }\nenum D { A = M.Z.R, B }",
'const k = 2;\nvar M;\n((M) => {\n let Z;\n ((Z) => {\n Z[Z["Q"] = 20] = "Q";\n Z[Z["R"] = 21] = "R";\n })(Z = M.Z ||= {});\n})(M ||= {});\nvar D;\n((D) => {\n D[D["A"] = 21] = "A";\n D[D["B"] = 22] = "B";\n})(D ||= {})',
);
});

it("a second block of the same enum sees the members of the first", () => {
exp(
"namespace M { export enum Z { A = 1 } export enum Z { B = A + 1, C } }\nenum D { X = M.Z.C, Y }",
'var M;\n((M) => {\n let Z;\n ((Z) => {\n Z[Z["A"] = 1] = "A";\n })(Z = M.Z ||= {});\n ((Z) => {\n Z[Z["B"] = 2] = "B";\n Z[Z["C"] = 3] = "C";\n })(Z = M.Z ||= {});\n})(M ||= {});\nvar D;\n((D) => {\n D[D["X"] = 3] = "X";\n D[D["Y"] = 4] = "Y";\n})(D ||= {})',
);
});

it("statements before the namespace see the values too, as with top-level enums", () => {
exp(
"export function f() {\n return M.Z.Q;\n}\nnamespace M { export enum Z { Q = 1 } }",
'export function f() {\n return 1 /* Q */;\n}\nvar M;\n((M) => {\n let Z;\n ((Z) => {\n Z[Z["Q"] = 1] = "Q";\n })(Z = M.Z ||= {});\n})(M ||= {})',
);
});

it("a folded string member is stored flat", () => {
// Each inlined use copies the member's node; a rope would be shared and appended to.
ts.expectPrintedMin_(
'namespace M { export enum Z { S = "a" + "b" } }\nenum D { B = M.Z.S }\nconsole.log(`${D.B}x`, `${D.B}y`, `${M.Z.S}z`);',
'var M;\n((M) => {\n let Z;\n ((Z) => Z.S = "ab")(Z = M.Z ||= {});\n})(M ||= {});\nvar D;\n((D) => D.B = "ab")(D ||= {});\nconsole.log("abx", "aby", "abz")',
);
});

it("const enum members are inlined with the computed value", () => {
const out = ts.parsedMin(
"namespace M { export const enum Z { Q = 3 } }\nconst enum D { A = M.Z.Q, B }\nconsole.log(D.B);",
);
expect(out.split("\n").at(-1)).toBe("console.log(4 /* B */)");
});
});
});

describe("exports.replace", () => {
Expand Down
Loading