Repository: alloy-framework/alloy
Pull request #38 (2024-09-25 19:48:40)

Add indent prop to source file component, properly indent C#.


Patch:
diff --git a/.chronus/changes/add-indent-2024-8-25-12-46-11.md b/.chronus/changes/add-indent-2024-8-25-12-46-11.md
new file mode 100644
index 0000000..0c3eba9
--- /dev/null
+++ b/.chronus/changes/add-indent-2024-8-25-12-46-11.md
@@ -0,0 +1,7 @@
+---
+changeKind: feature
+packages:
+  - "@alloy-js/csharp"
+---
+
+Indent source files with 4 spaces as Anders intended.
\ No newline at end of file
diff --git a/.chronus/changes/add-indent-2024-8-25-12-46-38.md b/.chronus/changes/add-indent-2024-8-25-12-46-38.md
new file mode 100644
index 0000000..027e832
--- /dev/null
+++ b/.chronus/changes/add-indent-2024-8-25-12-46-38.md
@@ -0,0 +1,7 @@
+---
+changeKind: feature
+packages:
+  - "@alloy-js/core"
+---
+
+Add indent prop to source files for setting indent level.
\ No newline at end of file
diff --git a/.chronus/changes/feature-throws-2024-8-25-7-11-10.md b/.chronus/changes/feature-throws-2024-8-25-7-11-10.md
new file mode 100644
index 0000000..ebf326b
--- /dev/null
+++ b/.chronus/changes/feature-throws-2024-8-25-7-11-10.md
@@ -0,0 +1,7 @@
+---
+changeKind: feature
+packages:
+  - "@alloy-js/java"
+---
+
+@alloy-js/java: Allowing specifying `throws` clause to methods
diff --git a/packages/core/src/components/SourceFile.tsx b/packages/core/src/components/SourceFile.tsx
index f32f86d..a216052 100644
--- a/packages/core/src/components/SourceFile.tsx
+++ b/packages/core/src/components/SourceFile.tsx
@@ -5,15 +5,35 @@ import {
 } from "@alloy-js/core/jsx-runtime";
 import { join } from "pathe";
 import { useContext } from "../context.js";
