diff --git a/src/js_parser/parse/parse_entry.rs b/src/js_parser/parse/parse_entry.rs index 9a493b79fc17..9a4284967d65 100644 --- a/src/js_parser/parse/parse_entry.rs +++ b/src/js_parser/parse/parse_entry.rs @@ -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()) + } _ => {} } } diff --git a/src/js_parser/visit/mod.rs b/src/js_parser/visit/mod.rs index a0572137e51e..4a2f6cc0355e 100644 --- a/src/js_parser/visit/mod.rs +++ b/src/js_parser/visit/mod.rs @@ -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()) + } _ => {} } } diff --git a/src/js_parser/visit/visit_stmt.rs b/src/js_parser/visit/visit_stmt.rs index 6351597fb1e0..833973a064d4 100644 --- a/src/js_parser/visit/visit_stmt.rs +++ b/src/js_parser/visit/visit_stmt.rs @@ -59,6 +59,21 @@ fn ts_namespace_member_value(data: js_ast::ts::Data) -> Option } } +/// 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_: Ref, +) -> Option { + 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 { match name { @@ -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() { @@ -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 { @@ -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 = 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); + 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>, diff --git a/src/jsc/RuntimeTranspilerCache.rs b/src/jsc/RuntimeTranspilerCache.rs index 5d7b9f60791a..4fa4e9ab3db9 100644 --- a/src/jsc/RuntimeTranspilerCache.rs +++ b/src/jsc/RuntimeTranspilerCache.rs @@ -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 diff --git a/test/bundler/esbuild/ts.test.ts b/test/bundler/esbuild/ts.test.ts index ad9c6dcefb80..5bbe9fef565c 100644 --- a/test/bundler/esbuild/ts.test.ts +++ b/test/bundler/esbuild/ts.test.ts @@ -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]/); + }, + }); }); diff --git a/test/bundler/transpiler/transpiler.test.js b/test/bundler/transpiler/transpiler.test.js index dc5a3b1c0855..794fb7d59a53 100644 --- a/test/bundler/transpiler/transpiler.test.js +++ b/test/bundler/transpiler/transpiler.test.js @@ -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", () => {