Skip to content
Merged
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
8 changes: 8 additions & 0 deletions Compilation/ILCompiler.Modules.cs
Original file line number Diff line number Diff line change
Expand Up @@ -713,6 +713,14 @@ private void EmitModulesEntryPoint(List<ParsedModule> 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
Expand Down
104 changes: 104 additions & 0 deletions SharpTS.Tests/SharedTests/NamespaceInModuleTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
using SharpTS.Tests.Infrastructure;
using Xunit;

namespace SharpTS.Tests.SharedTests;

/// <summary>
/// Regression tests for #1245: in compiled mode, a <c>namespace</c> declaration combined with a
/// top-level <c>import</c> (which turns the file into an ES module) crashed at startup with a
/// <c>NullReferenceException</c> in <c>$TSNamespace.Set</c>. The multi-module entry point never
/// called <c>InitializeNamespaceFields</c>, so the namespace's backing static field was still null
/// when the module's <c>$Initialize</c> 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.
/// </summary>
public class NamespaceInModuleTests
{
[Theory]
[MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))]
public void Namespace_WithImportAfter_ExportedConst(ExecutionMode mode)
{
var files = new Dictionary<string, string>
{
["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<string, string>
{
["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<string, string>
{
["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<string, string>
{
["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);
}
}
Loading