diff --git a/Compilation/ILCompiler.Modules.cs b/Compilation/ILCompiler.Modules.cs index 09b4c572..1e64e534 100644 --- a/Compilation/ILCompiler.Modules.cs +++ b/Compilation/ILCompiler.Modules.cs @@ -713,6 +713,14 @@ private void EmitModulesEntryPoint(List modules) } } + // Initialize namespace static fields before any module/script $Initialize body runs. + // Namespace objects live in flat static fields ($ns_*) on $Program shared across every + // module, but a module's $Initialize only POPULATES them (EmitNamespace does + // $ns_X.Set(...)); it never creates them. The single-file entry point calls this at the + // top of Main; the multi-module entry point must too, or the first Set() dereferences a + // null namespace field (#1245). + InitializeNamespaceFields(il); + // Call each module/script's $Initialize method in dependency order. // CommonJS modules are initialized lazily — only the entry CJS module is run eagerly, // and require() triggers the rest. This matches Node semantics for the visible execution diff --git a/SharpTS.Tests/SharedTests/NamespaceInModuleTests.cs b/SharpTS.Tests/SharedTests/NamespaceInModuleTests.cs new file mode 100644 index 00000000..ca8cd36f --- /dev/null +++ b/SharpTS.Tests/SharedTests/NamespaceInModuleTests.cs @@ -0,0 +1,104 @@ +using SharpTS.Tests.Infrastructure; +using Xunit; + +namespace SharpTS.Tests.SharedTests; + +/// +/// Regression tests for #1245: in compiled mode, a namespace declaration combined with a +/// top-level import (which turns the file into an ES module) crashed at startup with a +/// NullReferenceException in $TSNamespace.Set. The multi-module entry point never +/// called InitializeNamespaceFields, so the namespace's backing static field was still null +/// when the module's $Initialize body tried to populate it. A namespace in a plain script +/// (no import) was unaffected — becoming an ES module is what broke it. The interpreter always ran +/// these correctly; each test runs both modes so the compiled path is pinned against that oracle. +/// +public class NamespaceInModuleTests +{ + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void Namespace_WithImportAfter_ExportedConst(ExecutionMode mode) + { + var files = new Dictionary + { + ["main.ts"] = """ + namespace NS { + export const x = 1; + } + console.log(NS.x); + import * as fs from "fs"; + console.log(typeof fs.statSync); + """ + }; + var output = TestHarness.RunModules(files, "main.ts", mode); + Assert.Equal("1\nfunction\n", output); + } + + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void Namespace_WithImportBefore_ExportedFunction(ExecutionMode mode) + { + var files = new Dictionary + { + ["main.ts"] = """ + import * as path from "path"; + namespace NS { + export function greet(): string { return "hi"; } + } + console.log(NS.greet()); + console.log(typeof path.join); + """ + }; + var output = TestHarness.RunModules(files, "main.ts", mode); + Assert.Equal("hi\nfunction\n", output); + } + + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void NestedNamespace_WithImport(ExecutionMode mode) + { + var files = new Dictionary + { + ["main.ts"] = """ + import * as path from "path"; + namespace Outer { + export const a = 10; + export namespace Inner { + export const b = 42; + } + } + console.log(Outer.a); + console.log(Outer.Inner.b); + console.log(typeof path.join); + """ + }; + var output = TestHarness.RunModules(files, "main.ts", mode); + Assert.Equal("10\n42\nfunction\n", output); + } + + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void Namespace_WithSiblingModuleImport_ClassAndEnumMembers(ExecutionMode mode) + { + var files = new Dictionary + { + ["helper.ts"] = """ + export function add(a: number, b: number): number { return a + b; } + """, + ["main.ts"] = """ + import { add } from "./helper"; + namespace Calc { + export const base = 10; + export function compute(): number { return add(base, 5); } + export enum Op { Add, Sub } + export class Adder { run(): number { return add(1, 2); } } + } + console.log(Calc.base); + console.log(Calc.compute()); + console.log(Calc.Op.Sub); + console.log(new Calc.Adder().run()); + """ + }; + var output = TestHarness.RunModules(files, "main.ts", mode); + Assert.Equal("10\n15\n1\n3\n", output); + } +}