diff --git a/Execution/Interpreter.Namespaces.cs b/Execution/Interpreter.Namespaces.cs index e9bab8e2..462f8eb9 100644 --- a/Execution/Interpreter.Namespaces.cs +++ b/Execution/Interpreter.Namespaces.cs @@ -16,8 +16,11 @@ private ExecutionResult ExecuteNamespace(Stmt.Namespace ns) { string name = ns.Name.Lexeme; - // Get or create namespace object - SharpTSNamespace? existingNs = _environment.GetNamespace(name); + // Get or create namespace object — check ONLY the current scope, not up the chain. + // GetNamespace walks up and would find a same-named outer namespace (e.g. top-level A + // when declaring O.A), incorrectly merging the nested namespace into the outer one and + // leaving the current scope without an "A" binding (#746). + SharpTSNamespace? existingNs = _environment.GetLocalNamespace(name); SharpTSNamespace nsObj; if (existingNs != null) diff --git a/Runtime/RuntimeEnvironment.cs b/Runtime/RuntimeEnvironment.cs index 5d71e3a0..3647be75 100644 --- a/Runtime/RuntimeEnvironment.cs +++ b/Runtime/RuntimeEnvironment.cs @@ -154,6 +154,17 @@ public void DefineNamespace(string name, SharpTSNamespace ns) return Enclosing?.GetNamespace(name); } + /// + /// Gets a namespace by name from THIS scope only (no chain traversal). + /// Use when deciding whether to merge vs. create a new namespace declaration — + /// avoids treating a same-named namespace in an enclosing scope as a merge target (#746). + /// + public SharpTSNamespace? GetLocalNamespace(string name) + { + _namespaces.TryGetValue(name, out var ns); + return ns; + } + /// /// Defines a variable with a boxed value (legacy compatibility). /// Wraps the value in RuntimeValue.FromBoxed automatically. diff --git a/SharpTS.Tests/SharedTests/NamespaceTests.cs b/SharpTS.Tests/SharedTests/NamespaceTests.cs index 42fae33a..1a23b77f 100644 --- a/SharpTS.Tests/SharedTests/NamespaceTests.cs +++ b/SharpTS.Tests/SharedTests/NamespaceTests.cs @@ -796,5 +796,25 @@ export namespace I { Assert.Equal("2\n", TestHarness.Run(code, mode)); } + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void NestedNamespace_ShadowsSameNamedTopLevelNamespace(ExecutionMode mode) + { + // Nested O.A shadows the top-level A for bare references from sibling O.B. + // The interpreter previously merged O.A's members into the global A and left + // namespaceEnvO without an "A" binding, so the pre-resolved distance-2 lookup + // returned Undefined → "Only instances and objects have properties" (#746). + var code = @" + namespace A { export function g() { return 100; } } + namespace O { + export namespace A { export function g() { return 5; } } + export namespace B { export function f() { return A.g(); } } + } + console.log(O.B.f()); + console.log(A.g()); + "; + Assert.Equal("5\n100\n", TestHarness.Run(code, mode)); + } + #endregion }