diff --git a/.chronus/changes/csharp-namespace-scope-chain-2026-2-4.md b/.chronus/changes/csharp-namespace-scope-chain-2026-2-4.md new file mode 100644 index 000000000..4f840f781 --- /dev/null +++ b/.chronus/changes/csharp-namespace-scope-chain-2026-2-4.md @@ -0,0 +1,8 @@ +--- +# Change versionKind to one of: breaking, feature, fix, internal +changeKind: fix +packages: + - "@alloy-js/csharp" +--- + +Fix namespace scope-chain construction for dotted namespaces declared inside `SourceFile` so refkey resolution no longer emits incorrect qualification or unnecessary using directives. Adds regression coverage for sibling and multi-level nested namespace scenarios. \ No newline at end of file diff --git a/packages/csharp/src/components/namespace.ref.test.tsx b/packages/csharp/src/components/namespace.ref.test.tsx index 2b4fc4eb1..94c656d78 100644 --- a/packages/csharp/src/components/namespace.ref.test.tsx +++ b/packages/csharp/src/components/namespace.ref.test.tsx @@ -162,3 +162,32 @@ it("can be referenced by refkey", () => { TestClass; `); }); + +it("references types across sibling namespaces under the same parent", () => { + const classRef = refkey(); + + const tree = ( + + + + + + + + {classRef}; + + + + ); + + expect(tree).toRenderTo(` + namespace Parent { + namespace Models { + class User; + } + namespace Services { + Models.User; + } + } + `); +}); diff --git a/packages/csharp/src/components/namespace/namespace.test.tsx b/packages/csharp/src/components/namespace/namespace.test.tsx index c7c2c628d..a3ce06a2b 100644 --- a/packages/csharp/src/components/namespace/namespace.test.tsx +++ b/packages/csharp/src/components/namespace/namespace.test.tsx @@ -1,9 +1,13 @@ import { TestNamespace } from "#test/utils.jsx"; -import { Output } from "@alloy-js/core"; +import { Output, refkey } from "@alloy-js/core"; import { d } from "@alloy-js/core/testing"; import { expect, it } from "vitest"; +import { createCSharpNamePolicy } from "../../name-policy.js"; import { ClassDeclaration } from "../class/declaration.jsx"; +import { Constructor } from "../constructor/constructor.jsx"; +import { Field } from "../field/field.jsx"; import { SourceFile } from "../source-file/source-file.jsx"; +import { StructDeclaration } from "../struct/declaration.jsx"; import { Namespace } from "./namespace.jsx"; it("defines multiple namespaces and source files with unique content", () => { @@ -141,3 +145,40 @@ it("define nested namespace in sourcefile", () => { } `); }); + +it("contains a struct with a private field initialized by a constructor", () => { + const fieldRefkey = refkey(); + const paramRefkey = refkey(); + + const tree = ( + + + + + + + + {fieldRefkey} = {paramRefkey}; + + + + + + ); + + expect(tree).toRenderTo(d` + namespace TestNamespace.Test { + public struct MyStruct + { + private int _value; + public MyStruct(int value) + { + _value = value; + } + } + } + `); +}); diff --git a/packages/csharp/src/components/namespace/namespace.tsx b/packages/csharp/src/components/namespace/namespace.tsx index d70aff421..3298547b0 100644 --- a/packages/csharp/src/components/namespace/namespace.tsx +++ b/packages/csharp/src/components/namespace/namespace.tsx @@ -1,8 +1,12 @@ import { Block, Namekey, Refkey } from "@alloy-js/core"; import { Children } from "@alloy-js/core/jsx-runtime"; -import { NamespaceContext } from "../../contexts/namespace.js"; +import { + NamespaceContext, + useNamespaceContext, +} from "../../contexts/namespace.js"; import { useSourceFileScope } from "../../scopes/source-file.js"; import { createNamespaceSymbol } from "../../symbols/factories.js"; +import { NamespaceSymbol } from "../../symbols/namespace.js"; import { NamespaceScope } from "../namespace-scopes.jsx"; import { NamespaceName } from "./namespace-name.jsx"; @@ -12,6 +16,32 @@ export interface NamespaceProps { children?: Children; } +/** + * Wraps children with namespace scopes for each level of the namespace + * hierarchy, stopping at the `stopAt` namespace symbol (which is already in + * scope from an enclosing NamespaceScopes). + */ +function wrapWithNamespaceScopes( + symbol: NamespaceSymbol, + children: Children, + stopAt?: NamespaceSymbol, +): Children { + if (symbol === stopAt) { + return children; + } + + const scopeChildren = ( + {children} + ); + + const enclosing = symbol.enclosingNamespace; + if (enclosing && !enclosing.isGlobal && enclosing !== stopAt) { + return wrapWithNamespaceScopes(enclosing, scopeChildren, stopAt); + } + + return scopeChildren; +} + export function Namespace(props: NamespaceProps) { const namespaceSymbol = createNamespaceSymbol(props.name, { refkeys: props.refkey, @@ -25,16 +55,23 @@ export function Namespace(props: NamespaceProps) { ); } else { + const nsContext = useNamespaceContext(); + const hasOuterNamespace = nsContext && !nsContext.symbol.isGlobal; + sfScope.hasBlockNamespace = true; return ( <> - namespace {" "} + namespace{" "} + {" "} - - - {props.children} - - + {wrapWithNamespaceScopes( + namespaceSymbol, + props.children, + nsContext?.symbol, + )} );