diff --git a/.cursor/mcp.json b/.cursor/mcp.json new file mode 100644 index 0000000..679468e --- /dev/null +++ b/.cursor/mcp.json @@ -0,0 +1,7 @@ +{ + "mcpServers": { + "nx-mcp": { + "url": "http://localhost:9267/sse" + } + } +} diff --git a/libs/csharp-ast/jest.config.ts b/libs/csharp-ast/jest.config.ts index d43a130..a2f2d41 100644 --- a/libs/csharp-ast/jest.config.ts +++ b/libs/csharp-ast/jest.config.ts @@ -10,8 +10,8 @@ export default { coverageDirectory: "../../coverage/libs/csharp-ast", coverageThreshold: { global: { - branches: 73, - lines: 74, + branches: 90, + lines: 90, }, }, }; diff --git a/libs/csharp-ast/src/lib/ast/Access.spec.ts b/libs/csharp-ast/src/lib/ast/Access.spec.ts new file mode 100644 index 0000000..c1d44c2 --- /dev/null +++ b/libs/csharp-ast/src/lib/ast/Access.spec.ts @@ -0,0 +1,68 @@ +import { Access } from "./Access"; + +describe("Access", () => { + it("should define Public access modifier", () => { + expect(Access.Public).toBe("public"); + }); + + it("should define Private access modifier", () => { + expect(Access.Private).toBe("private"); + }); + + it("should define Protected access modifier", () => { + expect(Access.Protected).toBe("protected"); + }); + + it("should maintain constant values", () => { + const initialValues = { + Public: Access.Public, + Private: Access.Private, + Protected: Access.Protected, + }; + + expect(Access.Public).toBe(initialValues.Public); + expect(Access.Private).toBe(initialValues.Private); + expect(Access.Protected).toBe(initialValues.Protected); + }); + + it("should not allow regular modification due to TypeScript readonly properties", () => { + // We validate this with TypeScript's type checking + // This line would error at compile time if uncommented: + // Access.Public = "modified"; + + // Runtime check for object as const + expect(typeof Access).toBe("object"); + expect(Object.keys(Access)).toEqual(["Public", "Private", "Protected"]); + }); + + it("should support type checking with string literals", () => { + const publicAccess: Access = "public"; + const privateAccess: Access = "private"; + const protectedAccess: Access = "protected"; + + expect(publicAccess).toBe(Access.Public); + expect(privateAccess).toBe(Access.Private); + expect(protectedAccess).toBe(Access.Protected); + }); + + it("should be usable in switch statements", () => { + const getDescription = (access: Access): string => { + switch (access) { + case Access.Public: + return "visible to all"; + case Access.Private: + return "visible only within class"; + case Access.Protected: + return "visible within class and subclasses"; + default: + return "unknown"; + } + }; + + expect(getDescription(Access.Public)).toBe("visible to all"); + expect(getDescription(Access.Private)).toBe("visible only within class"); + expect(getDescription(Access.Protected)).toBe( + "visible within class and subclasses", + ); + }); +}); diff --git a/libs/csharp-ast/src/lib/ast/Annotation.spec.ts b/libs/csharp-ast/src/lib/ast/Annotation.spec.ts new file mode 100644 index 0000000..07db87f --- /dev/null +++ b/libs/csharp-ast/src/lib/ast/Annotation.spec.ts @@ -0,0 +1,214 @@ +import { Annotation } from "./Annotation"; +import { ClassReference } from "./ClassReference"; +import { Writer } from "../core/Writer"; +import { AstNode } from "../core/AstNode"; + +describe("Annotation", () => { + describe("constructor", () => { + it("should initialize with reference", () => { + const reference = new ClassReference({ + name: "TestAttribute", + namespace: "Test.Namespace", + }); + + const annotation = new Annotation({ + reference, + }); + + expect(annotation["reference"]).toBe(reference); + expect(annotation["argument"]).toBeUndefined(); + }); + + it("should initialize with string argument", () => { + const reference = new ClassReference({ + name: "TestAttribute", + namespace: "Test.Namespace", + }); + + const annotation = new Annotation({ + reference, + argument: "argumentValue", + }); + + expect(annotation["reference"]).toBe(reference); + expect(annotation["argument"]).toBe("argumentValue"); + }); + + it("should initialize with AstNode argument", () => { + const reference = new ClassReference({ + name: "TestAttribute", + namespace: "Test.Namespace", + }); + + // Creating a simple AstNode for testing + class TestNode extends AstNode { + public write(writer: Writer): void { + writer.write("testNode"); + } + } + + const argumentNode = new TestNode(); + + const annotation = new Annotation({ + reference, + argument: argumentNode, + }); + + expect(annotation["reference"]).toBe(reference); + expect(annotation["argument"]).toBe(argumentNode); + }); + }); + + describe("write", () => { + it("should write annotation without arguments", () => { + const reference = new ClassReference({ + name: "TestAttribute", + namespace: "Test.Namespace", + }); + + const annotation = new Annotation({ + reference, + }); + + const writer = new Writer({}); + annotation.write(writer); + + const result = writer.toString(); + expect(result).toContain("TestAttribute()"); + expect(result).toContain("using Test.Namespace;"); + }); + + it("should write annotation with string argument", () => { + const reference = new ClassReference({ + name: "TestAttribute", + namespace: "Test.Namespace", + }); + + const annotation = new Annotation({ + reference, + argument: '"argumentValue"', + }); + + const writer = new Writer({}); + annotation.write(writer); + + const result = writer.toString(); + expect(result).toContain('TestAttribute("argumentValue")'); + expect(result).toContain("using Test.Namespace;"); + }); + + it("should write annotation with complex string argument", () => { + const reference = new ClassReference({ + name: "DisplayAttribute", + namespace: "System.ComponentModel.DataAnnotations", + }); + + const annotation = new Annotation({ + reference, + argument: 'Name = "Username", Description = "Your login name"', + }); + + const writer = new Writer({}); + annotation.write(writer); + + const result = writer.toString(); + expect(result).toContain( + 'DisplayAttribute(Name = "Username", Description = "Your login name")', + ); + expect(result).toContain("using System.ComponentModel.DataAnnotations;"); + }); + + it("should write annotation with AstNode argument", () => { + const reference = new ClassReference({ + name: "TestAttribute", + namespace: "Test.Namespace", + }); + + // Creating a simple AstNode for testing + class TestNode extends AstNode { + public write(writer: Writer): void { + writer.write("testNodeValue"); + } + } + + const argumentNode = new TestNode(); + + const annotation = new Annotation({ + reference, + argument: argumentNode, + }); + + const writer = new Writer({}); + annotation.write(writer); + + const result = writer.toString(); + expect(result).toContain("TestAttribute(testNodeValue)"); + expect(result).toContain("using Test.Namespace;"); + }); + + it("should add reference to writer", () => { + const reference = new ClassReference({ + name: "TestAttribute", + namespace: "Test.Namespace", + }); + + const annotation = new Annotation({ + reference, + }); + + const writer = new Writer({}); + + // Create a spy on addReference + const addReferenceSpy = jest.spyOn(writer, "addReference"); + + annotation.write(writer); + + expect(addReferenceSpy).toHaveBeenCalledWith(reference); + + // Clean up + addReferenceSpy.mockRestore(); + }); + }); + + describe("toString", () => { + it("should return string representation of annotation", () => { + const reference = new ClassReference({ + name: "TestAttribute", + namespace: "Test.Namespace", + }); + + const annotation = new Annotation({ + reference, + argument: '"argumentValue"', + }); + + const result = annotation.toString(); + + expect(result).toContain('TestAttribute("argumentValue")'); + expect(result).toContain("using Test.Namespace;"); + }); + + it("should create writer and call write method", () => { + const reference = new ClassReference({ + name: "TestAttribute", + namespace: "Test.Namespace", + }); + + const annotation = new Annotation({ + reference, + }); + + // Create a spy on the write method + const writeSpy = jest.spyOn(annotation, "write"); + + annotation.toString(); + + // Verify write was called with a Writer instance + expect(writeSpy).toHaveBeenCalledTimes(1); + expect(writeSpy.mock.calls[0][0]).toBeInstanceOf(Writer); + + // Clean up + writeSpy.mockRestore(); + }); + }); +}); diff --git a/libs/csharp-ast/src/lib/ast/Class.spec.ts b/libs/csharp-ast/src/lib/ast/Class.spec.ts new file mode 100644 index 0000000..31c1afd --- /dev/null +++ b/libs/csharp-ast/src/lib/ast/Class.spec.ts @@ -0,0 +1,225 @@ +import { Class } from "./Class"; +import { Access } from "./Access"; +import { Field } from "./Field"; +import { Method } from "./Method"; +import { Parameter } from "./Parameter"; +import { CodeBlock } from "./CodeBlock"; +import { Writer } from "../core/Writer"; +import { Type } from "./Type"; +import { Interface } from "./Interface"; +import { Annotation } from "./Annotation"; +import { ClassReference } from "./ClassReference"; + +describe("Class", () => { + it("should create a class with the correct properties", () => { + const classInstance = new Class({ + name: "TestClass", + namespace: "TestNamespace", + access: Access.Public, + abstract: true, + static_: false, + sealed: false, + partial: true, + }); + + expect(classInstance.name).toBe("TestClass"); + expect(classInstance.namespace).toBe("TestNamespace"); + expect(classInstance.access).toBe(Access.Public); + expect(classInstance.abstract).toBe(true); + expect(classInstance.static_).toBe(false); + expect(classInstance.sealed).toBe(false); + expect(classInstance.partial).toBe(true); + }); + + it("should add fields to the class", () => { + const classInstance = new Class({ + name: "TestClass", + namespace: "TestNamespace", + access: Access.Public, + }); + + const field = new Field({ + name: "testField", + type: Type.string(), + access: Access.Private, + }); + classInstance.addField(field); + + expect(classInstance.getFields()).toContain(field); + }); + + it("should add methods to the class", () => { + const classInstance = new Class({ + name: "TestClass", + namespace: "TestNamespace", + access: Access.Public, + }); + + const method = new Method({ + name: "testMethod", + return_: Type.boolean(), + access: Access.Public, + parameters: [], + isAsync: false, + }); + classInstance.addMethod(method); + + expect(classInstance.getMethods()).toContain(method); + }); + + it("should add constructors to the class", () => { + const classInstance = new Class({ + name: "TestClass", + namespace: "TestNamespace", + access: Access.Public, + }); + + const constructor = { + access: Access.Public, + parameters: [new Parameter({ name: "param1", type: Type.string() })], + body: new CodeBlock({ code: "this.param1 = param1;" }), + }; + classInstance.addConstructor(constructor); + + expect(classInstance["constructors"]).toContain(constructor); + }); + + it("should write the class declaration correctly", () => { + const writer = new Writer({ namespace: "TestNamespace" }); + const classInstance = new Class({ + name: "TestClass", + namespace: "TestNamespace", + access: Access.Public, + }); + + classInstance.write(writer); + + const output = writer.toString(); + expect(output).toContain("namespace TestNamespace;"); + expect(output).toContain("public class TestClass"); + }); + + it("should throw an error if multiple modifiers are conflicting", () => { + expect(() => { + new Class({ + name: "TestClass", + namespace: "TestNamespace", + access: Access.Public, + abstract: true, + static_: true, + }).toString(); + }).toThrowError( + "A class can only be one of abstract, sealed, or static at a time", + ); + }); + + it("should add nested classes to the class", () => { + const parentClass = new Class({ + name: "ParentClass", + namespace: "TestNamespace", + access: Access.Public, + }); + + const nestedClass = new Class({ + name: "NestedClass", + namespace: "TestNamespace", + access: Access.Private, + isNestedClass: true, + }); + + parentClass.addNestedClass(nestedClass); + + expect(parentClass["nestedClasses"]).toContain(nestedClass); + }); + + it("should add nested interfaces to the class", () => { + const parentClass = new Class({ + name: "ParentClass", + namespace: "TestNamespace", + access: Access.Public, + }); + + const nestedInterface = new Interface({ + name: "NestedInterface", + namespace: "TestNamespace", + access: Access.Private, + }); + + parentClass.addNestedInterface(nestedInterface); + + expect(parentClass["nestedInterfaces"]).toContain(nestedInterface); + }); + + it("should add annotations to the class", () => { + const annotation = new Annotation({ + reference: new ClassReference({ + name: "Serializable", + namespace: "System.Runtime.Serialization", + }), + }); + const classInstance = new Class({ + name: "TestClass", + namespace: "TestNamespace", + access: Access.Public, + annotations: [annotation], + }); + + expect(classInstance.annotations).toContain(annotation); + }); + + it("should handle parent class references correctly", () => { + const parentClassReference = new ClassReference({ + name: "BaseClass", + namespace: "BaseNamespace", + }); + + const classInstance = new Class({ + name: "DerivedClass", + namespace: "DerivedNamespace", + access: Access.Public, + parentClassReference, + }); + + expect(classInstance.parentClassReference).toBe(parentClassReference); + }); + + it("should handle interface references correctly", () => { + const interfaceReference = new ClassReference({ + name: "ITestInterface", + namespace: "TestNamespace", + }); + + const classInstance = new Class({ + name: "TestClass", + namespace: "TestNamespace", + access: Access.Public, + interfaceReferences: [interfaceReference], + }); + + expect(classInstance.interfaceReferences).toContain(interfaceReference); + }); + + it("should write annotations correctly", () => { + const writer = new Writer({ namespace: "TestNamespace" }); + const annotation = new Annotation({ + reference: new ClassReference({ + name: "Serializable", + namespace: "System.Runtime.Serialization", + }), + }); + const classInstance = new Class({ + name: "TestClass", + namespace: "TestNamespace", + access: Access.Public, + annotations: [annotation], + }); + + classInstance.write(writer); + + const output = writer.toString(); + expect(output).toContain(`using System.Runtime.Serialization;`); + expect(output).toContain(`namespace TestNamespace;`); + expect(output).toContain(`[Serializable()]`); + expect(output).toContain(`public class TestClass`); + }); +}); diff --git a/libs/csharp-ast/src/lib/ast/ClassInstantiation.spec.ts b/libs/csharp-ast/src/lib/ast/ClassInstantiation.spec.ts new file mode 100644 index 0000000..d90ac4a --- /dev/null +++ b/libs/csharp-ast/src/lib/ast/ClassInstantiation.spec.ts @@ -0,0 +1,121 @@ +import { ClassInstantiation } from "./ClassInstantiation"; +import { ClassReference } from "./ClassReference"; +import { CodeBlock } from "./CodeBlock"; +import { Writer } from "../core/Writer"; + +describe("ClassInstantiation", () => { + let writer: Writer; + + beforeEach(() => { + writer = new Writer({ namespace: "Test.Namespace" }); + }); + + describe("constructor", () => { + it("should initialize with class reference and unnamed arguments", () => { + const classRef = new ClassReference({ + name: "TestClass", + namespace: "Test.Namespace", + }); + + const classInstance = new ClassInstantiation({ + classReference: classRef, + arguments_: [new CodeBlock({ code: '"arg1"' })], + }); + + expect(classInstance.classReference).toBe(classRef); + expect(classInstance.arguments_).toHaveLength(1); + }); + + it("should initialize with class reference and named arguments", () => { + const classRef = new ClassReference({ + name: "TestClass", + namespace: "Test.Namespace", + }); + + const classInstance = new ClassInstantiation({ + classReference: classRef, + arguments_: [ + { + name: "param1", + assignment: new CodeBlock({ code: '"value1"' }), + }, + ], + }); + + expect(classInstance.classReference).toBe(classRef); + expect(classInstance.arguments_).toHaveLength(1); + }); + }); + + describe("write", () => { + it("should write class instantiation with no arguments", () => { + const classRef = new ClassReference({ + name: "TestClass", + namespace: "Test.Namespace", + }); + + const classInstance = new ClassInstantiation({ + classReference: classRef, + arguments_: [], + }); + + classInstance.write(writer); + const output = writer.toString(); + + expect(output).toContain("new TestClass("); + expect(output).toContain(")"); + }); + + it("should write class instantiation with unnamed arguments", () => { + const classRef = new ClassReference({ + name: "TestClass", + namespace: "Test.Namespace", + }); + + const classInstance = new ClassInstantiation({ + classReference: classRef, + arguments_: [ + new CodeBlock({ code: '"arg1"' }), + new CodeBlock({ code: "42" }), + ], + }); + + classInstance.write(writer); + const output = writer.toString(); + + expect(output).toContain("new TestClass("); + expect(output).toContain('"arg1"'); + expect(output).toContain("42"); + expect(output).toContain(")"); + }); + + it("should write class instantiation with named arguments", () => { + const classRef = new ClassReference({ + name: "TestClass", + namespace: "Test.Namespace", + }); + + const classInstance = new ClassInstantiation({ + classReference: classRef, + arguments_: [ + { + name: "param1", + assignment: new CodeBlock({ code: '"value1"' }), + }, + { + name: "param2", + assignment: new CodeBlock({ code: "42" }), + }, + ], + }); + + classInstance.write(writer); + const output = writer.toString(); + + expect(output).toContain("new TestClass("); + expect(output).toContain('param1: "value1"'); + expect(output).toContain("param2: 42"); + expect(output).toContain(")"); + }); + }); +}); diff --git a/libs/csharp-ast/src/lib/ast/ClassReference.spec.ts b/libs/csharp-ast/src/lib/ast/ClassReference.spec.ts new file mode 100644 index 0000000..a720841 --- /dev/null +++ b/libs/csharp-ast/src/lib/ast/ClassReference.spec.ts @@ -0,0 +1,141 @@ +import { + ClassReference, + OneOfClassReference, + StringEnumClassReference, +} from "./ClassReference"; +import { Writer } from "../core/Writer"; + +describe("ClassReference", () => { + describe("constructor", () => { + it("should initialize with name and namespace", () => { + const reference = new ClassReference({ + name: "TestClass", + namespace: "Test.Namespace", + }); + expect(reference.name).toBe("TestClass"); + expect(reference.namespace).toBe("Test.Namespace"); + }); + + it("should allow null namespace", () => { + const reference = new ClassReference({ + name: "TestClass", + namespace: null as any, + }); + expect(reference.name).toBe("TestClass"); + expect(reference.namespace).toBe(null); + }); + }); + + describe("write", () => { + it("should write class name to writer", () => { + const reference = new ClassReference({ + name: "TestClass", + namespace: "Test.Namespace", + }); + const writer = new Writer({}); + + reference.write(writer); + + expect(writer.toString()).toBe("using Test.Namespace;\n\nTestClass"); + }); + + it("should add namespace reference to writer", () => { + const reference = new ClassReference({ + name: "TestClass", + namespace: "Test.Namespace", + }); + const writer = new Writer({}); + + // Create a spy on the addReference method + const addReferenceSpy = jest.spyOn(writer, "addReference"); + + reference.write(writer); + + // Verify addReference was called with the reference + expect(addReferenceSpy).toHaveBeenCalledWith(reference); + + // Clean up + addReferenceSpy.mockRestore(); + }); + + it("should not add reference if namespace is current namespace", () => { + const currentNamespace = "Current.Namespace"; + const reference = new ClassReference({ + name: "TestClass", + namespace: currentNamespace, + }); + const writer = new Writer({ namespace: currentNamespace }); + + reference.write(writer); + + // Should only write the class name without the namespace import + expect(writer.toString()).toBe("TestClass"); + }); + + it("should handle null namespace", () => { + const reference = new ClassReference({ + name: "TestClass", + namespace: null as any, + }); + const writer = new Writer({}); + + reference.write(writer); + + // Should only write the class name without any namespace import + expect(writer.toString()).toBe("TestClass"); + }); + }); + + describe("toString", () => { + it("should return class name with namespace reference", () => { + const reference = new ClassReference({ + name: "TestClass", + namespace: "Test.Namespace", + }); + const result = reference.toString(); + + expect(result).toBe("using Test.Namespace;\n\nTestClass"); + }); + + it("should create writer and call write method", () => { + const reference = new ClassReference({ + name: "TestClass", + namespace: "Test.Namespace", + }); + + // Create a spy on the write method + const writeSpy = jest.spyOn(reference, "write"); + + reference.toString(); + + // Verify write was called with a Writer instance + expect(writeSpy).toHaveBeenCalledTimes(1); + expect(writeSpy.mock.calls[0][0]).toBeInstanceOf(Writer); + + // Clean up + writeSpy.mockRestore(); + }); + }); + + describe("predefined references", () => { + it("should expose OneOfClassReference", () => { + expect(OneOfClassReference).toBeInstanceOf(ClassReference); + expect(OneOfClassReference.name).toBe("OneOf"); + expect(OneOfClassReference.namespace).toBe("OneOf"); + }); + + it("should expose StringEnumClassReference", () => { + expect(StringEnumClassReference).toBeInstanceOf(ClassReference); + expect(StringEnumClassReference.name).toBe("StringEnum"); + expect(StringEnumClassReference.namespace).toBe("StringEnum"); + }); + + it("should write predefined references correctly", () => { + const writer = new Writer({}); + + OneOfClassReference.write(writer); + + expect(writer.toString()).toBe("using OneOf;\n\nOneOf"); + }); + }); +}); diff --git a/libs/csharp-ast/src/lib/ast/CodeBlock.spec.ts b/libs/csharp-ast/src/lib/ast/CodeBlock.spec.ts new file mode 100644 index 0000000..e4a3e9c --- /dev/null +++ b/libs/csharp-ast/src/lib/ast/CodeBlock.spec.ts @@ -0,0 +1,183 @@ +import { CodeBlock } from "./CodeBlock"; +import { Writer } from "../core/Writer"; +import { ClassReference } from "./ClassReference"; + +describe("CodeBlock", () => { + describe("constructor", () => { + it("should initialize with string code", () => { + const codeBlock = new CodeBlock({ + code: "return true;", + }); + + expect(codeBlock["value"]).toBe("return true;"); + expect(codeBlock["references"]).toEqual([]); + }); + + it("should initialize with function code", () => { + const codeFunc = (writer: Writer) => { + writer.write("return true;"); + }; + + const codeBlock = new CodeBlock({ + code: codeFunc, + }); + + expect(codeBlock["value"]).toBe(codeFunc); + expect(codeBlock["references"]).toEqual([]); + }); + + it("should initialize with references", () => { + const references = [ + new ClassReference({ + name: "TestClass", + namespace: "Test.Namespace", + }), + ]; + + const codeBlock = new CodeBlock({ + code: "var test = new TestClass();", + references, + }); + + expect(codeBlock["references"]).toEqual(references); + }); + + it("should handle null references", () => { + const codeBlock = new CodeBlock({ + code: "return true;", + references: null, + }); + + expect(codeBlock["references"]).toEqual([]); + }); + }); + + describe("write", () => { + it("should write string code to writer", () => { + const codeBlock = new CodeBlock({ + code: "return true;", + }); + + const writer = new Writer({}); + codeBlock.write(writer); + + expect(writer.toString()).toBe("return true;"); + }); + + it("should execute function code with writer", () => { + const codeFunc = (writer: Writer) => { + writer.writeLine("if (condition) {"); + writer.indent(); + writer.writeLine("return true;"); + writer.dedent(); + writer.write("}"); + }; + + const codeBlock = new CodeBlock({ + code: codeFunc, + }); + + const writer = new Writer({}); + codeBlock.write(writer); + + const result = writer.toString(); + expect(result).toContain("if (condition) {"); + expect(result).toContain(" return true;"); + expect(result).toContain("}"); + }); + + it("should add references to writer", () => { + const reference = new ClassReference({ + name: "TestClass", + namespace: "Test.Namespace", + }); + + const codeBlock = new CodeBlock({ + code: "var test = new TestClass();", + references: [reference], + }); + + const writer = new Writer({}); + + // Create a spy on addReference + const addReferenceSpy = jest.spyOn(writer, "addReference"); + + codeBlock.write(writer); + + expect(addReferenceSpy).toHaveBeenCalledWith(reference); + expect(writer.toString()).toContain("var test = new TestClass();"); + expect(writer.toString()).toContain("using Test.Namespace;"); + + // Clean up + addReferenceSpy.mockRestore(); + }); + + it("should handle multiple references", () => { + const ref1 = new ClassReference({ + name: "FirstClass", + namespace: "First.Namespace", + }); + + const ref2 = new ClassReference({ + name: "SecondClass", + namespace: "Second.Namespace", + }); + + const codeBlock = new CodeBlock({ + code: "var first = new FirstClass(); var second = new SecondClass();", + references: [ref1, ref2], + }); + + const writer = new Writer({}); + codeBlock.write(writer); + + const result = writer.toString(); + expect(result).toContain("using First.Namespace;"); + expect(result).toContain("using Second.Namespace;"); + expect(result).toContain( + "var first = new FirstClass(); var second = new SecondClass();", + ); + }); + }); + + describe("toString", () => { + it("should return string representation of code block", () => { + const codeBlock = new CodeBlock({ + code: "return true;", + }); + + expect(codeBlock.toString()).toBe("return true;"); + }); + + it("should create writer and call write method", () => { + const codeBlock = new CodeBlock({ + code: "return true;", + }); + + // Create a spy on the write method + const writeSpy = jest.spyOn(codeBlock, "write"); + + codeBlock.toString(); + + // Verify write was called with a Writer instance + expect(writeSpy).toHaveBeenCalledTimes(1); + expect(writeSpy.mock.calls[0][0]).toBeInstanceOf(Writer); + + // Clean up + writeSpy.mockRestore(); + }); + + it("should handle function code in toString", () => { + const codeBlock = new CodeBlock({ + code: (writer: Writer) => { + writer.writeLine("line 1"); + writer.writeLine("line 2"); + }, + }); + + const result = codeBlock.toString(); + expect(result).toContain("line 1"); + expect(result).toContain("line 2"); + }); + }); +}); diff --git a/libs/csharp-ast/src/lib/ast/CoreClassReference.spec.ts b/libs/csharp-ast/src/lib/ast/CoreClassReference.spec.ts new file mode 100644 index 0000000..ffbb2ca --- /dev/null +++ b/libs/csharp-ast/src/lib/ast/CoreClassReference.spec.ts @@ -0,0 +1,39 @@ +import { CoreClassReference } from "./CoreClassReference"; +import { Writer } from "../core/Writer"; + +describe("CoreClassReference", () => { + let writer: Writer; + + beforeEach(() => { + writer = new Writer({ namespace: "Test.Namespace" }); + }); + + describe("constructor", () => { + it("should initialize with a name", () => { + const coreRef = new CoreClassReference({ name: "string" }); + expect(coreRef.name).toBe("string"); + }); + }); + + describe("write", () => { + it("should write a core class reference", () => { + const coreRef = new CoreClassReference({ name: "string" }); + coreRef.write(writer); + expect(writer.toString()).toBe("string"); + }); + + it("should write a core class reference with a different name", () => { + const coreRef = new CoreClassReference({ name: "int" }); + coreRef.write(writer); + expect(writer.toString()).toBe("int"); + }); + + it("should not add any using statements", () => { + const coreRef = new CoreClassReference({ name: "object" }); + coreRef.write(writer); + const output = writer.toString(); + expect(output).not.toContain("using"); + expect(output).toBe("object"); + }); + }); +}); diff --git a/libs/csharp-ast/src/lib/ast/Dictionary.spec.ts b/libs/csharp-ast/src/lib/ast/Dictionary.spec.ts new file mode 100644 index 0000000..b1f5f89 --- /dev/null +++ b/libs/csharp-ast/src/lib/ast/Dictionary.spec.ts @@ -0,0 +1,94 @@ +import { Dictionary } from "./Dictionary"; +import { CodeBlock } from "./CodeBlock"; +import { Writer } from "../core/Writer"; +import { Type } from "./Type"; + +describe("Dictionary", () => { + let writer: Writer; + + beforeEach(() => { + writer = new Writer({ namespace: "Test.Namespace" }); + }); + + describe("constructor", () => { + it("should initialize with key type, value type, and entries", () => { + const dictionary = new Dictionary({ + keyType: Type.string(), + valueType: Type.integer(), + entries: [ + { + key: new CodeBlock({ code: '"key1"' }), + value: new CodeBlock({ code: "1" }), + }, + ], + }); + + expect(dictionary).toBeDefined(); + }); + }); + + describe("write", () => { + it("should write an empty dictionary", () => { + const dictionary = new Dictionary({ + keyType: Type.string(), + valueType: Type.integer(), + entries: [], + }); + + dictionary.write(writer); + const output = writer.toString(); + + expect(output).toContain("new Dictionary {"); + expect(output).toContain("}"); + }); + + it("should write a dictionary with multiple entries", () => { + const dictionary = new Dictionary({ + keyType: Type.string(), + valueType: Type.integer(), + entries: [ + { + key: new CodeBlock({ code: '"key1"' }), + value: new CodeBlock({ code: "1" }), + }, + { + key: new CodeBlock({ code: '"key2"' }), + value: new CodeBlock({ code: "2" }), + }, + ], + }); + + dictionary.write(writer); + const output = writer.toString(); + + expect(output).toContain("new Dictionary {"); + expect(output).toContain('{ "key1", 1 }'); + expect(output).toContain('{ "key2", 2 }'); + expect(output).toContain("}"); + }); + + it("should write a dictionary with complex types", () => { + const dictionary = new Dictionary({ + keyType: Type.string(), + valueType: Type.list(Type.string()), + entries: [ + { + key: new CodeBlock({ code: '"key1"' }), + value: new CodeBlock({ + code: 'new List() { "value1", "value2" }', + }), + }, + ], + }); + + dictionary.write(writer); + const output = writer.toString(); + + expect(output).toContain("new Dictionary> {"); + expect(output).toContain( + '{ "key1", new List() { "value1", "value2" } }', + ); + expect(output).toContain("}"); + }); + }); +}); diff --git a/libs/csharp-ast/src/lib/ast/Enum.spec.ts b/libs/csharp-ast/src/lib/ast/Enum.spec.ts new file mode 100644 index 0000000..78c46eb --- /dev/null +++ b/libs/csharp-ast/src/lib/ast/Enum.spec.ts @@ -0,0 +1,240 @@ +import { Enum } from "./Enum"; +import { Access } from "./Access"; +import { Writer } from "../core/Writer"; +import { ClassReference } from "./ClassReference"; +import { Annotation } from "./Annotation"; + +describe("Enum", () => { + describe("constructor", () => { + it("should initialize with name, namespace, and access", () => { + const enumDecl = new Enum({ + name: "TestEnum", + namespace: "Test.Namespace", + access: Access.Public, + }); + + expect(enumDecl.name).toBe("TestEnum"); + expect(enumDecl.namespace).toBe("Test.Namespace"); + expect(enumDecl.access).toBe("public"); + }); + + it("should create a class reference", () => { + const enumDecl = new Enum({ + name: "TestEnum", + namespace: "Test.Namespace", + access: Access.Public, + }); + + expect(enumDecl.reference).toBeInstanceOf(ClassReference); + expect(enumDecl.reference.name).toBe("TestEnum"); + expect(enumDecl.reference.namespace).toBe("Test.Namespace"); + }); + + it("should initialize with annotations", () => { + const annotation = new Annotation({ + reference: new ClassReference({ + name: "TestAttribute", + namespace: "Test.Attributes", + }), + }); + + const enumDecl = new Enum({ + name: "TestEnum", + namespace: "Test.Namespace", + access: Access.Public, + annotations: [annotation], + }); + + // The annotations array is private, so we can verify it in write method tests + expect(enumDecl["annotations"]).toEqual([annotation]); + }); + + it("should initialize with empty members array", () => { + const enumDecl = new Enum({ + name: "TestEnum", + namespace: "Test.Namespace", + access: Access.Public, + }); + + expect(enumDecl["fields"]).toEqual([]); + }); + }); + + describe("addMember", () => { + it("should add a member to the enum", () => { + const enumDecl = new Enum({ + name: "TestEnum", + namespace: "Test.Namespace", + access: Access.Public, + }); + + enumDecl.addMember({ + name: "FirstValue", + value: "first", + }); + + expect(enumDecl["fields"]).toHaveLength(1); + expect(enumDecl["fields"][0].name).toBe("FirstValue"); + + // The value is converted to an annotation with EnumMember + expect(enumDecl["fields"][0].value).toBeInstanceOf(Annotation); + }); + + it("should add multiple members", () => { + const enumDecl = new Enum({ + name: "TestEnum", + namespace: "Test.Namespace", + access: Access.Public, + }); + + enumDecl.addMember({ name: "First", value: "first" }); + enumDecl.addMember({ name: "Second", value: "second" }); + enumDecl.addMember({ name: "Third", value: "third" }); + + expect(enumDecl["fields"]).toHaveLength(3); + expect(enumDecl["fields"][0].name).toBe("First"); + expect(enumDecl["fields"][1].name).toBe("Second"); + expect(enumDecl["fields"][2].name).toBe("Third"); + }); + }); + + describe("write", () => { + it("should write an empty enum declaration", () => { + const enumDecl = new Enum({ + name: "TestEnum", + namespace: "Test.Namespace", + access: Access.Public, + }); + + const writer = new Writer({}); + enumDecl.write(writer); + + const result = writer.toString(); + expect(result).toContain("namespace Test.Namespace;"); + expect(result).toContain("public enum TestEnum"); + expect(result).toContain("{"); + expect(result).toContain("}"); + }); + + it("should write an enum with members", () => { + const enumDecl = new Enum({ + name: "TestEnum", + namespace: "Test.Namespace", + access: Access.Public, + }); + + enumDecl.addMember({ name: "First", value: "first" }); + enumDecl.addMember({ name: "Second", value: "second" }); + + const writer = new Writer({}); + enumDecl.write(writer); + + const result = writer.toString(); + expect(result).toContain('[EnumMember(Value = "first")]'); + expect(result).toContain("First,"); + expect(result).toContain('[EnumMember(Value = "second")]'); + expect(result).toContain("Second"); + expect(result).toContain("using System.Runtime.Serialization;"); + }); + + it("should write an enum with annotations", () => { + const annotation = new Annotation({ + reference: new ClassReference({ + name: "TestAttribute", + namespace: "Test.Attributes", + }), + }); + + const enumDecl = new Enum({ + name: "TestEnum", + namespace: "Test.Namespace", + access: Access.Private, + annotations: [annotation], + }); + + const writer = new Writer({}); + enumDecl.write(writer); + + const result = writer.toString(); + expect(result).toContain("[TestAttribute()]"); + expect(result).toContain("using Test.Attributes;"); + expect(result).toContain("private enum TestEnum"); + }); + + it("should use different access modifiers", () => { + const publicEnum = new Enum({ + name: "PublicEnum", + namespace: "Test.Namespace", + access: Access.Public, + }); + + const privateEnum = new Enum({ + name: "PrivateEnum", + namespace: "Test.Namespace", + access: Access.Private, + }); + + const protectedEnum = new Enum({ + name: "ProtectedEnum", + namespace: "Test.Namespace", + access: Access.Protected, + }); + + const publicWriter = new Writer({}); + publicEnum.write(publicWriter); + expect(publicWriter.toString()).toContain("public enum PublicEnum"); + + const privateWriter = new Writer({}); + privateEnum.write(privateWriter); + expect(privateWriter.toString()).toContain("private enum PrivateEnum"); + + const protectedWriter = new Writer({}); + protectedEnum.write(protectedWriter); + expect(protectedWriter.toString()).toContain( + "protected enum ProtectedEnum", + ); + }); + }); + + describe("toString", () => { + it("should return the string representation of the enum", () => { + const enumDecl = new Enum({ + name: "TestEnum", + namespace: "Test.Namespace", + access: Access.Public, + }); + + enumDecl.addMember({ name: "First", value: "first" }); + enumDecl.addMember({ name: "Second", value: "second" }); + + const result = enumDecl.toString(); + + expect(result).toContain("namespace Test.Namespace;"); + expect(result).toContain("public enum TestEnum"); + expect(result).toContain('[EnumMember(Value = "first")]'); + expect(result).toContain("First,"); + expect(result).toContain('[EnumMember(Value = "second")]'); + expect(result).toContain("Second"); + }); + + it("should create writer and call write method", () => { + const enumDecl = new Enum({ + name: "TestEnum", + namespace: "Test.Namespace", + access: Access.Public, + }); + + // Create a spy on the write method + const writeSpy = jest.spyOn(enumDecl, "write"); + + enumDecl.toString(); + + // Verify write was called with a Writer instance + expect(writeSpy).toHaveBeenCalledTimes(1); + expect(writeSpy.mock.calls[0][0]).toBeInstanceOf(Writer); + + // Clean up + writeSpy.mockRestore(); + }); + }); +}); diff --git a/libs/csharp-ast/src/lib/ast/Field.spec.ts b/libs/csharp-ast/src/lib/ast/Field.spec.ts new file mode 100644 index 0000000..dcb9a2e --- /dev/null +++ b/libs/csharp-ast/src/lib/ast/Field.spec.ts @@ -0,0 +1,91 @@ +import { Field } from "./Field"; +import { ClassReference } from "./ClassReference"; +import { CodeBlock } from "./CodeBlock"; +import { Writer } from "../core/Writer"; +import { Type } from "./Type"; +import { Access } from "./Access"; +import { Annotation } from "./Annotation"; + +describe("Field", () => { + let writer: Writer; + + beforeEach(() => { + writer = new Writer({ namespace: "Test.Namespace" }); + }); + + describe("constructor", () => { + it("should initialize with given arguments", () => { + const args = { + name: "testField", + type: Type.string(), + access: Access.Public, + readonly_: true, + annotations: [ + new Annotation({ + reference: new ClassReference({ + name: "TestAnnotation", + namespace: "Test.Namespace", + }), + }), + ], + initializer: new CodeBlock({ code: '"default"' }), + summary: "This is a test field", + jsonPropertyName: "test_field", + }; + const field = new Field(args); + + expect(field.name).toBe("testField"); + expect(field.access).toBe(Access.Public); + expect(field.readonly_).toBe(true); + // We'll verify annotations through the write method instead + }); + }); + + describe("write", () => { + it("should write a field with all properties", () => { + const args = { + name: "testField", + type: Type.string(), + access: Access.Public, + readonly_: true, + annotations: [ + new Annotation({ + reference: new ClassReference({ + name: "TestAnnotation", + namespace: "Test.Namespace", + }), + }), + ], + initializer: new CodeBlock({ code: '"default"' }), + summary: "This is a test field", + jsonPropertyName: "test_field", + }; + const field = new Field(args); + + field.write(writer); + const output = writer.toString(); + + expect(output).toContain("/// "); + expect(output).toContain("/// This is a test field"); + expect(output).toContain("[TestAnnotation()]"); + expect(output).toContain('[JsonPropertyName("test_field")]'); + expect(output).toContain('public readonly string testField = "default";'); + }); + + it("should write a field with accessors", () => { + const args = { + name: "testField", + type: Type.string(), + access: Access.Private, + get: true, + set: true, + }; + const field = new Field(args); + + field.write(writer); + const output = writer.toString(); + + expect(output).toContain("private string testField { get; set; }"); + }); + }); +}); diff --git a/libs/csharp-ast/src/lib/ast/GenericClassReference.spec.ts b/libs/csharp-ast/src/lib/ast/GenericClassReference.spec.ts new file mode 100644 index 0000000..5febd4b --- /dev/null +++ b/libs/csharp-ast/src/lib/ast/GenericClassReference.spec.ts @@ -0,0 +1,90 @@ +import { GenericClassReference } from "./GenericClassReference"; +import { ClassReference } from "./ClassReference"; +import { Writer } from "../core/Writer"; +import { Type } from "./Type"; + +describe("GenericClassReference", () => { + let writer: Writer; + + beforeEach(() => { + writer = new Writer({ namespace: "Test.Namespace" }); + }); + + describe("constructor", () => { + it("should initialize with a reference and inner type", () => { + const listRef = new ClassReference({ + name: "List", + namespace: "System.Collections.Generic", + }); + const intType = Type.integer(); + + const genericRef = new GenericClassReference({ + reference: listRef, + innerType: intType, + }); + + expect(genericRef).toBeDefined(); + }); + }); + + describe("write", () => { + it("should write a generic class reference with a simple type parameter", () => { + const listRef = new ClassReference({ + name: "List", + namespace: "System.Collections.Generic", + }); + const intType = Type.integer(); + + const genericRef = new GenericClassReference({ + reference: listRef, + innerType: intType, + }); + + genericRef.write(writer); + const output = writer.toString(); + + expect(output).toContain("using System.Collections.Generic;"); + expect(output).toContain("List"); + }); + + it("should write a generic class reference with a complex type parameter", () => { + const dictionaryRef = new ClassReference({ + name: "Dictionary", + namespace: "System.Collections.Generic", + }); + const keyValuePairType = Type.map(Type.string(), Type.integer()); + + const genericRef = new GenericClassReference({ + reference: dictionaryRef, + innerType: keyValuePairType, + }); + + genericRef.write(writer); + const output = writer.toString(); + + expect(output).toContain("using System.Collections.Generic;"); + expect(output).toContain("Dictionary>"); + }); + + it("should handle nested generic class references", () => { + const listRef = new ClassReference({ + name: "List", + namespace: "System.Collections.Generic", + }); + + // Create List> + const innerListType = Type.list(Type.integer()); + + const genericRef = new GenericClassReference({ + reference: listRef, + innerType: innerListType, + }); + + genericRef.write(writer); + const output = writer.toString(); + + expect(output).toContain("using System.Collections.Generic;"); + expect(output).toContain("List>"); + }); + }); +}); diff --git a/libs/csharp-ast/src/lib/ast/Interface.spec.ts b/libs/csharp-ast/src/lib/ast/Interface.spec.ts new file mode 100644 index 0000000..c5416cf --- /dev/null +++ b/libs/csharp-ast/src/lib/ast/Interface.spec.ts @@ -0,0 +1,410 @@ +import { Interface } from "./Interface"; +import { Access } from "./Access"; +import { Writer } from "../core/Writer"; +import { Field } from "./Field"; +import { Method, MethodClassType } from "./Method"; +import { Type } from "./Type"; +import { Parameter } from "./Parameter"; + +describe("Interface", () => { + describe("constructor", () => { + it("should initialize with name, namespace, and access", () => { + const iface = new Interface({ + name: "ITestInterface", + namespace: "Test.Namespace", + access: Access.Public, + }); + + expect(iface.name).toBe("ITestInterface"); + expect(iface.namespace).toBe("Test.Namespace"); + expect(iface.access).toBe("public"); + expect(iface.partial).toBe(false); + expect(iface.isNestedInterface).toBe(false); + }); + + it("should initialize with partial flag", () => { + const iface = new Interface({ + name: "ITestInterface", + namespace: "Test.Namespace", + access: Access.Public, + partial: true, + }); + + expect(iface.partial).toBe(true); + }); + + it("should initialize with isNestedInterface flag", () => { + const iface = new Interface({ + name: "ITestInterface", + namespace: "Test.Namespace", + access: Access.Public, + isNestedInterface: true, + }); + + expect(iface.isNestedInterface).toBe(true); + }); + + it("should create a reference for this interface", () => { + const iface = new Interface({ + name: "ITestInterface", + namespace: "Test.Namespace", + access: Access.Public, + }); + + expect(iface.reference).toBeDefined(); + expect(iface.reference.name).toBe("ITestInterface"); + expect(iface.reference.namespace).toBe("Test.Namespace"); + }); + + it("should initialize with empty fields and methods", () => { + const iface = new Interface({ + name: "ITestInterface", + namespace: "Test.Namespace", + access: Access.Public, + }); + + expect(iface["fields"]).toEqual([]); + expect(iface["methods"]).toEqual([]); + }); + }); + + describe("addField", () => { + it("should add a field to the interface", () => { + const iface = new Interface({ + name: "ITestInterface", + namespace: "Test.Namespace", + access: Access.Public, + }); + + const field = new Field({ + name: "TestField", + type: Type.string(), + access: Access.Public, + }); + + iface.addField(field); + + expect(iface["fields"]).toHaveLength(1); + expect(iface["fields"][0]).toBe(field); + }); + + it("should add multiple fields", () => { + const iface = new Interface({ + name: "ITestInterface", + namespace: "Test.Namespace", + access: Access.Public, + }); + + const field1 = new Field({ + name: "Field1", + type: Type.string(), + access: Access.Public, + }); + + const field2 = new Field({ + name: "Field2", + type: Type.integer(), + access: Access.Public, + }); + + iface.addField(field1); + iface.addField(field2); + + expect(iface["fields"]).toHaveLength(2); + expect(iface["fields"][0]).toBe(field1); + expect(iface["fields"][1]).toBe(field2); + }); + }); + + describe("addMethod", () => { + it("should add a method to the interface", () => { + const iface = new Interface({ + name: "ITestInterface", + namespace: "Test.Namespace", + access: Access.Public, + }); + + const method = new Method({ + name: "TestMethod", + access: Access.Public, + isAsync: false, + parameters: [], + }); + + iface.addMethod(method); + + expect(iface["methods"]).toHaveLength(1); + expect(iface["methods"][0]).toBe(method); + }); + + it("should set the method's classType to INTERFACE", () => { + const iface = new Interface({ + name: "ITestInterface", + namespace: "Test.Namespace", + access: Access.Public, + }); + + const method = new Method({ + name: "TestMethod", + access: Access.Public, + isAsync: false, + parameters: [], + }); + + expect(method.classType).toBe(MethodClassType.CLASS); + + iface.addMethod(method); + + expect(method.classType).toBe(MethodClassType.INTERFACE); + }); + + it("should add methods with parameters", () => { + const iface = new Interface({ + name: "ITestInterface", + namespace: "Test.Namespace", + access: Access.Public, + }); + + const param = new Parameter({ + name: "param", + type: Type.string(), + }); + + const method = new Method({ + name: "TestMethod", + access: Access.Public, + isAsync: false, + parameters: [param], + }); + + iface.addMethod(method); + + expect(iface["methods"]).toHaveLength(1); + expect(iface["methods"][0].getParameters()).toEqual([param]); + }); + }); + + describe("getMethods", () => { + it("should return all methods", () => { + const iface = new Interface({ + name: "ITestInterface", + namespace: "Test.Namespace", + access: Access.Public, + }); + + const method1 = new Method({ + name: "Method1", + access: Access.Public, + isAsync: false, + parameters: [], + }); + + const method2 = new Method({ + name: "Method2", + access: Access.Public, + isAsync: false, + parameters: [], + }); + + iface.addMethod(method1); + iface.addMethod(method2); + + const methods = iface.getMethods(); + + expect(methods).toHaveLength(2); + expect(methods).toContain(method1); + expect(methods).toContain(method2); + }); + }); + + describe("write", () => { + it("should write a basic interface declaration", () => { + const iface = new Interface({ + name: "ITestInterface", + namespace: "Test.Namespace", + access: Access.Public, + }); + + const writer = new Writer({}); + iface.write(writer); + + const result = writer.toString(); + expect(result).toContain("namespace Test.Namespace;"); + expect(result).toContain("public interface ITestInterface"); + expect(result).toContain("{"); + expect(result).toContain("}"); + }); + + it("should write a partial interface", () => { + const iface = new Interface({ + name: "ITestInterface", + namespace: "Test.Namespace", + access: Access.Public, + partial: true, + }); + + const writer = new Writer({}); + iface.write(writer); + + const result = writer.toString(); + expect(result).toContain("public partial interface ITestInterface"); + }); + + it("should not write namespace for nested interface", () => { + const iface = new Interface({ + name: "INestedInterface", + namespace: "Test.Namespace", + access: Access.Public, + isNestedInterface: true, + }); + + const writer = new Writer({}); + iface.write(writer); + + const result = writer.toString(); + expect(result).not.toContain("namespace Test.Namespace;"); + expect(result).toContain("public interface INestedInterface"); + }); + + it("should write interface with fields", () => { + const iface = new Interface({ + name: "ITestInterface", + namespace: "Test.Namespace", + access: Access.Public, + }); + + const field = new Field({ + name: "TestField", + type: Type.string(), + access: Access.Public, + }); + + iface.addField(field); + + const writer = new Writer({}); + iface.write(writer); + + const result = writer.toString(); + expect(result).toContain("public string TestField"); + }); + + it("should write interface with methods", () => { + const iface = new Interface({ + name: "ITestInterface", + namespace: "Test.Namespace", + access: Access.Public, + }); + + const method = new Method({ + name: "TestMethod", + access: Access.Public, + isAsync: false, + parameters: [], + return_: Type.string(), + }); + + iface.addMethod(method); + + const writer = new Writer({}); + iface.write(writer); + + const result = writer.toString(); + expect(result).toContain("public string TestMethod();"); + const methodLine = result + .split("\n") + .find((line) => line.trim().startsWith("public string TestMethod")); + expect(methodLine).not.toContain("{"); + expect(methodLine).not.toContain("}"); + }); + + it("should write interface with methods and parameters", () => { + const iface = new Interface({ + name: "ITestInterface", + namespace: "Test.Namespace", + access: Access.Public, + }); + + const param = new Parameter({ + name: "param", + type: Type.string(), + }); + + const method = new Method({ + name: "TestMethod", + access: Access.Public, + isAsync: false, + parameters: [param], + return_: Type.string(), + }); + + iface.addMethod(method); + + const writer = new Writer({}); + iface.write(writer); + + const result = writer.toString(); + expect(result).toContain("public string TestMethod(string param);"); + }); + + it("should handle different access modifiers", () => { + const publicIface = new Interface({ + name: "IPublicInterface", + namespace: "Test.Namespace", + access: Access.Public, + }); + + const privateIface = new Interface({ + name: "IPrivateInterface", + namespace: "Test.Namespace", + access: Access.Private, + }); + + const publicWriter = new Writer({}); + publicIface.write(publicWriter); + expect(publicWriter.toString()).toContain( + "public interface IPublicInterface", + ); + + const privateWriter = new Writer({}); + privateIface.write(privateWriter); + expect(privateWriter.toString()).toContain( + "private interface IPrivateInterface", + ); + }); + }); + + describe("toString", () => { + it("should return string representation of interface", () => { + const iface = new Interface({ + name: "ITestInterface", + namespace: "Test.Namespace", + access: Access.Public, + }); + + const result = iface.toString(); + + expect(result).toContain("namespace Test.Namespace;"); + expect(result).toContain("public interface ITestInterface"); + }); + + it("should create writer and call write method", () => { + const iface = new Interface({ + name: "ITestInterface", + namespace: "Test.Namespace", + access: Access.Public, + }); + + // Create a spy on the write method + const writeSpy = jest.spyOn(iface, "write"); + + iface.toString(); + + // Verify write was called with a Writer instance + expect(writeSpy).toHaveBeenCalledTimes(1); + expect(writeSpy.mock.calls[0][0]).toBeInstanceOf(Writer); + + // Clean up + writeSpy.mockRestore(); + }); + }); +}); diff --git a/libs/csharp-ast/src/lib/ast/Interface.ts b/libs/csharp-ast/src/lib/ast/Interface.ts index b90b342..a48d437 100644 --- a/libs/csharp-ast/src/lib/ast/Interface.ts +++ b/libs/csharp-ast/src/lib/ast/Interface.ts @@ -120,9 +120,7 @@ export class Interface extends AstNode { field.write(writer); writer.writeLine(""); } - writer.dedent(); - writer.indent(); for (const method of this.methods) { method.write(writer); writer.writeLine(""); diff --git a/libs/csharp-ast/src/lib/ast/Method.spec.ts b/libs/csharp-ast/src/lib/ast/Method.spec.ts new file mode 100644 index 0000000..d1977b3 --- /dev/null +++ b/libs/csharp-ast/src/lib/ast/Method.spec.ts @@ -0,0 +1,500 @@ +import { Method, MethodType, MethodClassType } from "./Method"; +import { Access } from "./Access"; +import { Writer } from "../core/Writer"; +import { ClassReference } from "./ClassReference"; +import { Parameter } from "./Parameter"; +import { Type } from "./Type"; +import { CodeBlock } from "./CodeBlock"; +import { Annotation } from "./Annotation"; +import { MethodInvocation } from "./MethodInvocation"; + +describe("Method", () => { + describe("constructor", () => { + it("should initialize with basic properties", () => { + const method = new Method({ + name: "TestMethod", + access: Access.Public, + isAsync: false, + parameters: [], + }); + + expect(method.name).toBe("TestMethod"); + expect(method.access).toBe("public"); + expect(method.isAsync).toBe(false); + expect(method.type).toBe(MethodType.INSTANCE); + expect(method.classType).toBe(MethodClassType.CLASS); + expect(method.getParameters()).toEqual([]); + }); + + it("should initialize with return type", () => { + const returnType = Type.string(); + const method = new Method({ + name: "TestMethod", + access: Access.Public, + isAsync: false, + parameters: [], + return_: returnType, + }); + + expect(method.return).toBe(returnType); + }); + + it("should initialize with parameters", () => { + const param1 = new Parameter({ + name: "param1", + type: Type.string(), + }); + + const param2 = new Parameter({ + name: "param2", + type: Type.integer(), + }); + + const method = new Method({ + name: "TestMethod", + access: Access.Public, + isAsync: false, + parameters: [param1, param2], + }); + + expect(method.getParameters()).toEqual([param1, param2]); + }); + + it("should initialize with body", () => { + const body = new CodeBlock({ + code: "return true;", + }); + + const method = new Method({ + name: "TestMethod", + access: Access.Public, + isAsync: false, + parameters: [], + body, + }); + + expect(method.body).toBe(body); + }); + + it("should initialize with class reference", () => { + const classRef = new ClassReference({ + name: "TestClass", + namespace: "Test.Namespace", + }); + + const method = new Method({ + name: "TestMethod", + access: Access.Public, + isAsync: false, + parameters: [], + classReference: classRef, + }); + + expect(method.reference).toBe(classRef); + }); + + it("should initialize with summary", () => { + const summary = "This is a test method."; + + const method = new Method({ + name: "TestMethod", + access: Access.Public, + isAsync: false, + parameters: [], + summary, + }); + + expect(method.summary).toBe(summary); + }); + + it("should initialize with method type", () => { + const method = new Method({ + name: "TestMethod", + access: Access.Public, + isAsync: false, + parameters: [], + type: MethodType.STATIC, + }); + + expect(method.type).toBe(MethodType.STATIC); + }); + + it("should initialize with annotations", () => { + const annotation = new Annotation({ + reference: new ClassReference({ + name: "TestAttribute", + namespace: "Test.Attributes", + }), + }); + + const method = new Method({ + name: "TestMethod", + access: Access.Public, + isAsync: false, + parameters: [], + annotations: [annotation], + }); + + expect(method.annotations).toEqual([annotation]); + }); + + it("should initialize with extension parameter", () => { + const extensionParam = new Parameter({ + name: "this", + type: Type.string(), + }); + + const method = new Method({ + name: "TestMethod", + access: Access.Public, + isAsync: false, + parameters: [], + extensionParameter: extensionParam, + }); + + expect(method["extensionParameter"]).toBe(extensionParam); + }); + }); + + describe("addParameter", () => { + it("should add a parameter to the method", () => { + const method = new Method({ + name: "TestMethod", + access: Access.Public, + isAsync: false, + parameters: [], + }); + + const param = new Parameter({ + name: "param1", + type: Type.string(), + }); + + method.addParameter(param); + + expect(method.getParameters()).toContain(param); + }); + + it("should add multiple parameters", () => { + const method = new Method({ + name: "TestMethod", + access: Access.Public, + isAsync: false, + parameters: [], + }); + + const param1 = new Parameter({ + name: "param1", + type: Type.string(), + }); + + const param2 = new Parameter({ + name: "param2", + type: Type.integer(), + }); + + method.addParameter(param1); + method.addParameter(param2); + + expect(method.getParameters()).toEqual([param1, param2]); + }); + }); + + describe("write", () => { + it("should write a basic method", () => { + const method = new Method({ + name: "TestMethod", + access: Access.Public, + isAsync: false, + parameters: [], + }); + + const writer = new Writer({}); + method.write(writer); + + const result = writer.toString(); + expect(result).toContain("public void TestMethod()"); + expect(result).toContain("{"); + expect(result).toContain("}"); + }); + + it("should write a method with return type", () => { + const method = new Method({ + name: "TestMethod", + access: Access.Public, + isAsync: false, + parameters: [], + return_: Type.string(), + }); + + const writer = new Writer({}); + method.write(writer); + + const result = writer.toString(); + expect(result).toContain("public string TestMethod()"); + }); + + it("should write a static method", () => { + const method = new Method({ + name: "TestMethod", + access: Access.Public, + isAsync: false, + parameters: [], + type: MethodType.STATIC, + }); + + const writer = new Writer({}); + method.write(writer); + + const result = writer.toString(); + expect(result).toContain("public static void TestMethod()"); + }); + + it("should write an async method", () => { + const method = new Method({ + name: "TestMethod", + access: Access.Public, + isAsync: true, + parameters: [], + }); + + const writer = new Writer({}); + method.write(writer); + + const result = writer.toString(); + expect(result).toContain("public async Task TestMethod()"); + }); + + it("should write an async method with return type", () => { + const method = new Method({ + name: "TestMethod", + access: Access.Public, + isAsync: true, + parameters: [], + return_: Type.string(), + }); + + const writer = new Writer({}); + method.write(writer); + + const result = writer.toString(); + expect(result).toContain("public async Task TestMethod()"); + }); + + it("should write a method with parameters", () => { + const param1 = new Parameter({ + name: "param1", + type: Type.string(), + }); + + const param2 = new Parameter({ + name: "param2", + type: Type.integer(), + }); + + const method = new Method({ + name: "TestMethod", + access: Access.Public, + isAsync: false, + parameters: [param1, param2], + }); + + const writer = new Writer({}); + method.write(writer); + + const result = writer.toString(); + expect(result).toContain( + "public void TestMethod(string param1, int param2)", + ); + }); + + it("should write a method with body", () => { + const body = new CodeBlock({ + code: 'return "test";', + }); + + const method = new Method({ + name: "TestMethod", + access: Access.Public, + isAsync: false, + parameters: [], + body, + return_: Type.string(), + }); + + const writer = new Writer({}); + method.write(writer); + + const result = writer.toString(); + expect(result).toContain('return "test";'); + }); + + it("should write XML documentation for the method", () => { + const method = new Method({ + name: "TestMethod", + access: Access.Public, + isAsync: false, + parameters: [], + summary: "This is a test method.", + }); + + const writer = new Writer({}); + method.write(writer); + + const result = writer.toString(); + expect(result).toContain("/// "); + expect(result).toContain("/// This is a test method."); + expect(result).toContain("/// "); + }); + + it("should write annotations for the method", () => { + const annotation = new Annotation({ + reference: new ClassReference({ + name: "TestAttribute", + namespace: "Test.Attributes", + }), + }); + + const method = new Method({ + name: "TestMethod", + access: Access.Public, + isAsync: false, + parameters: [], + annotations: [annotation], + }); + + const writer = new Writer({}); + method.write(writer); + + const result = writer.toString(); + expect(result).toContain("[TestAttribute()]"); + expect(result).toContain("using Test.Attributes;"); + }); + + it("should write an interface method", () => { + const method = new Method({ + name: "TestMethod", + access: Access.Public, + isAsync: false, + parameters: [], + }); + + method.classType = MethodClassType.INTERFACE; + + const writer = new Writer({}); + method.write(writer); + + const result = writer.toString(); + expect(result).toContain("public void TestMethod();"); + expect(result).not.toContain("{"); + expect(result).not.toContain("}"); + }); + + it("should write a method with extension parameter", () => { + const extensionParam = new Parameter({ + name: "str", + type: Type.string(), + }); + + const method = new Method({ + name: "TestExtension", + access: Access.Public, + isAsync: false, + parameters: [], + extensionParameter: extensionParam, + }); + + const writer = new Writer({}); + method.write(writer); + + const result = writer.toString(); + expect(result).toContain("TestExtension(this string str)"); + }); + }); + + describe("getInvocation", () => { + it("should create a method invocation", () => { + const param1 = new Parameter({ + name: "param1", + type: Type.string(), + }); + + const param2 = new Parameter({ + name: "param2", + type: Type.integer(), + }); + + const method = new Method({ + name: "TestMethod", + access: Access.Public, + isAsync: false, + parameters: [param1, param2], + }); + + const args = new Map(); + args.set(param1, new CodeBlock({ code: '"test"' })); + args.set(param2, new CodeBlock({ code: "42" })); + + const invocation = method.getInvocation(args); + + expect(invocation).toBeInstanceOf(MethodInvocation); + expect(invocation["method"]).toBe(method); + expect(invocation["arguments"]).toBe(args); + }); + + it("should create a method invocation with 'on' parameter", () => { + const method = new Method({ + name: "TestMethod", + access: Access.Public, + isAsync: false, + parameters: [], + }); + + const on = new CodeBlock({ code: "obj" }); + const args = new Map(); + + const invocation = method.getInvocation(args, on); + + expect(invocation["on"]).toBe(on); + }); + }); + + describe("toString", () => { + it("should return the string representation of the method", () => { + const method = new Method({ + name: "TestMethod", + access: Access.Public, + isAsync: false, + parameters: [], + return_: Type.string(), + body: new CodeBlock({ code: 'return "test";' }), + }); + + const result = method.toString(); + + expect(result).toContain("public string TestMethod()"); + expect(result).toContain('return "test";'); + }); + + it("should create writer and call write method", () => { + const method = new Method({ + name: "TestMethod", + access: Access.Public, + isAsync: false, + parameters: [], + }); + + // Create a spy on the write method + const writeSpy = jest.spyOn(method, "write"); + + method.toString(); + + // Verify write was called with a Writer instance + expect(writeSpy).toHaveBeenCalledTimes(1); + expect(writeSpy.mock.calls[0][0]).toBeInstanceOf(Writer); + + // Clean up + writeSpy.mockRestore(); + }); + }); +}); diff --git a/libs/csharp-ast/src/lib/ast/MethodInvocation.spec.ts b/libs/csharp-ast/src/lib/ast/MethodInvocation.spec.ts new file mode 100644 index 0000000..9286799 --- /dev/null +++ b/libs/csharp-ast/src/lib/ast/MethodInvocation.spec.ts @@ -0,0 +1,243 @@ +import { Writer } from "../core/Writer"; +import { Access } from "./Access"; +import { CodeBlock } from "./CodeBlock"; +import { Method } from "./Method"; +import { MethodInvocation } from "./MethodInvocation"; +import { Parameter } from "./Parameter"; +import { Type } from "./Type"; + +describe("MethodInvocation", () => { + // Setup test objects + let method: Method; + let param1: Parameter; + let param2: Parameter; + let argumentsMap: Map; + + beforeEach(() => { + param1 = new Parameter({ + name: "param1", + type: Type.string(), + }); + + param2 = new Parameter({ + name: "param2", + type: Type.integer(), + }); + + method = new Method({ + name: "TestMethod", + access: Access.Public, + isAsync: false, + parameters: [param1, param2], + }); + + argumentsMap = new Map(); + argumentsMap.set(param1, new CodeBlock({ code: '"test"' })); + argumentsMap.set(param2, new CodeBlock({ code: "42" })); + }); + + describe("constructor", () => { + it("should initialize with method and arguments", () => { + const invocation = new MethodInvocation({ + method, + arguments_: argumentsMap, + }); + + expect(invocation["method"]).toBe(method); + expect(invocation["arguments"]).toBe(argumentsMap); + expect(invocation["on"]).toBeUndefined(); + }); + + it("should initialize with 'on' object", () => { + const on = new CodeBlock({ code: "this.instance" }); + + const invocation = new MethodInvocation({ + method, + arguments_: argumentsMap, + on, + }); + + expect(invocation["method"]).toBe(method); + expect(invocation["arguments"]).toBe(argumentsMap); + expect(invocation["on"]).toBe(on); + }); + }); + + describe("write", () => { + it("should write a basic method invocation", () => { + const invocation = new MethodInvocation({ + method, + arguments_: argumentsMap, + }); + + const writer = new Writer({}); + invocation.write(writer); + + const result = writer.toString(); + expect(result).toContain("TestMethod("); + expect(result).toContain('string param1"test"'); + expect(result).toContain("int param242"); + }); + + it("should write a method invocation with 'on' object", () => { + const on = new CodeBlock({ code: "this.instance" }); + + const invocation = new MethodInvocation({ + method, + arguments_: argumentsMap, + on, + }); + + const writer = new Writer({}); + invocation.write(writer); + + const result = writer.toString(); + expect(result).toContain("this.instance.TestMethod("); + }); + + it("should write an async method invocation", () => { + const asyncMethod = new Method({ + name: "TestAsyncMethod", + access: Access.Public, + isAsync: true, + parameters: [param1, param2], + }); + + const invocation = new MethodInvocation({ + method: asyncMethod, + arguments_: argumentsMap, + }); + + const writer = new Writer({}); + invocation.write(writer); + + const result = writer.toString(); + expect(result).toContain("await TestAsyncMethod("); + }); + + it("should write a method invocation with no parameters", () => { + const noParamMethod = new Method({ + name: "NoParamMethod", + access: Access.Public, + isAsync: false, + parameters: [], + }); + + const invocation = new MethodInvocation({ + method: noParamMethod, + arguments_: new Map(), + }); + + const writer = new Writer({}); + invocation.write(writer); + + const result = writer.toString(); + expect(result).toBe("NoParamMethod()"); + }); + + it("should write a method invocation with a single parameter", () => { + const singleParamMethod = new Method({ + name: "SingleParamMethod", + access: Access.Public, + isAsync: false, + parameters: [param1], + }); + + const args = new Map(); + args.set(param1, new CodeBlock({ code: '"singleValue"' })); + + const invocation = new MethodInvocation({ + method: singleParamMethod, + arguments_: args, + }); + + const writer = new Writer({}); + invocation.write(writer); + + const result = writer.toString(); + expect(result).toContain("SingleParamMethod("); + expect(result).toContain('string param1"singleValue"'); + expect(result).not.toContain(","); + }); + + it("should handle complex argument expressions", () => { + const args = new Map(); + args.set( + param1, + new CodeBlock({ + code: "GetStringValue()", + }), + ); + args.set( + param2, + new CodeBlock({ + code: "CalculateValue() + 5", + }), + ); + + const invocation = new MethodInvocation({ + method, + arguments_: args, + }); + + const writer = new Writer({}); + invocation.write(writer); + + const result = writer.toString(); + expect(result).toContain("string param1GetStringValue()"); + expect(result).toContain("int param2CalculateValue() + 5"); + }); + + it("should handle 'on' object with references", () => { + const on = new CodeBlock({ + code: "this.GetService()", + }); + + const invocation = new MethodInvocation({ + method, + arguments_: argumentsMap, + on, + }); + + const writer = new Writer({}); + invocation.write(writer); + + const result = writer.toString(); + expect(result).toContain("this.GetService().TestMethod("); + }); + }); + + describe("toString", () => { + it("should return string representation of method invocation", () => { + const invocation = new MethodInvocation({ + method, + arguments_: argumentsMap, + }); + + const result = invocation.toString(); + + expect(result).toContain("TestMethod("); + expect(result).toContain('string param1"test"'); + expect(result).toContain("int param242"); + }); + + it("should create writer and call write method", () => { + const invocation = new MethodInvocation({ + method, + arguments_: argumentsMap, + }); + + // Create a spy on the write method + const writeSpy = jest.spyOn(invocation, "write"); + + invocation.toString(); + + // Verify write was called with a Writer instance + expect(writeSpy).toHaveBeenCalledTimes(1); + expect(writeSpy.mock.calls[0][0]).toBeInstanceOf(Writer); + + // Clean up + writeSpy.mockRestore(); + }); + }); +}); diff --git a/libs/csharp-ast/src/lib/ast/Parameter.spec.ts b/libs/csharp-ast/src/lib/ast/Parameter.spec.ts new file mode 100644 index 0000000..f2ea793 --- /dev/null +++ b/libs/csharp-ast/src/lib/ast/Parameter.spec.ts @@ -0,0 +1,251 @@ +import { Writer } from "../core/Writer"; +import { Annotation } from "./Annotation"; +import { ClassReference } from "./ClassReference"; +import { Parameter } from "./Parameter"; +import { Type } from "./Type"; + +describe("Parameter", () => { + describe("constructor", () => { + it("should initialize with name and type", () => { + const param = new Parameter({ + name: "testParam", + type: Type.string(), + }); + + expect(param.name).toBe("testParam"); + expect(param["type"]).toEqual(Type.string()); + }); + + it("should initialize with docs", () => { + const param = new Parameter({ + name: "testParam", + type: Type.string(), + docs: "Parameter documentation", + }); + + expect(param.docs).toBe("Parameter documentation"); + }); + + it("should initialize with initializer", () => { + const param = new Parameter({ + name: "testParam", + type: Type.string(), + initializer: '"default"', + }); + + expect(param.initializer).toBe('"default"'); + }); + + it("should initialize with annotations", () => { + const annotation = new Annotation({ + reference: new ClassReference({ + name: "Required", + namespace: "System.ComponentModel.DataAnnotations", + }), + }); + + const param = new Parameter({ + name: "testParam", + type: Type.string(), + annotations: [annotation], + }); + + expect(param.annotations).toEqual([annotation]); + }); + + it("should set default values for optional properties", () => { + const param = new Parameter({ + name: "testParam", + type: Type.string(), + }); + + expect(param.annotations).toEqual([]); + expect(param.splitAnnotations).toBe(true); + expect(param.docs).toBeUndefined(); + expect(param.initializer).toBeUndefined(); + }); + + it("should allow setting splitAnnotations option", () => { + const param = new Parameter({ + name: "testParam", + type: Type.string(), + splitAnnotations: false, + }); + + expect(param.splitAnnotations).toBe(false); + }); + }); + + describe("write", () => { + it("should write parameter with type and name", () => { + const param = new Parameter({ + name: "testParam", + type: Type.string(), + }); + + const writer = new Writer({}); + param.write(writer); + + expect(writer.toString()).toContain("string testParam"); + }); + + it("should write parameter with initializer", () => { + const param = new Parameter({ + name: "testParam", + type: Type.string(), + initializer: '"default"', + }); + + const writer = new Writer({}); + param.write(writer); + + expect(writer.toString()).toContain('string testParam = "default"'); + }); + + it("should write parameter with integer type", () => { + const param = new Parameter({ + name: "testParam", + type: Type.integer(), + }); + + const writer = new Writer({}); + param.write(writer); + + expect(writer.toString()).toContain("int testParam"); + }); + + it("should write parameter with complex type", () => { + const listType = Type.list(Type.string()); + + const param = new Parameter({ + name: "testParam", + type: listType, + }); + + const writer = new Writer({}); + param.write(writer); + + expect(writer.toString()).toContain("List testParam"); + }); + + it("should write annotations with splitAnnotations=true", () => { + const annotation = new Annotation({ + reference: new ClassReference({ + name: "Required", + namespace: "System.ComponentModel.DataAnnotations", + }), + }); + + const param = new Parameter({ + name: "testParam", + type: Type.string(), + annotations: [annotation], + splitAnnotations: true, + }); + + const writer = new Writer({}); + param.write(writer); + + const result = writer.toString(); + expect(result).toContain("[Required()]"); + expect(result).toContain("string testParam"); + expect(result).toContain("using System.ComponentModel.DataAnnotations;"); + }); + + it("should write annotations with splitAnnotations=false", () => { + const annotation1 = new Annotation({ + reference: new ClassReference({ + name: "Required", + namespace: "System.ComponentModel.DataAnnotations", + }), + }); + + const annotation2 = new Annotation({ + reference: new ClassReference({ + name: "StringLength", + namespace: "System.ComponentModel.DataAnnotations", + }), + argument: "100", + }); + + const param = new Parameter({ + name: "testParam", + type: Type.string(), + annotations: [annotation1, annotation2], + splitAnnotations: false, + }); + + const writer = new Writer({}); + param.write(writer); + + const result = writer.toString(); + expect(result).toContain("[Required(), StringLength(100)]"); + expect(result).toContain("string testParam"); + }); + + it("should write multiple annotations on separate lines", () => { + const annotation1 = new Annotation({ + reference: new ClassReference({ + name: "Required", + namespace: "System.ComponentModel.DataAnnotations", + }), + }); + + const annotation2 = new Annotation({ + reference: new ClassReference({ + name: "StringLength", + namespace: "System.ComponentModel.DataAnnotations", + }), + argument: "100", + }); + + const param = new Parameter({ + name: "testParam", + type: Type.string(), + annotations: [annotation1, annotation2], + splitAnnotations: true, + }); + + const writer = new Writer({}); + param.write(writer); + + const result = writer.toString(); + expect(result).toContain("[Required()]"); + expect(result).toContain("[StringLength(100)]"); + expect(result).toContain("string testParam"); + }); + }); + + describe("toString", () => { + it("should return string representation of parameter", () => { + const param = new Parameter({ + name: "testParam", + type: Type.string(), + initializer: '"default"', + }); + + const result = param.toString(); + + expect(result).toContain('string testParam = "default"'); + }); + + it("should create writer and call write method", () => { + const param = new Parameter({ + name: "testParam", + type: Type.string(), + }); + + // Create a spy on the write method + const writeSpy = jest.spyOn(param, "write"); + + param.toString(); + + // Verify write was called with a Writer instance + expect(writeSpy).toHaveBeenCalledTimes(1); + expect(writeSpy.mock.calls[0][0]).toBeInstanceOf(Writer); + + // Clean up + writeSpy.mockRestore(); + }); + }); +}); diff --git a/libs/csharp-ast/src/lib/ast/ProgramClass.spec.ts b/libs/csharp-ast/src/lib/ast/ProgramClass.spec.ts new file mode 100644 index 0000000..2267e1f --- /dev/null +++ b/libs/csharp-ast/src/lib/ast/ProgramClass.spec.ts @@ -0,0 +1,95 @@ +import { ProgramClass } from "./ProgramClass"; +import { ClassReference } from "./ClassReference"; +import { CodeBlock } from "./CodeBlock"; +import { Writer } from "../core/Writer"; + +describe("ProgramClass", () => { + let programClass: ProgramClass; + let writer: Writer; + + beforeEach(() => { + writer = new Writer({ namespace: "Test.Namespace" }); + }); + + describe("constructor", () => { + it("should initialize with given arguments", () => { + const args = { + references: [ + new ClassReference({ name: "TestRef", namespace: "Test.Namespace" }), + ], + builderServicesBlocks: [ + new CodeBlock({ code: "builder.Services.Add();" }), + ], + appBlocks: [new CodeBlock({ code: "app.Use();" })], + }; + programClass = new ProgramClass(args); + + expect(programClass.references).toHaveLength(1); + expect(programClass.builderServicesBlocks).toHaveLength(1); + expect(programClass.appBlocks).toHaveLength(1); + }); + }); + + describe("addReference", () => { + it("should add a class reference", () => { + const args = { + references: [], + builderServicesBlocks: [], + appBlocks: [], + }; + programClass = new ProgramClass(args); + const newRef = new ClassReference({ + name: "NewRef", + namespace: "Test.Namespace", + }); + + programClass.addReference(newRef); + + expect(programClass.references).toContain(newRef); + }); + }); + + describe("write", () => { + it("should write a basic program structure", () => { + const args = { + references: [ + new ClassReference({ name: "TestRef", namespace: "Test.Namespace" }), + ], + builderServicesBlocks: [ + new CodeBlock({ code: "builder.Services.Add();" }), + ], + appBlocks: [new CodeBlock({ code: "app.Use();" })], + }; + programClass = new ProgramClass(args); + + programClass.write(writer); + const output = writer.toString(); + + expect(output).toContain("builder.Services.Add();"); + expect(output).toContain("app.Use();"); + expect(output).toContain("app.Run();"); + }); + + it("should write a program with try-catch-finally blocks", () => { + const args = { + references: [], + builderServicesBlocks: [], + appBlocks: [], + catchBlocks: [new CodeBlock({ code: "Console.WriteLine(ex);" })], + finallyBlocks: [ + new CodeBlock({ code: "Console.WriteLine('Finally');" }), + ], + }; + programClass = new ProgramClass(args); + + programClass.write(writer); + const output = writer.toString(); + + expect(output).toContain("try"); + expect(output).toContain("catch(Exception ex)"); + expect(output).toContain("Console.WriteLine(ex);"); + expect(output).toContain("finally"); + expect(output).toContain("Console.WriteLine('Finally');"); + }); + }); +}); diff --git a/libs/csharp-ast/src/lib/ast/Type.spec.ts b/libs/csharp-ast/src/lib/ast/Type.spec.ts new file mode 100644 index 0000000..a05c822 --- /dev/null +++ b/libs/csharp-ast/src/lib/ast/Type.spec.ts @@ -0,0 +1,218 @@ +import { Type } from "./Type"; +import { Writer } from "../core/Writer"; +import { ClassReference } from "./ClassReference"; +import { GenericClassReference } from "./GenericClassReference"; +import { CoreClassReference } from "./CoreClassReference"; + +describe("Type", () => { + describe("static factory methods", () => { + it("should create string type", () => { + const type = Type.string(); + expect(type).toBeDefined(); + const writer = new Writer({}); + type.write(writer); + expect(writer.toString()).toBe("string"); + }); + + it("should create boolean type", () => { + const type = Type.boolean(); + expect(type).toBeDefined(); + const writer = new Writer({}); + type.write(writer); + expect(writer.toString()).toBe("bool"); + }); + + it("should create integer type", () => { + const type = Type.integer(); + expect(type).toBeDefined(); + const writer = new Writer({}); + type.write(writer); + expect(writer.toString()).toBe("int"); + }); + + it("should create long type", () => { + const type = Type.long(); + expect(type).toBeDefined(); + const writer = new Writer({}); + type.write(writer); + expect(writer.toString()).toBe("long"); + }); + + it("should create double type", () => { + const type = Type.double(); + expect(type).toBeDefined(); + const writer = new Writer({}); + type.write(writer); + expect(writer.toString()).toBe("double"); + }); + + it("should create date type", () => { + const type = Type.date(); + expect(type).toBeDefined(); + const writer = new Writer({}); + type.write(writer); + expect(writer.toString()).toBe("DateOnly"); + }); + + it("should create dateTime type", () => { + const type = Type.dateTime(); + expect(type).toBeDefined(); + const writer = new Writer({}); + type.write(writer); + expect(writer.toString()).toBe("DateTime"); + }); + + it("should create uuid type", () => { + const type = Type.uuid(); + expect(type).toBeDefined(); + const writer = new Writer({}); + type.write(writer); + expect(writer.toString()).toBe("Guid"); + }); + + it("should create object type", () => { + const type = Type.object(); + expect(type).toBeDefined(); + const writer = new Writer({}); + type.write(writer); + expect(writer.toString()).toBe("object"); + }); + + it("should create list type", () => { + const elementType = Type.string(); + const type = Type.list(elementType); + expect(type).toBeDefined(); + const writer = new Writer({}); + type.write(writer); + expect(writer.toString()).toBe("List"); + }); + + it("should create set type", () => { + const elementType = Type.integer(); + const type = Type.set(elementType); + expect(type).toBeDefined(); + const writer = new Writer({}); + type.write(writer); + expect(writer.toString()).toBe("HashSet"); + }); + + it("should create map type", () => { + const keyType = Type.string(); + const valueType = Type.integer(); + const type = Type.map(keyType, valueType); + expect(type).toBeDefined(); + const writer = new Writer({}); + type.write(writer); + expect(writer.toString()).toBe("Dictionary"); + }); + + it("should create optional type", () => { + const valueType = Type.string(); + const type = Type.optional(valueType); + expect(type).toBeDefined(); + const writer = new Writer({}); + type.write(writer); + expect(writer.toString()).toBe("string?"); + }); + + it("should create reference type", () => { + const classRef = new ClassReference({ + name: "TestClass", + namespace: "Test.Namespace", + }); + const type = Type.reference(classRef); + expect(type).toBeDefined(); + const writer = new Writer({ namespace: "Test.Namespace" }); + type.write(writer); + expect(writer.toString()).toBe("TestClass"); + }); + + it("should create generic reference type", () => { + const classRef = new ClassReference({ + name: "GenericClass", + namespace: "Test.Namespace", + }); + const genericRef = new GenericClassReference({ + reference: classRef, + innerType: Type.string(), + }); + const type = Type.genericReference(genericRef); + expect(type).toBeDefined(); + const writer = new Writer({ namespace: "Test.Namespace" }); + type.write(writer); + expect(writer.toString()).toBe("GenericClass"); + }); + + it("should create core class reference type", () => { + const coreRef = new CoreClassReference({ + name: "CoreClass", + }); + const type = Type.coreClass(coreRef); + expect(type).toBeDefined(); + const writer = new Writer({}); + type.write(writer); + expect(writer.toString()).toBe("CoreClass"); + }); + + it("should create oneOf type with multiple types", () => { + const memberTypes = [Type.string(), Type.integer()]; + const type = Type.oneOf(memberTypes); + expect(type).toBeDefined(); + }); + + it("should create string enum type", () => { + const enumRef = new ClassReference({ + name: "StringEnum", + namespace: "Test.Namespace", + }); + const type = Type.stringEnum(enumRef); + expect(type).toBeDefined(); + const writer = new Writer({ namespace: "Test.Namespace" }); + type.write(writer); + expect(writer.toString()).toBe( + "using StringEnum;\n\nStringEnum", + ); + }); + }); + + describe("write", () => { + it("should write complex nested types correctly", () => { + // Dictionary> + const nestedType = Type.map(Type.string(), Type.list(Type.integer())); + const writer = new Writer({}); + nestedType.write(writer); + expect(writer.toString()).toBe("Dictionary>"); + }); + + it("should write doubly nested types correctly", () => { + // List> + const nestedType = Type.list(Type.map(Type.string(), Type.integer())); + const writer = new Writer({}); + nestedType.write(writer); + expect(writer.toString()).toBe("List>"); + }); + + it("should write optional complex types correctly", () => { + // List? + const optionalListType = Type.optional(Type.list(Type.string())); + const writer = new Writer({}); + optionalListType.write(writer); + expect(writer.toString()).toBe("List?"); + }); + + it("should write class references with generic parameters", () => { + const resultClassRef = new ClassReference({ + name: "Result", + namespace: "System", + }); + const genericRef = new GenericClassReference({ + reference: resultClassRef, + innerType: Type.string(), + }); + const type = Type.genericReference(genericRef); + const writer = new Writer({ namespace: "System" }); + type.write(writer); + expect(writer.toString()).toBe("Result"); + }); + }); +}); diff --git a/libs/csharp-ast/src/lib/core/AstNode.spec.ts b/libs/csharp-ast/src/lib/core/AstNode.spec.ts new file mode 100644 index 0000000..51aaf79 --- /dev/null +++ b/libs/csharp-ast/src/lib/core/AstNode.spec.ts @@ -0,0 +1,79 @@ +import { AstNode } from "./AstNode"; +import { Writer } from "./Writer"; +import { IWriter } from "@amplication/ast-types"; + +// Create a concrete implementation of AstNode for testing +class TestAstNode extends AstNode { + private content: string; + + constructor(content = "") { + super(); + this.content = content; + } + + public write(writer: IWriter): void { + writer.write(this.content); + } +} + +describe("AstNode", () => { + let testNode: TestAstNode; + + beforeEach(() => { + testNode = new TestAstNode("Test content"); + }); + + describe("write", () => { + it("should write content to the writer", () => { + const writer = new Writer({}); + testNode.write(writer); + expect(writer.toString()).toBe("Test content"); + }); + + it("should handle empty content", () => { + const emptyNode = new TestAstNode(""); + const writer = new Writer({}); + emptyNode.write(writer); + expect(writer.toString()).toBe(""); + }); + }); + + describe("toString", () => { + it("should return the string representation of the node", () => { + const result = testNode.toString(); + expect(result).toBe("Test content"); + }); + + it("should create a writer and call write method", () => { + // Create a spy on the write method + const writeSpy = jest.spyOn(testNode, "write"); + + testNode.toString(); + + // Verify that write was called with a Writer instance + expect(writeSpy).toHaveBeenCalledTimes(1); + expect(writeSpy.mock.calls[0][0]).toBeInstanceOf(Writer); + + // Clean up + writeSpy.mockRestore(); + }); + + it("should work with complex content", () => { + const complexNode = new TestAstNode("Line 1\nLine 2\nLine 3"); + expect(complexNode.toString()).toBe("Line 1\nLine 2\nLine 3"); + }); + }); + + describe("inheritance", () => { + it("should allow extension with custom implementations", () => { + class CustomNode extends AstNode { + write(writer: IWriter): void { + writer.write("Custom implementation"); + } + } + + const customNode = new CustomNode(); + expect(customNode.toString()).toBe("Custom implementation"); + }); + }); +}); diff --git a/libs/csharp-ast/src/lib/core/Writer.spec.ts b/libs/csharp-ast/src/lib/core/Writer.spec.ts new file mode 100644 index 0000000..3d37e9f --- /dev/null +++ b/libs/csharp-ast/src/lib/core/Writer.spec.ts @@ -0,0 +1,213 @@ +import { Writer } from "./Writer"; +import { ClassReference } from "../.."; +import { IAstNode, IWriter } from "@amplication/ast-types"; + +// Create a test AST node for testing writeNode +class TestNode implements IAstNode { + private content: string; + + constructor(content = "") { + this.content = content; + } + + public write(writer: IWriter): void { + writer.write(this.content); + } +} + +describe("Writer", () => { + let writer: Writer; + + beforeEach(() => { + writer = new Writer({}); + }); + + describe("initialization", () => { + it("should initialize with empty buffer", () => { + expect(writer.toString()).toBe(""); + }); + + it("should initialize with namespace", () => { + const nsWriter = new Writer({ namespace: "Test.Namespace" }); + expect(nsWriter["namespace"]).toBe("Test.Namespace"); + }); + }); + + describe("write", () => { + it("should write text to buffer", () => { + writer.write("Hello World"); + expect(writer.toString()).toBe("Hello World"); + }); + + it("should handle newlines with proper indentation", () => { + writer.write("Line 1\nLine 2"); + expect(writer.toString()).toBe("Line 1\nLine 2"); + }); + + it("should respect indentation level", () => { + writer.indent(); + writer.write("Indented"); + expect(writer.toString()).toBe(" Indented"); + }); + + it("should handle multiple indentation levels", () => { + writer.indent(); + writer.indent(); + writer.write("Double Indented"); + expect(writer.toString()).toBe(" Double Indented"); + }); + }); + + describe("writeLine", () => { + it("should write text and add newline", () => { + writer.writeLine("Hello"); + expect(writer.toString()).toBe("Hello\n"); + }); + + it("should work with empty text", () => { + writer.writeLine(); + expect(writer.toString()).toBe("\n"); + }); + + it("should respect indentation", () => { + writer.indent(); + writer.writeLine("Indented Line"); + expect(writer.toString()).toBe(" Indented Line\n"); + }); + }); + + describe("newLine", () => { + it("should add a newline character", () => { + writer.newLine(); + expect(writer.toString()).toBe("\n"); + }); + + it("should work with existing content", () => { + writer.write("Content"); + writer.newLine(); + expect(writer.toString()).toBe("Content\n"); + }); + }); + + describe("writeNewLineIfLastLineNot", () => { + it("should add newline if last character is not a newline", () => { + writer.write("No newline"); + writer.writeNewLineIfLastLineNot(); + expect(writer.toString()).toBe("No newline\n"); + }); + + it("should not add newline if last character is already a newline", () => { + writer.write("Has newline\n"); + writer.writeNewLineIfLastLineNot(); + expect(writer.toString()).toBe("Has newline\n"); + }); + }); + + describe("indent and dedent", () => { + it("should increase indentation level", () => { + writer.indent(); + writer.writeLine("First level"); + writer.indent(); + writer.writeLine("Second level"); + expect(writer.toString()).toBe(" First level\n Second level\n"); + }); + + it("should decrease indentation level", () => { + writer.indent(); + writer.indent(); + writer.writeLine("Double indented"); + writer.dedent(); + writer.writeLine("Single indented"); + expect(writer.toString()).toBe( + " Double indented\n Single indented\n", + ); + }); + }); + + describe("writeNode", () => { + it("should write node content", () => { + const node = new TestNode("Node content"); + writer.writeNode(node); + expect(writer.toString()).toBe("Node content"); + }); + + it("should respect indentation when writing node", () => { + const node = new TestNode("Node content"); + writer.indent(); + writer.writeNode(node); + expect(writer.toString()).toBe(" Node content"); + }); + }); + + describe("addReference", () => { + it("should add reference to namespaces", () => { + writer = new Writer({ namespace: "Current.Namespace" }); + const reference = new ClassReference({ + namespace: "Test.Namespace", + name: "TestClass", + }); + writer.addReference(reference); + writer.write("Some content"); + expect(writer.toString()).toBe("using Test.Namespace;\n\nSome content"); + }); + + it("should skip current namespace", () => { + writer = new Writer({ namespace: "Current.Namespace" }); + const reference = new ClassReference({ + namespace: "Current.Namespace", + name: "TestClass", + }); + writer.addReference(reference); + writer.write("Some content"); + expect(writer.toString()).toBe("Some content"); + }); + + it("should handle multiple references", () => { + writer = new Writer({}); + writer.addReference( + new ClassReference({ + namespace: "First.Namespace", + name: "FirstClass", + }), + ); + writer.addReference( + new ClassReference({ + namespace: "Second.Namespace", + name: "SecondClass", + }), + ); + writer.write("Content"); + const result = writer.toString(); + expect(result).toContain("using First.Namespace;"); + expect(result).toContain("using Second.Namespace;"); + expect(result).toContain("Content"); + }); + + it("should handle null namespace", () => { + writer = new Writer({}); + const reference = new ClassReference({ + namespace: null as any, + name: "TestClass", + }); + writer.addReference(reference); + writer.write("Content"); + expect(writer.toString()).toBe("Content"); + }); + }); + + describe("toString", () => { + it("should return buffer content", () => { + writer.write("Test content"); + expect(writer.toString()).toBe("Test content"); + }); + + it("should include namespaces when present", () => { + writer = new Writer({}); + writer.addReference( + new ClassReference({ namespace: "Test.Namespace", name: "TestClass" }), + ); + writer.write("Content"); + expect(writer.toString()).toBe("using Test.Namespace;\n\nContent"); + }); + }); +}); diff --git a/libs/java-ast/jest.config.ts b/libs/java-ast/jest.config.ts index f12c1ab..1ea3dce 100644 --- a/libs/java-ast/jest.config.ts +++ b/libs/java-ast/jest.config.ts @@ -10,8 +10,8 @@ export default { coverageDirectory: "../../coverage/libs/java-ast", coverageThreshold: { global: { - branches: 10, - lines: 9, + branches: 82, + lines: 82, }, }, };