+import { IndentContext } from "../context/indent.js";
 import { SourceDirectoryContext } from "../context/source-directory.js";
 import { SourceFileContext } from "../context/source-file.js";
 import { Refkey } from "../refkey.js";
 
 export interface SourceFileProps {
+  /**
+   * The path of this file relative to its parent directory
+   */
   path: string;
+
+  /**
+   * The type of contents in this file.
+   */
   filetype: string;
+
   children?: Children[];
+
+  /**
+   * The component to use to render refkeys references within the file's
+   * contents.
+   */
   reference?: ComponentDefinition<{ refkey: Refkey }>;
+
+  /**
+   * A string representing one indent level, used when reindenting contents of
+   * this file.
+   */
+  indent?: string;
 }
 
 export function SourceFile(props: SourceFileProps) {
@@ -27,7 +47,13 @@ export function SourceFile(props: SourceFileProps) {
   const nodeContext = getContext()!;
   nodeContext.meta ??= {};
   nodeContext.meta.sourceFile = context;
+
   return <SourceFileContext.Provider value={context}>
-    {props.children}
+    { props.indent
+      ? <IndentContext.Provider value={{ level: 0, indent: props.indent, indentString: "" }}>
+          {props.children}
+        </IndentContext.Provider>
+      : props.children
+    }
   </SourceFileContext.Provider>;
 }
diff --git a/packages/csharp/src/components/SourceFile.tsx b/packages/csharp/src/components/SourceFile.tsx
index 00bbee7..0083bb8 100644
--- a/packages/csharp/src/components/SourceFile.tsx
+++ b/packages/csharp/src/components/SourceFile.tsx
@@ -49,7 +49,7 @@ export function SourceFile(props: SourceFileProps) {
     addUsing,
   };
 
-  return <core.SourceFile path={props.path} filetype="cs" reference={Reference}>
+  return <core.SourceFile path={props.path} filetype="cs" reference={Reference} indent="    ">
       <SourceFileContext.Provider value={sourceFileCtx}>
         <core.Scope name={props.path} kind="source-file">
           {using.length > 0 ? (
diff --git a/packages/java/src/components/Method.tsx b/packages/java/src/components/Method.tsx
index 7c163c6..7c95b92 100644
--- a/packages/java/src/components/Method.tsx
+++ b/packages/java/src/components/Method.tsx
@@ -7,6 +7,7 @@ import { Parameters } from "./Parameters.js";
 export interface MethodProps extends ObjectModifiers, GenericTypes {
   name: string;
   return?: Children;
+  throws?: Children;
   parameters?: Record<string, Children>;
   children?: Children;
 }
@@ -15,6 +16,7 @@ export function Method(props: MethodProps) {
   const params = <Parameters parameters={props.parameters}></Parameters>;
   const name = useJavaNamePolicy().getName(props.name, "method");
   const modifiers = collectModifiers(props);
+  const throwsClause = props.throws ? code` throws ${props.throws}` : "";
   const generics = props.generics ?
     code`${collectGenerics(props.generics)}${" "}`
   : "";
@@ -26,6 +28,6 @@ export function Method(props: MethodProps) {
   `
   : ";";
   return code`
-        ${modifiers}${generics}${props.return ?? "void"} ${name}(${params})${sBody}
+        ${modifiers}${generics}${props.return ?? "void"} ${name}(${params})${throwsClause}${sBody}
     `;
 }


Test patch:
diff --git a/packages/core/test/components/source-file.test.tsx b/packages/core/test/components/source-file.test.tsx
index f9c60cf..6eb60b0 100644
--- a/packages/core/test/components/source-file.test.tsx
+++ b/packages/core/test/components/source-file.test.tsx
@@ -43,3 +43,31 @@ it("has reactive context", () => {
 
   expect(tree.contents[1].contents).toEqual("hi.txt contents.txt");
 });
+
+it("can change its indent level", () => {
+  function Nested() {
+    return <>
+      base
+        indented
+    </>;
+  }
+  expect(
+    <Output>
+    <SourceFile path="hi.txt" filetype="text" indent="    ">
+      hello
+        there
+
+      <Nested />
+        <Nested />
+    </SourceFile>
+  </Output>,
+  ).toRenderTo(`
+    hello
+        there
+
+    base
+        indented
+        base
+            indented
+  `);
+});
diff --git a/packages/csharp/test/class.test.tsx b/packages/csharp/test/class.test.tsx
index 0bab8da..6c18f11 100644
--- a/packages/csharp/test/class.test.tsx
+++ b/packages/csharp/test/class.test.tsx
@@ -12,7 +12,7 @@ it("declares class with no members", () => {
   expect(res).toBe(coretest.d`
     namespace TestCode
     {
-      public class TestClass;
+        public class TestClass;
     }
 
   `);
@@ -29,11 +29,11 @@ it("declares class with some members", () => {
   expect(res).toBe(coretest.d`
     namespace TestCode
     {
-      public class TestClass
-      {
-        public string MemberOne;
-        private int memberTwo;
-      }
+        public class TestClass
+        {
+            public string MemberOne;
+            private int memberTwo;
+        }
     }
 
   `);
@@ -50,11 +50,11 @@ it("declares class with some methods", () => {
   expect(res).toBe(coretest.d`
     namespace TestCode
     {
-      public class TestClass
-      {
-        public void MethodOne() {}
-        private virtual void MethodTwo() {}
-      }
+        public class TestClass
+        {
+            public void MethodOne() {}
+            private virtual void MethodTwo() {}
+        }
     }
 
   `);
@@ -80,10 +80,10 @@ it("declares class with params and return type", () => {
   expect(res).toBe(coretest.d`
     namespace TestCode
     {
-      public class TestClass
-      {
-        public string MethodOne(int intParam, string stringParam) {}
-      }
+        public class TestClass
+        {
+            public string MethodOne(int intParam, string stringParam) {}
+        }
     }
 
   `);
@@ -129,21 +129,21 @@ it("uses refkeys for members, params, and return type", () => {
   expect(res.contents[0].contents).toBe(coretest.d`
     namespace TestCode
     {
-      public enum TestEnum
-      {
-        One,
-        Two
-      }
-      public class TestInput;
-      public class TestResult;
-      public class TestClass
-      {
-        private TestEnum memberOne;
-        public TestResult MethodOne(int intParam, TestInput bodyParam)
+        public enum TestEnum
         {
-          return new TestResult();
+            One,
+            Two
+        }
+        public class TestInput;
+        public class TestResult;
+        public class TestClass
+        {
+            private TestEnum memberOne;
+            public TestResult MethodOne(int intParam, TestInput bodyParam)
+            {
+                return new TestResult();
+            }
         }
-      }
     }
 
   `);
@@ -165,11 +165,11 @@ it("declares class with generic parameters", () => {
   expect(res).toBe(coretest.d`
     namespace TestCode
     {
-      public class TestClass<T, U>
-      {
-        public T MemberOne;
-        private U memberTwo;
-      }
+        public class TestClass<T, U>
+        {
+            public T MemberOne;
+            private U memberTwo;
+        }
     }
 
   `);
@@ -197,10 +197,10 @@ it("declares class with constructor", () => {
   expect(res).toBe(coretest.d`
     namespace TestCode
     {
-      public class TestClass
-      {
-        public TestClass() {}
-      }
+        public class TestClass
+        {
+            public TestClass() {}
+        }
     }
 
   `);
@@ -241,16 +241,16 @@ it("declares class with constructor params and assigns values to fields", () =>
   expect(res).toBe(coretest.d`
     namespace TestCode
     {
-      public class TestClass
-      {
-        private string name;
-        private int size;
-        public TestClass(string name, int size)
+        public class TestClass
         {
-          name = name;
-          size = size;
+            private string name;
+            private int size;
+            public TestClass(string name, int size)
+            {
+                name = name;
+                size = size;
+            }
         }
-      }
     }
 
   `);
diff --git a/packages/csharp/test/enum.test.tsx b/packages/csharp/test/enum.test.tsx
index 3c7683a..7726127 100644
--- a/packages/csharp/test/enum.test.tsx
+++ b/packages/csharp/test/enum.test.tsx
@@ -12,7 +12,7 @@ it("declares enum with no members", () => {
   expect(res).toBe(coretest.d`
     namespace TestCode
     {
-      public enum TestEnum;
+        public enum TestEnum;
     }
 
   `);
@@ -29,11 +29,11 @@ it("declares enum with members", () => {
   expect(res).toBe(coretest.d`
     namespace TestCode
     {
-      public enum TestEnum
-      {
-        One,
-        Two
-      }
+        public enum TestEnum
+        {
+            One,
+            Two
+        }
     }
 
   `);
@@ -50,11 +50,11 @@ it("applies naming policy to enum and members", () => {
   expect(res).toBe(coretest.d`
     namespace TestCode
     {
-      public enum TestEnum
-      {
-        One,
-        Two
-      }
+        public enum TestEnum
+        {
+            One,
+            Two
+        }
     }
 
   `);
@@ -82,13 +82,13 @@ it("can reference things by refkey", () => {
   expect(res.contents[0].contents).toBe(coretest.d`
     namespace TestCode
     {
-      public enum TestEnum
-      {
-        One,
-        Two
-      }
-      TestEnum;
-      TestEnum.Two;
+        public enum TestEnum
+        {
+            One,
+            Two
+        }
+        TestEnum;
+        TestEnum.Two;
     }
 
   `);
@@ -125,13 +125,13 @@ it("can reference things by refkey across files", () => {
   expect(res.contents[0].contents).toBe(coretest.d`
     namespace TestCode
     {
-      public enum TestEnum
-      {
-        One,
-        Two
-      }
-      OtherEnum;
-      OtherEnum.Bar;
+        public enum TestEnum
+        {
+            One,
+            Two
+        }
+        OtherEnum;
+        OtherEnum.Bar;
     }
 
   `);
@@ -140,13 +140,13 @@ it("can reference things by refkey across files", () => {
   expect(res.contents[1].contents).toBe(coretest.d`
     namespace TestCode
     {
-      public enum OtherEnum
-      {
-        Foo,
-        Bar
-      }
-      OtherEnum;
-      OtherEnum.Bar;
+        public enum OtherEnum
+        {
+            Foo,
+            Bar
+        }
+        OtherEnum;
+        OtherEnum.Bar;
     }
 
   `);
diff --git a/packages/csharp/test/namespace.test.tsx b/packages/csharp/test/namespace.test.tsx
index 2aebfe3..74fb5f2 100644
--- a/packages/csharp/test/namespace.test.tsx
+++ b/packages/csharp/test/namespace.test.tsx
@@ -29,7 +29,7 @@ it("defines multiple namespaces and source files with unique content", () => {
   expect(res.contents[0].contents).toBe(coretest.d`
     namespace Namespace1
     {
-      public class Model1;
+        public class Model1;
     }
 
   `);
@@ -38,7 +38,7 @@ it("defines multiple namespaces and source files with unique content", () => {
   expect(res.contents[1].contents).toBe(coretest.d`
     namespace Namespace1
     {
-      public class Model2;
+        public class Model2;
     }
 
   `);
@@ -47,7 +47,7 @@ it("defines multiple namespaces and source files with unique content", () => {
   expect(res.contents[2].contents).toBe(coretest.d`
     namespace Namespace2
     {
-      public class Model3;
+        public class Model3;
     }
 
   `);
@@ -56,7 +56,7 @@ it("defines multiple namespaces and source files with unique content", () => {
   expect(res.contents[3].contents).toBe(coretest.d`
     namespace Namespace2
     {
-      public class Model4;
+        public class Model4;
     }
 
   `);
diff --git a/packages/csharp/test/projectdirectory.test.tsx b/packages/csharp/test/projectdirectory.test.tsx
index 6bed2a3..7ed089f 100644
--- a/packages/csharp/test/projectdirectory.test.tsx
+++ b/packages/csharp/test/projectdirectory.test.tsx
@@ -41,7 +41,7 @@ it("defines a project directory file with multiple source files", () => {
   expect(srcDir.contents[0].contents).toBe(coretest.d`
     namespace TestCode
     {
-      public class TestClass1;
+        public class TestClass1;
     }
 
   `);
@@ -50,7 +50,7 @@ it("defines a project directory file with multiple source files", () => {
   expect(srcDir.contents[1].contents).toBe(coretest.d`
     namespace TestCode
     {
-      public class TestClass2;
+        public class TestClass2;
     }
 
   `);
@@ -94,7 +94,7 @@ it("defines a project directory file with multiple source files and a custom TFM
   expect(srcDir.contents[0].contents).toBe(coretest.d`
     namespace TestCode
     {
-      public class TestClass1;
+        public class TestClass1;
     }
 
   `);
@@ -103,7 +103,7 @@ it("defines a project directory file with multiple source files and a custom TFM
   expect(srcDir.contents[1].contents).toBe(coretest.d`
     namespace TestCode
     {
-      public class TestClass2;
+        public class TestClass2;
     }
 
   `);
diff --git a/packages/csharp/test/sourcefile.test.tsx b/packages/csharp/test/sourcefile.test.tsx
index 1824477..7a169f4 100644
--- a/packages/csharp/test/sourcefile.test.tsx
+++ b/packages/csharp/test/sourcefile.test.tsx
@@ -21,7 +21,7 @@ it("defines multiple source files with unique content", () => {
   expect(res.contents[0].contents).toBe(coretest.d`
     namespace TestCode
     {
-      public class TestClass1;
+        public class TestClass1;
     }
 
   `);
@@ -30,7 +30,7 @@ it("defines multiple source files with unique content", () => {
   expect(res.contents[1].contents).toBe(coretest.d`
     namespace TestCode
     {
-      public class TestClass2;
+        public class TestClass2;
     }
 
   `);
diff --git a/packages/csharp/test/using.test.tsx b/packages/csharp/test/using.test.tsx
index 7b18a17..c409d89 100644
--- a/packages/csharp/test/using.test.tsx
+++ b/packages/csharp/test/using.test.tsx
@@ -76,13 +76,13 @@ it("adds using statement across namespaces", () => {
   expect(res.contents[0].contents).toBe(coretest.d`
     namespace Models
     {
-      public class Input;
-      public class Output;
-      public enum TestEnum
-      {
-        One,
-        Two
-      }
+        public class Input;
+        public class Output;
+        public enum TestEnum
+        {
+            One,
+            Two
+        }
     }
 
   `);
@@ -93,11 +93,11 @@ it("adds using statement across namespaces", () => {
 
     namespace Client
     {
-      public class Client
-      {
-        public Output MethodOne(Input bodyParam) {}
-      }
-      TestEnum.Two;
+        public class Client
+        {
+            public Output MethodOne(Input bodyParam) {}
+        }
+        TestEnum.Two;
     }
 
   `);
diff --git a/packages/java/test/method.test.tsx b/packages/java/test/method.test.tsx
index d0351ac..728a2e4 100644
--- a/packages/java/test/method.test.tsx
+++ b/packages/java/test/method.test.tsx
@@ -1,6 +1,6 @@
 import { code, Declaration, refkey } from "@alloy-js/core";
 import { d } from "@alloy-js/core/testing";
-import { expect, it } from "vitest";
+import { describe, expect, it } from "vitest";
 import * as jv from "../src/components/index.js";
 import { assertFileContents, testRender, toSourceText } from "./utils.js";
 
@@ -48,6 +48,67 @@ it("declares bodyless function", () => {
   `);
 });
 
+describe("can throw errors", () => {
+  it("of generic Exception", () => {
+    const res = toSourceText(
+      <jv.Declaration name="Test">
+        {code`
+        class Test {
+          ${<jv.Method public name="testMethod" throws={'Exception'} />}
+        }
+      `}
+      </jv.Declaration>,
+    );
+
+    expect(res).toBe(d`
+    package me.test.code;
+    
+    class Test {
+      public void testMethod() throws Exception;
+    }
+  `);
+  });
+
+  it("of custom Exception", () => {
+    const res = testRender(
+      <>
+        <jv.SourceFile path="Test.java">
+          <jv.Declaration name="Test">
+            {code`
+              class Test {
+                ${<jv.Method public name="testMethod" throws={refkey('CustomError')} />}
+              }
+            `}
+          </jv.Declaration>
+        </jv.SourceFile>
+        <jv.PackageDirectory package="errors">
+          <jv.SourceFile path="CustomError.java">
+            <jv.Declaration name="CustomError">
+              {code`
+                public class CustomError extends Exception {
+                }
+              `}
+            </jv.Declaration>
+          </jv.SourceFile>
+
+        </jv.PackageDirectory>
+      </>,
+    );
+
+    assertFileContents(res, {
+      "Test.java": d`
+      package me.test.code;
+      
+      import me.test.code.errors.CustomError;
+
+      class Test {
+        public void testMethod() throws CustomError;
+      }
+    `,
+    });
+  });
+});
+
 it("declares return type", () => {
   const res = toSourceText(
     <jv.Declaration name="Test">