Repository: alloy-framework/alloy
Pull request #193 (2025-06-16 16:08:48)

Add class modifiers
fix #180 

github-actions[bot]: <!--chronus-github-change-commenter-->
:x: There is undocummented changes. Run `chronus add` to add a changeset or [click here](https://github.com/timotheeguerin/alloy/new/class-modifiers?filename=.chronus/changes/class-modifiers-2025-5-16-16-9-11.md&value=---%0A%23%20Change%20versionKind%20to%20one%20of%3A%20breaking%2C%20feature%2C%20fix%2C%20internal%0AchangeKind%3A%20fix%0Apackages%3A%0A%20%20-%20%22%40alloy-js%2Fcsharp%22%0A---%0A%0AAdd%20class%20modifiers).

**The following packages have changes but are not documented.**
 - :x:`@alloy-js/csharp`

<details>
<summary>Show changes</summary>


</details>


Patch:
diff --git a/.chronus/changes/class-modifiers-2025-5-16-16-9-11.md b/.chronus/changes/class-modifiers-2025-5-16-16-9-11.md
new file mode 100644
index 0000000..188f323
--- /dev/null
+++ b/.chronus/changes/class-modifiers-2025-5-16-16-9-11.md
@@ -0,0 +1,8 @@
+---
+# Change versionKind to one of: breaking, feature, fix, internal
+changeKind: feature
+packages:
+  - "@alloy-js/csharp"
+---
+
+Add support for class modifiers `abstract`, `partial`, `sealed` and `static`
diff --git a/packages/csharp/src/components/ClassDeclaration.tsx b/packages/csharp/src/components/ClassDeclaration.tsx
index 8f57015..0d034c3 100644
--- a/packages/csharp/src/components/ClassDeclaration.tsx
+++ b/packages/csharp/src/components/ClassDeclaration.tsx
@@ -3,6 +3,7 @@ import {
   AccessModifiers,
   computeModifiersPrefix,
   getAccessModifier,
+  makeModifiers,
 } from "../modifiers.js";
 import { CSharpElements, useCSharpNamePolicy } from "../name-policy.js";
 import { CSharpOutputSymbol } from "../symbols/csharp-output-symbol.js";
@@ -10,10 +11,25 @@ import { CSharpMemberScope, useCSharpScope } from "../symbols/scopes.js";
 import { Name } from "./Name.jsx";
 import { ParameterProps, Parameters } from "./Parameters.jsx";
 
+export interface ClassModifiers {
+  readonly abstract?: boolean;
+  readonly partial?: boolean;
+  readonly sealed?: boolean;
+  readonly static?: boolean;
+}
+
+const getClassModifiers = makeModifiers<ClassModifiers>([
+  "abstract",
+  "partial",
+  "sealed",
+  "static",
+]);
+
 // properties for creating a class
-export interface ClassProps
+export interface ClassDeclarationProps
   extends Omit<core.DeclarationProps, "nameKind">,
-    AccessModifiers {
+    AccessModifiers,
+    ClassModifiers {
   name: string;
   refkey?: core.Refkey;
   typeParameters?: Record<string, core.Refkey>;
@@ -43,7 +59,7 @@ export interface ClassProps
  * }
  * ```
  */
-export function ClassDeclaration(props: ClassProps) {
+export function ClassDeclaration(props: ClassDeclarationProps) {
   const name = useCSharpNamePolicy().getName(props.name!, "class");
 
   const thisClassSymbol = new CSharpOutputSymbol(name, {
@@ -83,7 +99,10 @@ export function ClassDeclaration(props: ClassProps) {
     );
   }
 
-  const modifiers = computeModifiersPrefix([getAccessModifier(props)]);
+  const modifiers = computeModifiersPrefix([
+    getAccessModifier(props),
+    getClassModifiers(props),
+  ]);
   return (
     <core.Declaration symbol={thisClassSymbol}>
       {modifiers}class <Name />
diff --git a/packages/csharp/src/components/EnumDeclaration.tsx b/packages/csharp/src/components/EnumDeclaration.tsx
index 6a3aa80..6ab0c77 100644
--- a/packages/csharp/src/components/EnumDeclaration.tsx
+++ b/packages/csharp/src/components/EnumDeclaration.tsx
@@ -10,7 +10,7 @@ import { CSharpMemberScope, useCSharpScope } from "../symbols/scopes.js";
 import { Name } from "./Name.jsx";
 
 // properties for creating an enum
-export interface EnumProps extends AccessModifiers {
+export interface EnumDeclarationProps extends AccessModifiers {
   name: string;
   refkey?: core.Refkey;
   children?: core.Children;
@@ -36,7 +36,7 @@ export interface EnumProps extends AccessModifiers {
  * }
  * ```
  */
-export function EnumDeclaration(props: EnumProps) {
+export function EnumDeclaration(props: EnumDeclarationProps) {
   const name = useCSharpNamePolicy().getName(props.name!, "enum");
   const scope = useCSharpScope();
 
diff --git a/packages/csharp/src/modifiers.ts b/packages/csharp/src/modifiers.ts
index b3f330d..9d3cce0 100644
--- a/packages/csharp/src/modifiers.ts
+++ b/packages/csharp/src/modifiers.ts
@@ -10,17 +10,13 @@ export interface AccessModifiers {
   readonly file?: boolean;
 }
 
-export function getAccessModifier(data: AccessModifiers): string {
-  return [
-    data.public && "public",
-    data.protected && "protected",
-    data.private && "private",
-    data.internal && "internal",
-    data.file && "file",
-  ]
-    .filter((x) => x)
-    .join(" ");
-}
+export const getAccessModifier = makeModifiers<AccessModifiers>([
+  "public",
+  "protected",
+  "private",
+  "internal",
+  "file",
+]);
 
 /** Method modifiers. Can only be one. */
 export interface MethodModifiers {
@@ -30,16 +26,12 @@ export interface MethodModifiers {
   readonly virtual?: boolean;
 }
 
-export function getMethodModifier(data: MethodModifiers): string {
-  return [
-    data.abstract && "abstract",
-    data.sealed && "sealed",
-    data.static && "static",
-    data.virtual && "virtual",
-  ]
-    .filter((x) => x)
-    .join(" ");
-}
+export const getMethodModifier = makeModifiers<MethodModifiers>([
+  "abstract",
+  "sealed",
+  "static",
+  "virtual",
+]);
 
 export function getAsyncModifier(async?: boolean): string {
   return async ? "async" : "";
@@ -52,3 +44,12 @@ export function computeModifiersPrefix(
   const resolved = modifiers.filter((x) => x);
   return resolved.length > 0 ? resolved.join(" ") + " " : "";
 }
+
+export function makeModifiers<T>(obj: Array<keyof T>) {
+  return (data: T) => {
+    return obj
+      .map((key) => (data[key] ? key : undefined))
+      .filter((x) => x)
+      .join(" ");
+  };
+}


Test patch:
diff --git a/packages/csharp/test/class.test.tsx b/packages/csharp/test/class.test.tsx
index a637c08..5ec8f85 100644
--- a/packages/csharp/test/class.test.tsx
+++ b/packages/csharp/test/class.test.tsx
@@ -1,22 +1,52 @@
 import * as core from "@alloy-js/core";
 import * as coretest from "@alloy-js/core/testing";
-import { expect, it } from "vitest";
+import { describe, expect, it } from "vitest";
 import * as csharp from "../src/index.js";
+import { ClassDeclaration } from "../src/index.js";
 import * as utils from "./utils.js";
 
 it("declares class with no members", () => {
-  const res = utils.toSourceText(
-    <csharp.ClassDeclaration public name="TestClass" />,
-  );
-
-  expect(res).toBe(coretest.d`
-    namespace TestCode
-    {
-        public class TestClass;
-    }
+  expect(
+    <utils.TestNamespace>
+      <ClassDeclaration name="TestClass" />
+    </utils.TestNamespace>,
+  ).toRenderTo(`
+      class TestClass;
   `);
 });
 
+describe("modifiers", () => {
+  it.each(["public", "private"])("%s", (mod) => {
+    expect(
+      <utils.TestNamespace>
+        <ClassDeclaration {...{ [mod]: true }} name="TestClass" />
+      </utils.TestNamespace>,
+    ).toRenderTo(`
+        ${mod} class TestClass;
+    `);
+  });
+
+  it.each(["partial", "abstract", "static", "sealed"])("%s", (mod) => {
+    expect(
+      <utils.TestNamespace>
+        <ClassDeclaration {...{ [mod]: true }} name="TestClass" />
+      </utils.TestNamespace>,
+    ).toRenderTo(`
+        ${mod} class TestClass;
+    `);
+  });
+
+  it("combines modifiers", () => {
+    expect(
+      <utils.TestNamespace>
+        <ClassDeclaration public abstract partial name="TestClass" />
+      </utils.TestNamespace>,
+    ).toRenderTo(`
+        public abstract partial class TestClass;
+    `);
+  });
+});
+
 it("declares class with some members", () => {
   const res = utils.toSourceText(
     <csharp.ClassDeclaration public name="TestClass">