Skip to content
Open
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 .chronus/changes/csharp-namespace-scope-chain-2026-2-4.md
Original file line number Diff line number Diff line change
@@ -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.
29 changes: 29 additions & 0 deletions packages/csharp/src/components/namespace.ref.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
<Output>
<SourceFile path="test.cs">
<Namespace name="Parent">
<Namespace name="Models">
<ClassDeclaration name="User" refkey={classRef} />
</Namespace>
<hbr />
<Namespace name="Services">{classRef};</Namespace>
</Namespace>
</SourceFile>
</Output>
);

expect(tree).toRenderTo(`
namespace Parent {
namespace Models {
class User;
}
namespace Services {
Models.User;
}
}
`);
});
43 changes: 42 additions & 1 deletion packages/csharp/src/components/namespace/namespace.test.tsx
Original file line number Diff line number Diff line change
@@ -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", () => {
Expand Down Expand Up @@ -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 = (
<Output namePolicy={createCSharpNamePolicy()}>
<SourceFile path="MyStruct.cs">
<Namespace name="TestNamespace.Test">
<StructDeclaration public name="MyStruct">
<Field private name="value" type="int" refkey={fieldRefkey} />
<hbr />
<Constructor
public
parameters={[{ name: "value", type: "int", refkey: paramRefkey }]}
>
{fieldRefkey} = {paramRefkey};
</Constructor>
</StructDeclaration>
</Namespace>
</SourceFile>
</Output>
);

expect(tree).toRenderTo(d`
namespace TestNamespace.Test {
public struct MyStruct
{
private int _value;
public MyStruct(int value)
{
_value = value;
}
}
}
`);
});
51 changes: 44 additions & 7 deletions packages/csharp/src/components/namespace/namespace.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -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(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see we already do that in NamespaceScopes can we maybe reuse that component instead?

symbol: NamespaceSymbol,
children: Children,
stopAt?: NamespaceSymbol,
): Children {
if (symbol === stopAt) {
return children;
}

const scopeChildren = (
<NamespaceScope symbol={symbol}>{children}</NamespaceScope>
);

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,
Expand All @@ -25,16 +55,23 @@ export function Namespace(props: NamespaceProps) {
</NamespaceContext.Provider>
);
} else {
const nsContext = useNamespaceContext();
const hasOuterNamespace = nsContext && !nsContext.symbol.isGlobal;

sfScope.hasBlockNamespace = true;
return (
<>
namespace <NamespaceName symbol={namespaceSymbol} relative />{" "}
namespace{" "}
<NamespaceName
symbol={namespaceSymbol}
relative={!!hasOuterNamespace}
/>{" "}
<Block>
<NamespaceContext.Provider value={{ symbol: namespaceSymbol }}>
<NamespaceScope symbol={namespaceSymbol}>
{props.children}
</NamespaceScope>
</NamespaceContext.Provider>
{wrapWithNamespaceScopes(
namespaceSymbol,
props.children,
nsContext?.symbol,
)}
</Block>
</>
);
Expand Down
Loading