diff options
| author | Michael Williamson <mike@zwobble.org> | 2026-05-23 21:27:24 +0100 |
|---|---|---|
| committer | Michael Williamson <mike@zwobble.org> | 2026-05-23 21:42:22 +0100 |
| commit | 5c0b6c30c84067dcc36dbdc9d8e4075e94490131 (patch) | |
| tree | f1b557f3ec55cd2f0c561fe7a315b41f1927a6d0 | |
| parent | 1cc30d7d45377b98ee754e248149bf2f0cc485fb (diff) | |
Preserve existing custom areas
11 files changed, 288 insertions, 120 deletions
diff --git a/examples/05-custom-code/hobgoblin.json5 b/examples/05-custom-code/hobgoblin.json5 new file mode 100644 index 0000000..19cd31b --- /dev/null +++ b/examples/05-custom-code/hobgoblin.json5 @@ -0,0 +1,9 @@ +{ + outputs: [ + { + generator: "java-types", + path: "output/java/src/main/java", + packageName: "org.zwobble.example.types" + }, + ], +} diff --git a/examples/05-custom-code/output/java/.gitignore b/examples/05-custom-code/output/java/.gitignore new file mode 100644 index 0000000..ff511d4 --- /dev/null +++ b/examples/05-custom-code/output/java/.gitignore @@ -0,0 +1,2 @@ +/src/gen/ +/target/ diff --git a/examples/05-custom-code/output/java/pom.xml b/examples/05-custom-code/output/java/pom.xml new file mode 100644 index 0000000..7493667 --- /dev/null +++ b/examples/05-custom-code/output/java/pom.xml @@ -0,0 +1,39 @@ +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <groupId>org.zwobble.hobgoblin.examples</groupId> + <artifactId>example</artifactId> + <version>1.0-SNAPSHOT</version> + + <properties> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + <maven.compiler.source>25</maven.compiler.source> + <maven.compiler.target>25</maven.compiler.target> + </properties> + + <build> + <plugins> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>build-helper-maven-plugin</artifactId> + <version>3.6.1</version> + <executions> + <execution> + <id>add-source</id> + <phase>generate-sources</phase> + <goals> + <goal>add-source</goal> + </goals> + <configuration> + <sources> + <source>src/gen/java</source> + </sources> + </configuration> + </execution> + </executions> + </plugin> + </plugins> + </build> +</project> diff --git a/examples/05-custom-code/output/java/src/main/java/org/zwobble/example/Main.java b/examples/05-custom-code/output/java/src/main/java/org/zwobble/example/Main.java new file mode 100644 index 0000000..2d2366d --- /dev/null +++ b/examples/05-custom-code/output/java/src/main/java/org/zwobble/example/Main.java @@ -0,0 +1,10 @@ +package org.zwobble.example; + +import org.zwobble.example.types.shapes.Square; + +public class Main { + public static void main() { + Square square = new Square(10); + assert square.area() == 100; + } +} diff --git a/examples/05-custom-code/output/java/src/main/java/org/zwobble/example/types/shapes/Square.java b/examples/05-custom-code/output/java/src/main/java/org/zwobble/example/types/shapes/Square.java new file mode 100644 index 0000000..dd48603 --- /dev/null +++ b/examples/05-custom-code/output/java/src/main/java/org/zwobble/example/types/shapes/Square.java @@ -0,0 +1,28 @@ +package org.zwobble.example.types.shapes; + +public record Square(int width) { + public static org.zwobble.example.types.shapes.Square.Builder arbitrary() { + return new org.zwobble.example.types.shapes.Square.Builder(0); + } + + public record Builder(int width) { + public org.zwobble.example.types.shapes.Square build() { + return new org.zwobble.example.types.shapes.Square(width); + } + + public org.zwobble.example.types.shapes.Square.Builder withWidth(int width) { + return new org.zwobble.example.types.shapes.Square.Builder(width); + } + + // Custom area start: org.zwobble.example.types.shapes.Square.Builder body + // Custom area end: org.zwobble.example.types.shapes.Square.Builder body + } + + // Custom area start: org.zwobble.example.types.shapes.Square body + + public int area() { + return this.width * this.width; + } + + // Custom area end: org.zwobble.example.types.shapes.Square body +} diff --git a/examples/05-custom-code/src/shapes.hob b/examples/05-custom-code/src/shapes.hob new file mode 100644 index 0000000..5dfe833 --- /dev/null +++ b/examples/05-custom-code/src/shapes.hob @@ -0,0 +1,3 @@ +struct Square { + field width: Int32; +} diff --git a/src/main/java/org/zwobble/hobgoblin/compiler/output/CodeWriter.java b/src/main/java/org/zwobble/hobgoblin/compiler/output/CodeWriter.java index 3e08c85..b8b0709 100644 --- a/src/main/java/org/zwobble/hobgoblin/compiler/output/CodeWriter.java +++ b/src/main/java/org/zwobble/hobgoblin/compiler/output/CodeWriter.java @@ -4,21 +4,92 @@ import java.io.FileWriter; import java.io.IOException; import java.io.Writer; import java.nio.file.Files; +import java.nio.file.NoSuchFileException; import java.nio.file.Path; +import java.util.*; public class CodeWriter implements AutoCloseable { - public static CodeWriter file(Path path) throws IOException { + public record CustomAreas( + Map<String, List<String>> existing, + String customAreaStart, + String customAreaEnd + ) { + public static CustomAreas read( + String existingString, + String customAreaStart, + String customAreaEnd + ) { + var existing = new HashMap<String, List<String>>(); + + Iterable<String> lines = () -> existingString.lines().iterator(); + Optional<String> name = Optional.empty(); + var customAreaLines = new ArrayList<String>(); + for (var line : lines) { + var trimmedLine = line.trim(); + if (trimmedLine.startsWith(customAreaStart)) { + name = Optional.of(trimmedLine.substring(customAreaStart.length())); + } else if (name.isPresent() && trimmedLine.startsWith(customAreaEnd + name.get())) { + existing.put(name.get(), customAreaLines); + customAreaLines = new ArrayList<>(); + name = Optional.empty(); + } else if (name.isPresent()) { + customAreaLines.add(line); + } + } + + return new CustomAreas(existing, customAreaStart, customAreaEnd); + } + + public static CustomAreas empty( + String customAreaStart, + String customAreaEnd + ) { + return new CustomAreas(Map.of(), customAreaStart, customAreaEnd); + } + + public Iterable<String> lines(String name) { + var lines = this.existing.get(name); + if (lines == null) { + return List.of(); + } else { + return lines; + } + } + } + + public static CodeWriter file( + Path path, + String customAreaStart, + String customAreaEnd + ) throws IOException { + var customAreas = readCustomAreas(path, customAreaStart, customAreaEnd); Files.createDirectories(path.getParent()); var writer = new FileWriter(path.toFile()); - return new CodeWriter(writer); + // TODO: warn if custom areas are unused? + return new CodeWriter(writer, customAreas); + } + + private static CustomAreas readCustomAreas( + Path path, + String customAreaStart, + String customAreaEnd + ) throws IOException { + try { + var contents = Files.readString(path); + return CustomAreas.read(contents, customAreaStart, customAreaEnd); + } catch (NoSuchFileException exception) { + return CustomAreas.empty(customAreaStart, customAreaEnd); + } } private final Writer writer; + private final CustomAreas customAreas; private int indent; private boolean pendingIndent; - public CodeWriter(Writer writer) { + public CodeWriter(Writer writer, CustomAreas customAreas) { this.writer = writer; + this.customAreas = customAreas; this.indent = 0; this.pendingIndent = false; } @@ -47,6 +118,21 @@ public class CodeWriter implements AutoCloseable { this.indent -= 1; } + public void writeCustomArea(String name) throws IOException { + this.write(this.customAreas.customAreaStart); + this.write(name); + + for (var line : this.customAreas.lines(name)) { + this.writer.write("\n"); + this.write(line); + } + + this.newLine(); + this.write(this.customAreas.customAreaEnd); + this.write(name); + this.newLine(); + } + @Override public void close() throws Exception { this.writer.close(); diff --git a/src/main/java/org/zwobble/hobgoblin/compiler/output/generators/javatypes/JavaTypesGenerator.java b/src/main/java/org/zwobble/hobgoblin/compiler/output/generators/javatypes/JavaTypesGenerator.java index 77c2ecd..9356519 100644 --- a/src/main/java/org/zwobble/hobgoblin/compiler/output/generators/javatypes/JavaTypesGenerator.java +++ b/src/main/java/org/zwobble/hobgoblin/compiler/output/generators/javatypes/JavaTypesGenerator.java @@ -4,7 +4,6 @@ import org.zwobble.hobgoblin.compiler.ast.DocComment; import org.zwobble.hobgoblin.compiler.ast.typed.*; import org.zwobble.hobgoblin.compiler.builtins.NativeTypes; import org.zwobble.hobgoblin.compiler.config.OutputConfig; -import org.zwobble.hobgoblin.compiler.output.CodeWriter; import org.zwobble.hobgoblin.compiler.output.generators.Generator; import org.zwobble.hobgoblin.compiler.output.lang.java.JavaWriter; import org.zwobble.hobgoblin.compiler.output.lang.java.ast.*; @@ -78,7 +77,7 @@ public class JavaTypesGenerator implements Generator { var javaCompilationUnitPath = this.sourceRootDirectory .resolve(String.join(File.separator, javaCompilationUnit.packageName())) .resolve(javaCompilationUnit.typeDeclaration().name() + ".java"); - try (var writer = new JavaWriter(CodeWriter.file(javaCompilationUnitPath))) { + try (var writer = JavaWriter.file(javaCompilationUnitPath)) { writer.writeCompilationUnit(javaCompilationUnit); } catch (Exception e) { // TODO: better handling @@ -115,7 +114,7 @@ public class JavaTypesGenerator implements Generator { sumDefinition.variants().stream() .map(variant -> generateTypeRef(variant.type(), context)) .toList(), - JavaCustomArea.EMPTY, + JavaCustomArea.type(generateTypeRef(sumDefinition.type(), context), "body"), sumDefinition.docComment() ) ); @@ -168,7 +167,7 @@ public class JavaTypesGenerator implements Generator { ), generateStructDefinitionBuilder(structDefinition, components, context) ), - JavaCustomArea.EMPTY, + JavaCustomArea.type(generateTypeRef(structDefinition.type(), context), "body"), structDefinition.docComment() ); } @@ -216,7 +215,7 @@ public class JavaTypesGenerator implements Generator { components, List.of(), body, - JavaCustomArea.EMPTY, + JavaCustomArea.type(builderJavaTypeRef, "body"), DocComment.EMPTY ); } diff --git a/src/main/java/org/zwobble/hobgoblin/compiler/output/lang/java/JavaWriter.java b/src/main/java/org/zwobble/hobgoblin/compiler/output/lang/java/JavaWriter.java index 9f87e0f..82c2f9b 100644 --- a/src/main/java/org/zwobble/hobgoblin/compiler/output/lang/java/JavaWriter.java +++ b/src/main/java/org/zwobble/hobgoblin/compiler/output/lang/java/JavaWriter.java @@ -5,8 +5,18 @@ import org.zwobble.hobgoblin.compiler.output.CodeWriter; import org.zwobble.hobgoblin.compiler.output.lang.java.ast.*; import java.io.IOException; +import java.io.StringWriter; +import java.nio.file.Path; public class JavaWriter implements AutoCloseable { + public static JavaWriter file(Path path) throws IOException { + return new JavaWriter(CodeWriter.file( + path, + "// Custom area start: ", + "// Custom area end: " + )); + } + private final CodeWriter writer; public JavaWriter(CodeWriter writer) { @@ -84,7 +94,7 @@ public class JavaWriter implements AutoCloseable { } this.writer.newLine(); - this.writeCustomArea("body", classDeclaration.customArea()); + this.writeCustomArea(classDeclaration.customArea()); this.writer.dedent(); this.writer.write("}"); @@ -102,19 +112,12 @@ public class JavaWriter implements AutoCloseable { } } - private void writeCustomArea(String name, JavaCustomArea javaCustomArea) throws IOException { - this.writer.write("// Custom area start: "); - this.writer.write(name); - this.writer.newLine(); - - for (var line : javaCustomArea.lines()) { - this.writer.write(line); - this.writer.newLine(); - } - - this.writer.write("// Custom area end: "); - this.writer.write(name); - this.writer.newLine(); + private void writeCustomArea(JavaCustomArea customArea) throws IOException { + var customAreaName = new StringWriter(); + writeTypeRef(customArea.type(), customAreaName::write); + customAreaName.write(" "); + customAreaName.write(customArea.name()); + this.writer.writeCustomArea(customAreaName.toString()); } void writeExpression(JavaExpression expression) throws IOException { @@ -165,7 +168,7 @@ public class JavaWriter implements AutoCloseable { this.writer.write("{"); this.writer.indent(); this.writer.newLine(); - this.writeCustomArea("body", interfaceDeclaration.customArea()); + this.writeCustomArea(interfaceDeclaration.customArea()); this.writer.dedent(); this.writer.write("}"); } @@ -271,7 +274,7 @@ public class JavaWriter implements AutoCloseable { } this.writer.newLine(); - this.writeCustomArea("body", recordDeclaration.customArea()); + this.writeCustomArea(recordDeclaration.customArea()); this.writer.dedent(); this.writer.write("}"); @@ -301,6 +304,10 @@ public class JavaWriter implements AutoCloseable { } void writeTypeRef(JavaTypeRef type) throws IOException { + writeTypeRef(type, writer::write); + } + + private static void writeTypeRef(JavaTypeRef type, WriteString writer) throws IOException { for (var part : type.packageName()) { writer.write(part); writer.write("."); @@ -316,14 +323,14 @@ public class JavaWriter implements AutoCloseable { writer.write("<"); writeWithSeparator( type.args(), - this::writeTypeRef, + arg -> writeTypeRef(arg, writer), () -> writer.write(", ") ); writer.write(">"); } } - private <T> void writeWithSeparator( + private static <T> void writeWithSeparator( Iterable<T> iterable, WriteElement<T> writeElement, WriteSeparator writeSeparator @@ -349,4 +356,8 @@ public class JavaWriter implements AutoCloseable { interface WriteSeparator { void write() throws IOException; } + + interface WriteString { + void write(String string) throws IOException; + } } diff --git a/src/main/java/org/zwobble/hobgoblin/compiler/output/lang/java/ast/JavaCustomArea.java b/src/main/java/org/zwobble/hobgoblin/compiler/output/lang/java/ast/JavaCustomArea.java index 42d38d1..51e3096 100644 --- a/src/main/java/org/zwobble/hobgoblin/compiler/output/lang/java/ast/JavaCustomArea.java +++ b/src/main/java/org/zwobble/hobgoblin/compiler/output/lang/java/ast/JavaCustomArea.java @@ -1,7 +1,7 @@ package org.zwobble.hobgoblin.compiler.output.lang.java.ast; -import java.util.List; - -public record JavaCustomArea(List<String> lines) { - public static final JavaCustomArea EMPTY = new JavaCustomArea(List.of()); +public record JavaCustomArea(JavaTypeRef type, String name) { + public static JavaCustomArea type(JavaTypeRef type, String name) { + return new JavaCustomArea(type, name); + } } diff --git a/src/test/java/org/zwobble/hobgoblin/compiler/output/lang/java/JavaWriterTests.java b/src/test/java/org/zwobble/hobgoblin/compiler/output/lang/java/JavaWriterTests.java index e13c3dd..beef2ca 100644 --- a/src/test/java/org/zwobble/hobgoblin/compiler/output/lang/java/JavaWriterTests.java +++ b/src/test/java/org/zwobble/hobgoblin/compiler/output/lang/java/JavaWriterTests.java @@ -8,6 +8,7 @@ import org.zwobble.hobgoblin.compiler.output.lang.java.ast.*; import java.io.IOException; import java.io.StringWriter; import java.util.List; +import java.util.Optional; import static org.zwobble.precisely.AssertThat.assertThat; import static org.zwobble.precisely.Matchers.equalTo; @@ -18,7 +19,7 @@ public class JavaWriterTests { var java = new JavaClassDeclaration( "Rectangle", List.of(), - JavaCustomArea.EMPTY, + new JavaCustomArea(JavaTypeRef.topLevel(List.of(), "Rectangle"), "body"), DocComment.EMPTY ); @@ -26,8 +27,8 @@ public class JavaWriterTests { assertThat(string, equalTo(""" public class Rectangle { - // Custom area start: body - // Custom area end: body + // Custom area start: Rectangle body + // Custom area end: Rectangle body }""" )); } @@ -52,7 +53,7 @@ public class JavaWriterTests { JavaBlock.EMPTY ) ), - JavaCustomArea.EMPTY, + new JavaCustomArea(JavaTypeRef.topLevel(List.of(), "Rectangle"), "body"), DocComment.EMPTY ); @@ -66,28 +67,8 @@ public class JavaWriterTests { public void b() { } - // Custom area start: body - // Custom area end: body - }""" - )); - } - - @Test - public void classDeclarationWithCustomArea() throws IOException { - var java = new JavaClassDeclaration( - "Point", - List.of(), - new JavaCustomArea(List.of("// <custom area>")), - DocComment.EMPTY - ); - - var string = write(writer -> writer.writeTypeDeclaration(java)); - - assertThat(string, equalTo(""" - public class Point { - // Custom area start: body - // <custom area> - // Custom area end: body + // Custom area start: Rectangle body + // Custom area end: Rectangle body }""" )); } @@ -97,7 +78,7 @@ public class JavaWriterTests { var java = new JavaClassDeclaration( "Point", List.of(), - JavaCustomArea.EMPTY, + new JavaCustomArea(JavaTypeRef.topLevel(List.of(), "Point"), "body"), new DocComment("A 2D point.") ); @@ -106,8 +87,8 @@ public class JavaWriterTests { assertThat(string, equalTo(""" /// A 2D point. public class Point { - // Custom area start: body - // Custom area end: body + // Custom area start: Point body + // Custom area end: Point body }""" )); } @@ -117,7 +98,7 @@ public class JavaWriterTests { var java = new JavaInterfaceDeclaration( "Shape", List.of(), - JavaCustomArea.EMPTY, + new JavaCustomArea(JavaTypeRef.topLevel(List.of(), "Shape"), "body"), DocComment.EMPTY ); @@ -125,8 +106,8 @@ public class JavaWriterTests { assertThat(string, equalTo(""" public sealed interface Shape { - // Custom area start: body - // Custom area end: body + // Custom area start: Shape body + // Custom area end: Shape body }""" )); } @@ -139,7 +120,7 @@ public class JavaWriterTests { JavaTypeRef.topLevel(List.of("abc", "def"), "One"), JavaTypeRef.topLevel(List.of("abc", "def"), "Two") ), - JavaCustomArea.EMPTY, + new JavaCustomArea(JavaTypeRef.topLevel(List.of(), "Shape"), "body"), DocComment.EMPTY ); @@ -147,28 +128,8 @@ public class JavaWriterTests { assertThat(string, equalTo(""" public sealed interface Shape permits abc.def.One, abc.def.Two { - // Custom area start: body - // Custom area end: body - }""" - )); - } - - @Test - public void interfaceDeclarationWithCustomArea() throws IOException { - var java = new JavaInterfaceDeclaration( - "Shape", - List.of(), - new JavaCustomArea(List.of("// <custom area>")), - DocComment.EMPTY - ); - - var string = write(writer -> writer.writeTypeDeclaration(java)); - - assertThat(string, equalTo(""" - public sealed interface Shape { - // Custom area start: body - // <custom area> - // Custom area end: body + // Custom area start: Shape body + // Custom area end: Shape body }""" )); } @@ -178,7 +139,7 @@ public class JavaWriterTests { var java = new JavaInterfaceDeclaration( "Shape", List.of(), - JavaCustomArea.EMPTY, + new JavaCustomArea(JavaTypeRef.topLevel(List.of(), "Shape"), "body"), new DocComment("A 2D shape.") ); @@ -187,8 +148,8 @@ public class JavaWriterTests { assertThat(string, equalTo(""" /// A 2D shape. public sealed interface Shape { - // Custom area start: body - // Custom area end: body + // Custom area start: Shape body + // Custom area end: Shape body }""" )); } @@ -200,28 +161,7 @@ public class JavaWriterTests { List.of(), List.of(), List.of(), - JavaCustomArea.EMPTY, - DocComment.EMPTY - ); - - var string = write(writer -> writer.writeTypeDeclaration(java)); - - assertThat(string, equalTo(""" - public record Point() { - // Custom area start: body - // Custom area end: body - }""" - )); - } - - @Test - public void recordDeclarationWithCustomArea() throws IOException { - var java = new JavaRecordDeclaration( - "Point", - List.of(), - List.of(), - List.of(), - new JavaCustomArea(List.of("// <custom area>")), + new JavaCustomArea(JavaTypeRef.topLevel(List.of(), "Point"), "body"), DocComment.EMPTY ); @@ -229,9 +169,8 @@ public class JavaWriterTests { assertThat(string, equalTo(""" public record Point() { - // Custom area start: body - // <custom area> - // Custom area end: body + // Custom area start: Point body + // Custom area end: Point body }""" )); } @@ -243,7 +182,7 @@ public class JavaWriterTests { List.of(), List.of(), List.of(), - JavaCustomArea.EMPTY, + new JavaCustomArea(JavaTypeRef.topLevel(List.of(), "Point"), "body"), new DocComment("A 2D point.") ); @@ -252,8 +191,8 @@ public class JavaWriterTests { assertThat(string, equalTo(""" /// A 2D point. public record Point() { - // Custom area start: body - // Custom area end: body + // Custom area start: Point body + // Custom area end: Point body }""" )); } @@ -280,7 +219,7 @@ public class JavaWriterTests { JavaBlock.EMPTY ) ), - JavaCustomArea.EMPTY, + new JavaCustomArea(JavaTypeRef.topLevel(List.of(), "Rectangle"), "body"), DocComment.EMPTY ); @@ -294,8 +233,8 @@ public class JavaWriterTests { public void b() { } - // Custom area start: body - // Custom area end: body + // Custom area start: Rectangle body + // Custom area end: Rectangle body }""" )); } @@ -463,9 +402,51 @@ public class JavaWriterTests { assertThat(string, equalTo("abc.def.One.Two")); } + @Test + public void customAreaContentsArePreserved() throws IOException { + var java = new JavaRecordDeclaration( + "Point", + List.of(), + List.of(), + List.of(), + new JavaCustomArea(JavaTypeRef.topLevel(List.of(), "Point"), "body"), + DocComment.EMPTY + ); + + var string = write( + writer -> writer.writeTypeDeclaration(java), + """ + public class Point { + // Custom area start: Point body + // <custom area> + // Custom area end: Point body + } + """ + ); + + assertThat(string, equalTo(""" + public record Point() { + // Custom area start: Point body + // <custom area> + // Custom area end: Point body + }""" + )); + } + private String write(Write write) throws IOException { + return write(write, Optional.empty()); + } + + private String write(Write write, String originalContents) throws IOException { + return write(write, Optional.of(originalContents)); + } + + private String write(Write write, Optional<String> originalContents) throws IOException { var stringWriter = new StringWriter(); - var javaWriter = new JavaWriter(new CodeWriter(stringWriter)); + var javaWriter = new JavaWriter(new CodeWriter( + stringWriter, + CodeWriter.CustomAreas.read(originalContents.orElse(""), "// Custom area start: ", "// Custom area end: ") + )); write.write(javaWriter); return stringWriter.toString(); } |
