From bc94b2d3c79a2d09162edac923c9608065961ffe Mon Sep 17 00:00:00 2001 From: Raul Gomis Date: Sat, 11 Apr 2026 12:10:35 +1000 Subject: [PATCH 1/4] Add GitHub Actions tests for feature branches --- .github/workflows/feature-branches.yml | 31 +++++++++ .github/workflows/master.yml | 22 +++++-- README.md | 31 +++++---- pom.xml | 30 +++------ .../com/raulgomis/djc/DynamicByteObject.java | 5 +- .../com/raulgomis/djc/DynamicClassLoader.java | 22 +++---- .../com/raulgomis/djc/DynamicCompiler.java | 42 +++++++----- .../djc/DynamicCompilerException.java | 19 +++--- .../raulgomis/djc/DynamicCompilerUtils.java | 17 ++--- .../djc/DynamicExtendedFileManager.java | 26 ++++---- .../raulgomis/djc/DynamicStringObject.java | 1 + .../raulgomis/djc/DynamicCompilerTest.java | 64 +++++++------------ .../raulgomis/djc/utils/CompilerExecutor.java | 8 +-- .../djc/utils/CompilerTestInput.java | 32 +++++----- .../djc/utils/CompilerTestUtils.java | 11 +--- 15 files changed, 178 insertions(+), 183 deletions(-) create mode 100644 .github/workflows/feature-branches.yml diff --git a/.github/workflows/feature-branches.yml b/.github/workflows/feature-branches.yml new file mode 100644 index 0000000..68cd5a3 --- /dev/null +++ b/.github/workflows/feature-branches.yml @@ -0,0 +1,31 @@ +name: Feature Branch Tests + +on: + push: + branches: + - 'feature/**' + pull_request: + branches: + - master + types: [opened, synchronize, reopened, ready_for_review] + +permissions: + contents: read + +jobs: + test: + if: github.event_name != 'pull_request' || github.event.pull_request.draft == false + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up JDK 25 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '25' + cache: maven + + - name: Run unit tests + run: mvn -B test --file pom.xml diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index cbb3902..d90a5d6 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -6,14 +6,22 @@ on: pull_request: branches: [master] +permissions: + contents: read + jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Set up JDK 1.8 - uses: actions/setup-java@v1 - with: - java-version: 1.8 - - name: Build with Maven - run: mvn -B package --file pom.xml + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up JDK 25 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '25' + cache: maven + + - name: Run tests + run: mvn -B test --file pom.xml diff --git a/README.md b/README.md index c603dfc..80e6f95 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,42 @@ # What is dynamic-java-compiler? ![Build](https://github.com/raulgomis/dynamic-java-compiler/workflows/Build/badge.svg) -_Dynamic-java-compiler_ is a library that allows users to dynamically compile and execute any java source code. Writing dynamically executed Java applications require some boilerplate code: working with classloaders, compilation error handling, etc. The idea behind this library is to free you from this development and let you focus on your business logic. +_Dynamic-java-compiler_ is a library that allows users to dynamically compile and execute Java source code. Writing dynamically executed Java applications usually requires boilerplate code (working with classloaders, compilation error handling, etc.). This library removes that overhead so you can focus on business logic. -# How does it work? +## Java compatibility -The dynamic ompilation task is very simple with this library. Imagine we want to compile this source code introduced dynamically as a text string by the user: +This project is configured to compile using Java **25** (`maven.compiler.release=25`) and has tests updated for modern JDK APIs. + +## How does it work? + +The dynamic compilation task is very simple with this library. Imagine we want to compile this source code introduced dynamically as a text string by the user: ```java public class Test01 implements Runnable { - public void run() { - System.out.println("Hello World!"); - } + public void run() { + System.out.println("Hello World!"); + } } ``` -So simple, we just need to instantiate the compiler and compile the code: +Now instantiate the compiler and compile the code: ```java DynamicCompiler compiler = new DynamicCompiler<>(); // Read source code as String Class clazz = compiler.compile(null, "Test01", source); -final Runnable r; -try { - r = clazz.newInstance(); - r.run(); -} catch (InstantiationException | IllegalAccessException e) { - e.printStackTrace(); -} +Runnable runnable = clazz.getDeclaredConstructor().newInstance(); +runnable.run(); ``` The final result will be: + ``` Hello World! ``` - ## Contribution You are welcome to contribute to the project using pull requests on GitHub. -If you find a bug or want to request a feature, please use the [issue tracker](https://github.com/raulgomis/dynamic-java-compiler/issues) of Github. +If you find a bug or want to request a feature, please use the [issue tracker](https://github.com/raulgomis/dynamic-java-compiler/issues) on GitHub. diff --git a/pom.xml b/pom.xml index ed9a419..169cef3 100644 --- a/pom.xml +++ b/pom.xml @@ -17,7 +17,7 @@ The MIT License https://github.com/raulgomis/dynamic-java-compiler/blob/master/LICENSE - + @@ -29,27 +29,15 @@ UTF-8 - 1.8 - 1.8 + 25 + 5.12.2 org.junit.jupiter - junit-jupiter-engine - 5.6.1 - test - - - org.junit.jupiter - junit-jupiter-params - 5.6.1 - test - - - org.junit.jupiter - junit-jupiter-api - 5.6.1 + junit-jupiter + ${junit.jupiter.version} test @@ -59,17 +47,15 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.1 + 3.14.1 - ${maven.compiler.source} - ${maven.compiler.target} + ${maven.compiler.release} - org.apache.maven.plugins maven-surefire-plugin - 2.22.2 + 3.5.4 diff --git a/src/main/java/com/raulgomis/djc/DynamicByteObject.java b/src/main/java/com/raulgomis/djc/DynamicByteObject.java index 248c055..8fd5146 100644 --- a/src/main/java/com/raulgomis/djc/DynamicByteObject.java +++ b/src/main/java/com/raulgomis/djc/DynamicByteObject.java @@ -1,11 +1,12 @@ package com.raulgomis.djc; -import javax.tools.SimpleJavaFileObject; import java.io.ByteArrayOutputStream; import java.io.OutputStream; +import javax.tools.SimpleJavaFileObject; public class DynamicByteObject extends SimpleJavaFileObject { - private ByteArrayOutputStream outputStream; + + private final ByteArrayOutputStream outputStream; DynamicByteObject(String name, Kind kind) { super(DynamicCompilerUtils.createURI(name), kind); diff --git a/src/main/java/com/raulgomis/djc/DynamicClassLoader.java b/src/main/java/com/raulgomis/djc/DynamicClassLoader.java index f075ada..24c0260 100644 --- a/src/main/java/com/raulgomis/djc/DynamicClassLoader.java +++ b/src/main/java/com/raulgomis/djc/DynamicClassLoader.java @@ -1,29 +1,28 @@ package com.raulgomis.djc; -import javax.tools.JavaFileObject.Kind; import java.io.ByteArrayInputStream; import java.io.InputStream; -import java.util.HashMap; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import javax.tools.JavaFileObject.Kind; public final class DynamicClassLoader extends ClassLoader { - private Map classes = new HashMap<>(); + private final Map classes = new ConcurrentHashMap<>(); DynamicClassLoader(ClassLoader parentClassLoader) { super(parentClassLoader); } void addClass(DynamicByteObject compiledObj) { - System.out.println("Compiled " + compiledObj.getName()); classes.put(compiledObj.getName(), compiledObj); } @Override public Class findClass(String className) throws ClassNotFoundException { - DynamicByteObject byteObject = classes.get(className); + final DynamicByteObject byteObject = classes.get(className); if (byteObject != null) { - byte[] bytes = byteObject.getBytes(); + final byte[] bytes = byteObject.getBytes(); return defineClass(className, bytes, 0, bytes.length); } @@ -31,15 +30,10 @@ public Class findClass(String className) throws ClassNotFoundException { } @Override - protected synchronized Class loadClass(final String name, final boolean resolve) throws ClassNotFoundException { - return super.loadClass(name, resolve); - } - - @Override - public InputStream getResourceAsStream(final String name) { + public InputStream getResourceAsStream(String name) { if (name.endsWith(Kind.CLASS.extension)) { - String qualifiedClassName = name.substring(0, name.length() - Kind.CLASS.extension.length()).replace('/', '.'); - DynamicByteObject file = classes.get(qualifiedClassName); + final String qualifiedClassName = name.substring(0, name.length() - Kind.CLASS.extension.length()).replace('/', '.'); + final DynamicByteObject file = classes.get(qualifiedClassName); if (file != null) { return new ByteArrayInputStream(file.getBytes()); } diff --git a/src/main/java/com/raulgomis/djc/DynamicCompiler.java b/src/main/java/com/raulgomis/djc/DynamicCompiler.java index 614f2db..ea562e9 100644 --- a/src/main/java/com/raulgomis/djc/DynamicCompiler.java +++ b/src/main/java/com/raulgomis/djc/DynamicCompiler.java @@ -1,5 +1,6 @@ package com.raulgomis.djc; +import java.util.List; import javax.tools.DiagnosticCollector; import javax.tools.JavaCompiler; import javax.tools.JavaCompiler.CompilationTask; @@ -7,15 +8,13 @@ import javax.tools.StandardJavaFileManager; import javax.tools.StandardLocation; import javax.tools.ToolProvider; -import java.util.Arrays; public final class DynamicCompiler { - private JavaCompiler compiler; - private DynamicExtendedFileManager dynamicExtendedFileManager; - private DynamicClassLoader classLoader; - - private DiagnosticCollector diagnostics; + private final JavaCompiler compiler; + private final DynamicExtendedFileManager dynamicExtendedFileManager; + private final DynamicClassLoader classLoader; + private final DiagnosticCollector diagnostics; public DynamicCompiler() throws DynamicCompilerException { compiler = ToolProvider.getSystemJavaCompiler(); @@ -26,31 +25,40 @@ public DynamicCompiler() throws DynamicCompilerException { classLoader = new DynamicClassLoader(Thread.currentThread().getContextClassLoader()); diagnostics = new DiagnosticCollector<>(); - StandardJavaFileManager standardFileManager = compiler.getStandardFileManager(diagnostics, null, null); + final StandardJavaFileManager standardFileManager = compiler.getStandardFileManager(diagnostics, null, null); dynamicExtendedFileManager = new DynamicExtendedFileManager(standardFileManager, classLoader); } @SuppressWarnings("unchecked") public synchronized Class compile(String packageName, String className, String javaSource) throws DynamicCompilerException { try { + final String qualifiedClassName = DynamicCompilerUtils.getQualifiedClassName(packageName, className); + final DynamicStringObject sourceObj = new DynamicStringObject(className, javaSource); - String qualifiedClassName = DynamicCompilerUtils.getQualifiedClassName(packageName, className); - DynamicStringObject sourceObj = new DynamicStringObject(className, javaSource); - - dynamicExtendedFileManager.putFileForInput(StandardLocation.SOURCE_PATH, packageName, - className + JavaFileObject.Kind.SOURCE.extension, sourceObj); + dynamicExtendedFileManager.putFileForInput( + StandardLocation.SOURCE_PATH, + packageName, + className + JavaFileObject.Kind.SOURCE.extension, + sourceObj + ); - CompilationTask task = compiler.getTask(null, dynamicExtendedFileManager, diagnostics, null, null, Arrays.asList(sourceObj)); - boolean result = task.call(); + final CompilationTask task = compiler.getTask( + null, + dynamicExtendedFileManager, + diagnostics, + null, + null, + List.of(sourceObj) + ); - if (!result) { + if (!task.call()) { throw new DynamicCompilerException("Compilation failure", diagnostics.getDiagnostics()); } dynamicExtendedFileManager.close(); - return (Class) classLoader.loadClass(qualifiedClassName); - + } catch (DynamicCompilerException exception) { + throw exception; } catch (Exception exception) { throw new DynamicCompilerException(exception, diagnostics.getDiagnostics()); } diff --git a/src/main/java/com/raulgomis/djc/DynamicCompilerException.java b/src/main/java/com/raulgomis/djc/DynamicCompilerException.java index b6eb814..c0c3709 100644 --- a/src/main/java/com/raulgomis/djc/DynamicCompilerException.java +++ b/src/main/java/com/raulgomis/djc/DynamicCompilerException.java @@ -1,34 +1,33 @@ package com.raulgomis.djc; +import java.util.List; import javax.tools.Diagnostic; import javax.tools.JavaFileObject; -import java.util.List; public final class DynamicCompilerException extends Exception { - private List> diagnostics; + private final List> diagnostics; public DynamicCompilerException(String message) { super(message); + this.diagnostics = List.of(); } public DynamicCompilerException(String message, List> diagnostics) { super(message); - this.diagnostics = diagnostics; + this.diagnostics = diagnostics == null ? List.of() : List.copyOf(diagnostics); } public DynamicCompilerException(Throwable e, List> diagnostics) { super(e); - this.diagnostics = diagnostics; + this.diagnostics = diagnostics == null ? List.of() : List.copyOf(diagnostics); } public String getDiagnosticsError() { - StringBuilder sb = new StringBuilder(); - if(diagnostics != null) { - diagnostics.forEach(diagnostic -> sb.append(String.format("Error on line %d: %s\n", - diagnostic.getLineNumber(), - diagnostic.getMessage(null)))); - } + final StringBuilder sb = new StringBuilder(); + diagnostics.forEach(diagnostic -> sb.append( + String.format("Error on line %d: %s%n", diagnostic.getLineNumber(), diagnostic.getMessage(null)) + )); return sb.toString(); } diff --git a/src/main/java/com/raulgomis/djc/DynamicCompilerUtils.java b/src/main/java/com/raulgomis/djc/DynamicCompilerUtils.java index 0356b31..e53c28b 100644 --- a/src/main/java/com/raulgomis/djc/DynamicCompilerUtils.java +++ b/src/main/java/com/raulgomis/djc/DynamicCompilerUtils.java @@ -1,31 +1,29 @@ package com.raulgomis.djc; -import javax.tools.JavaFileObject.Kind; import java.net.URI; import java.net.URISyntaxException; +import javax.tools.JavaFileObject.Kind; public final class DynamicCompilerUtils { - private DynamicCompilerUtils() { + public static final String EMPTY = ""; + private DynamicCompilerUtils() { } - public static final String EMPTY = ""; - static URI createURI(String str) { try { return new URI(str); } catch (URISyntaxException e) { - throw new RuntimeException(e); + throw new IllegalArgumentException("Invalid URI: " + str, e); } } static String getQualifiedClassName(String packageName, String className) { if (isEmpty(packageName)) { return className; - } else { - return packageName + "." + className; } + return packageName + "." + className; } static String getClassNameWithExt(String className) { @@ -33,7 +31,6 @@ static String getClassNameWithExt(String className) { } static boolean isEmpty(String str) { - return str == null || str.length() == 0; + return str == null || str.isEmpty(); } - -} \ No newline at end of file +} diff --git a/src/main/java/com/raulgomis/djc/DynamicExtendedFileManager.java b/src/main/java/com/raulgomis/djc/DynamicExtendedFileManager.java index e87340b..7782c53 100644 --- a/src/main/java/com/raulgomis/djc/DynamicExtendedFileManager.java +++ b/src/main/java/com/raulgomis/djc/DynamicExtendedFileManager.java @@ -1,19 +1,19 @@ package com.raulgomis.djc; +import java.io.IOException; +import java.net.URI; +import java.util.HashMap; +import java.util.Map; import javax.tools.FileObject; import javax.tools.ForwardingJavaFileManager; import javax.tools.JavaFileManager; import javax.tools.JavaFileObject; import javax.tools.JavaFileObject.Kind; import javax.tools.StandardLocation; -import java.io.IOException; -import java.net.URI; -import java.util.HashMap; -import java.util.Map; public final class DynamicExtendedFileManager extends ForwardingJavaFileManager { - private DynamicClassLoader classLoader; + private final DynamicClassLoader classLoader; private final Map fileObjects = new HashMap<>(); DynamicExtendedFileManager(JavaFileManager fileManager, DynamicClassLoader classLoader) { @@ -23,9 +23,9 @@ public final class DynamicExtendedFileManager extends ForwardingJavaFileManager< @Override public FileObject getFileForInput(Location location, String packageName, String relativeName) throws IOException { - FileObject o = fileObjects.get(uri(location, packageName, relativeName)); - if (o != null) { - return o; + final FileObject fileObject = fileObjects.get(uri(location, packageName, relativeName)); + if (fileObject != null) { + return fileObject; } return super.getFileForInput(location, packageName, relativeName); } @@ -38,11 +38,9 @@ private URI uri(Location location, String packageName, String relativeName) { return DynamicCompilerUtils.createURI(location.getName() + '/' + packageName + '/' + relativeName); } - @Override - public JavaFileObject getJavaFileForOutput(Location location, - String qualifiedName, Kind kind, FileObject outputFile) { - DynamicByteObject dynamicByteObject = new DynamicByteObject(qualifiedName, kind); + public JavaFileObject getJavaFileForOutput(Location location, String qualifiedName, Kind kind, FileObject outputFile) { + final DynamicByteObject dynamicByteObject = new DynamicByteObject(qualifiedName, kind); classLoader.addClass(dynamicByteObject); return dynamicByteObject; } @@ -53,10 +51,10 @@ public ClassLoader getClassLoader(JavaFileManager.Location location) { } @Override - public String inferBinaryName(Location loc, JavaFileObject file) { + public String inferBinaryName(Location location, JavaFileObject file) { if (file instanceof DynamicByteObject) { return file.getName(); } - return super.inferBinaryName(loc, file); + return super.inferBinaryName(location, file); } } diff --git a/src/main/java/com/raulgomis/djc/DynamicStringObject.java b/src/main/java/com/raulgomis/djc/DynamicStringObject.java index c7c5af7..d2b1059 100644 --- a/src/main/java/com/raulgomis/djc/DynamicStringObject.java +++ b/src/main/java/com/raulgomis/djc/DynamicStringObject.java @@ -3,6 +3,7 @@ import javax.tools.SimpleJavaFileObject; public class DynamicStringObject extends SimpleJavaFileObject { + private final String source; DynamicStringObject(String name, String source) { diff --git a/src/test/java/com/raulgomis/djc/DynamicCompilerTest.java b/src/test/java/com/raulgomis/djc/DynamicCompilerTest.java index 791cbb7..42c7d55 100644 --- a/src/test/java/com/raulgomis/djc/DynamicCompilerTest.java +++ b/src/test/java/com/raulgomis/djc/DynamicCompilerTest.java @@ -5,57 +5,41 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.assertThrows; class DynamicCompilerTest { @Test - void test01() { + void compilesAndInstantiatesSimpleRunnable() throws Exception { CompilerExecutor compilerExecutor = new CompilerExecutor(); - try { - Class clazz = compilerExecutor.compile("Test01.java"); - final Runnable r; - try { - r = clazz.newInstance(); - r.run(); - } catch (InstantiationException | IllegalAccessException e) { - e.printStackTrace(); - } - assertNotNull(clazz); - } catch (DynamicCompilerException e) { - e.printStackTrace(); - fail(); - } + + Class clazz = compilerExecutor.compile("Test01.java"); + Runnable runnable = clazz.getDeclaredConstructor().newInstance(); + + runnable.run(); + assertNotNull(clazz); } @Test - void test02() { + void returnsReadableDiagnosticsForCompilationFailure() { CompilerExecutor compilerExecutor = new CompilerExecutor(); - try { - compilerExecutor.compile("Test02.java"); - fail(); - } catch (DynamicCompilerException e) { - assertEquals("Error on line 3: ';' expected\n", - e.getDiagnosticsError()); - } + + DynamicCompilerException exception = assertThrows( + DynamicCompilerException.class, + () -> compilerExecutor.compile("Test02.java") + ); + + assertEquals("Error on line 3: ';' expected\n", exception.getDiagnosticsError()); } @Test - void test03() { - CompilerExecutor compilerExecutor = new CompilerExecutor(); - try { - Class clazz = compilerExecutor.compile("Test03.java"); - final Runnable r; - try { - r = clazz.newInstance(); - r.run(); - } catch (InstantiationException | IllegalAccessException e) { - e.printStackTrace(); - } - assertNotNull(clazz); - } catch (DynamicCompilerException e) { - e.printStackTrace(); - fail(); - } + void compilesRunnableWithImports() throws Exception { + CompilerExecutor compilerExecutor = new CompilerExecutor(); + + Class clazz = compilerExecutor.compile("Test03.java"); + Runnable runnable = clazz.getDeclaredConstructor().newInstance(); + + runnable.run(); + assertNotNull(clazz); } } diff --git a/src/test/java/com/raulgomis/djc/utils/CompilerExecutor.java b/src/test/java/com/raulgomis/djc/utils/CompilerExecutor.java index b3e1ce0..b618e92 100644 --- a/src/test/java/com/raulgomis/djc/utils/CompilerExecutor.java +++ b/src/test/java/com/raulgomis/djc/utils/CompilerExecutor.java @@ -3,15 +3,11 @@ import com.raulgomis.djc.DynamicCompiler; import com.raulgomis.djc.DynamicCompilerException; -/** - * @author rgomis - */ public class CompilerExecutor { - public Class compile(final String className) throws DynamicCompilerException { + public Class compile(String className) throws DynamicCompilerException { DynamicCompiler compiler = new DynamicCompiler<>(); CompilerTestInput compilerTestInput = CompilerTestUtils.getSource(className); - Class clazz = compiler.compile(null, compilerTestInput.getClassName(), compilerTestInput.getSource()); - return clazz; + return compiler.compile(null, compilerTestInput.getClassName(), compilerTestInput.getSource()); } } diff --git a/src/test/java/com/raulgomis/djc/utils/CompilerTestInput.java b/src/test/java/com/raulgomis/djc/utils/CompilerTestInput.java index 701ac0f..684c78a 100644 --- a/src/test/java/com/raulgomis/djc/utils/CompilerTestInput.java +++ b/src/test/java/com/raulgomis/djc/utils/CompilerTestInput.java @@ -1,7 +1,6 @@ package com.raulgomis.djc.utils; import com.raulgomis.djc.DynamicCompilerUtils; - import java.io.Serializable; import java.util.Objects; @@ -11,9 +10,13 @@ class CompilerTestInput implements Serializable { static final CompilerTestInput EMPTY = new CompilerTestInput(DynamicCompilerUtils.EMPTY, DynamicCompilerUtils.EMPTY); - private String className; + private final String className; + private final String source; - private String source; + CompilerTestInput(String className, String source) { + this.className = className; + this.source = source; + } String getClassName() { return className; @@ -23,19 +26,16 @@ String getSource() { return source; } - CompilerTestInput(String className, String source) { - this.className = className; - this.source = source; - } - - @Override public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } CompilerTestInput that = (CompilerTestInput) o; - return Objects.equals(className, that.className) && - Objects.equals(source, that.source); + return Objects.equals(className, that.className) && Objects.equals(source, that.source); } @Override @@ -46,8 +46,8 @@ public int hashCode() { @Override public String toString() { return "CompilerTestInput{" + - "className='" + className + '\'' + - ", source='" + source + '\'' + - '}'; + "className='" + className + '\'' + + ", source='" + source + '\'' + + '}'; } } diff --git a/src/test/java/com/raulgomis/djc/utils/CompilerTestUtils.java b/src/test/java/com/raulgomis/djc/utils/CompilerTestUtils.java index 96d5dba..0089ad7 100644 --- a/src/test/java/com/raulgomis/djc/utils/CompilerTestUtils.java +++ b/src/test/java/com/raulgomis/djc/utils/CompilerTestUtils.java @@ -1,7 +1,6 @@ package com.raulgomis.djc.utils; import com.raulgomis.djc.DynamicCompilerUtils; - import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; @@ -10,29 +9,23 @@ import java.nio.file.Path; import java.nio.file.Paths; -/** - * @author rgomis - */ class CompilerTestUtils { private static final String SPLIT_EXPRESSION = "\\."; private CompilerTestUtils() { - } static CompilerTestInput getSource(String fileName) { try { - URI uri = DynamicCompilerUtils.class.getResource("/" + fileName).toURI(); + URI uri = java.util.Objects.requireNonNull(DynamicCompilerUtils.class.getResource("/" + fileName)).toURI(); Path path = Paths.get(uri); - byte[] result = Files.readAllBytes(path); - String source = new String(result, StandardCharsets.UTF_8); + String source = Files.readString(path, StandardCharsets.UTF_8); String className = path.getFileName().toString().split(SPLIT_EXPRESSION)[0]; return new CompilerTestInput(className, source); } catch (URISyntaxException | IOException e) { - e.printStackTrace(); return CompilerTestInput.EMPTY; } } From d1590eaf4a572871db4f0f4a2c59db5386764015 Mon Sep 17 00:00:00 2001 From: Raul Gomis Date: Sat, 18 Jul 2026 23:14:06 +1000 Subject: [PATCH 2/4] Improve compatibility, diagnostics, and test coverage --- .github/workflows/feature-branches.yml | 18 +- .github/workflows/master.yml | 18 +- AGENTS.md | 67 +++++++ PULL_REQUEST.md | 41 ++++ README.md | 177 +++++++++++++++--- pom.xml | 22 ++- .../com/raulgomis/djc/DynamicCompiler.java | 5 +- .../djc/DynamicCompilerException.java | 2 +- .../raulgomis/djc/DynamicClassLoaderTest.java | 44 +++++ .../djc/DynamicCompilerExceptionTest.java | 64 +++++++ .../raulgomis/djc/DynamicCompilerTest.java | 64 +++++++ .../djc/DynamicExtendedFileManagerTest.java | 60 ++++++ 12 files changed, 548 insertions(+), 34 deletions(-) create mode 100644 AGENTS.md create mode 100644 PULL_REQUEST.md create mode 100644 src/test/java/com/raulgomis/djc/DynamicClassLoaderTest.java create mode 100644 src/test/java/com/raulgomis/djc/DynamicCompilerExceptionTest.java create mode 100644 src/test/java/com/raulgomis/djc/DynamicExtendedFileManagerTest.java diff --git a/.github/workflows/feature-branches.yml b/.github/workflows/feature-branches.yml index 68cd5a3..2b067b4 100644 --- a/.github/workflows/feature-branches.yml +++ b/.github/workflows/feature-branches.yml @@ -16,16 +16,26 @@ jobs: test: if: github.event_name != 'pull_request' || github.event.pull_request.draft == false runs-on: ubuntu-latest + strategy: + matrix: + java: ['17', '21', '25', '26'] steps: - name: Checkout uses: actions/checkout@v4 - - name: Set up JDK 25 + - name: Set up JDK ${{ matrix.java }} uses: actions/setup-java@v4 with: distribution: temurin - java-version: '25' + java-version: ${{ matrix.java }} cache: maven - - name: Run unit tests - run: mvn -B test --file pom.xml + - name: Run tests and generate coverage report + run: mvn -B verify --file pom.xml + + - name: Upload coverage report + uses: actions/upload-artifact@v4 + with: + name: jacoco-coverage-report-java-${{ matrix.java }} + path: target/site/jacoco/ + if-no-files-found: error diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index d90a5d6..f047000 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -12,16 +12,26 @@ permissions: jobs: build: runs-on: ubuntu-latest + strategy: + matrix: + java: ['17', '21', '25', '26'] steps: - name: Checkout uses: actions/checkout@v4 - - name: Set up JDK 25 + - name: Set up JDK ${{ matrix.java }} uses: actions/setup-java@v4 with: distribution: temurin - java-version: '25' + java-version: ${{ matrix.java }} cache: maven - - name: Run tests - run: mvn -B test --file pom.xml + - name: Run tests and generate coverage report + run: mvn -B verify --file pom.xml + + - name: Upload coverage report + uses: actions/upload-artifact@v4 + with: + name: jacoco-coverage-report-java-${{ matrix.java }} + path: target/site/jacoco/ + if-no-files-found: error diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..ae73a0c --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,67 @@ +# AGENTS.md + +## Project overview + +This repository contains a small Java library for compiling and loading Java +source code at runtime. Production code is under +`src/main/java/com/raulgomis/djc`, and tests are under the matching package in +`src/test/java`. + +The project targets Java 17 bytecode and uses Maven, JUnit 5, and JaCoCo. + +## Build and verification + +Run commands from the repository root. + +- `mvn test` runs the unit tests. +- `mvn verify` runs the tests and generates the JaCoCo report. +- `mvn clean verify` performs a clean, full verification when stale build + output may matter. +- The HTML coverage report is written to + `target/site/jacoco/index.html`. + +Before handing off a code change, run `mvn verify`. Also run +`git diff --check` to catch whitespace errors. + +## Implementation guidelines + +- Keep the public API small and compatible unless the task explicitly calls + for an API change. +- Compile with Java 17-compatible language and library features. +- Follow the existing four-space indentation and brace style. +- Prefer standard-library solutions; avoid adding dependencies for behavior + that can be implemented clearly without them. +- Keep compiler diagnostics associated with the individual compilation that + produced them. +- Preserve the in-memory design: generated source and bytecode should not + require temporary source or class files. +- Treat package names, binary class names, source file names, and resource + paths carefully. A source package and the qualified name used for loading + must agree. +- Preserve exception causes when wrapping unexpected compiler, file-manager, + or class-loading failures. + +## Testing guidelines + +- Use JUnit Jupiter. +- Name tests after observable behavior, such as + `compilesAClassInANamedPackage`. +- Add regression tests for bug fixes and focused tests for new branches. +- Test both successful compilation and relevant failure behavior, including + diagnostics and wrapped causes. +- Tests for package-private implementation classes should remain in + `com.raulgomis.djc` rather than widening production visibility. +- Prefer small inline source strings for a single scenario. Use + `src/test/resources` when a source fixture is substantial or reused. +- Dynamically compiled test code must be deterministic and must not perform + network access, modify user files, or depend on machine-specific state. +- Do not weaken assertions merely to increase the coverage percentage. + +## Change hygiene + +- The worktree may already contain user changes. Inspect `git status` and + preserve unrelated modifications. +- Do not edit generated files under `target`. +- Keep documentation and CI configuration consistent with the Java version + and Maven commands in `pom.xml`. +- Avoid broad formatting or refactoring unrelated to the requested change. diff --git a/PULL_REQUEST.md b/PULL_REQUEST.md new file mode 100644 index 0000000..15e258d --- /dev/null +++ b/PULL_REQUEST.md @@ -0,0 +1,41 @@ +# Improve Java compatibility, diagnostics, coverage, and documentation + +## Summary + +This change strengthens the library's compatibility and quality checks, fixes +diagnostic state leaking between compilations, expands test coverage, and +provides clearer documentation for users and contributors. + +## Changes + +- Target Java 17 bytecode so the library remains usable on Java 17 and later. +- Test the project in CI on Java 17, 21, 25, and 26. +- Run the Maven `verify` lifecycle in CI and upload the generated JaCoCo report + for each tested JDK. +- Create a fresh diagnostic collector for every compilation so failures do not + retain diagnostics from earlier attempts. +- Make diagnostic error output use a stable newline across operating systems. +- Add focused tests for: + - independent diagnostics across compilation failures + - named-package compilation + - wrapped class-loading failures + - dynamic class and resource loading + - file-manager lookup, delegation, output, and binary-name handling + - exception formatting, defensive copies, null diagnostics, and causes +- Expand the README with requirements, installation, usage, package and error + examples, security guidance, and contributor instructions. +- Add repository-specific guidance for coding agents in `AGENTS.md`. + +## Verification + +- [x] `mvn clean verify` +- [x] 26 tests pass +- [x] 97.7% line coverage +- [x] 91.7% branch coverage +- [x] 97.7% instruction coverage +- [x] `git diff --check` + +## Notes + +Dynamically compiled code is not sandboxed and runs with the permissions of the +host application. The updated README now calls this out explicitly. diff --git a/README.md b/README.md index 80e6f95..4918302 100644 --- a/README.md +++ b/README.md @@ -1,42 +1,177 @@ -# What is dynamic-java-compiler? -![Build](https://github.com/raulgomis/dynamic-java-compiler/workflows/Build/badge.svg) +# Dynamic Java Compiler -_Dynamic-java-compiler_ is a library that allows users to dynamically compile and execute Java source code. Writing dynamically executed Java applications usually requires boilerplate code (working with classloaders, compilation error handling, etc.). This library removes that overhead so you can focus on business logic. +[![Build](https://github.com/raulgomis/dynamic-java-compiler/actions/workflows/master.yml/badge.svg)](https://github.com/raulgomis/dynamic-java-compiler/actions/workflows/master.yml) -## Java compatibility +Compile Java source held in a string and load the resulting class directly into +the running JVM. -This project is configured to compile using Java **25** (`maven.compiler.release=25`) and has tests updated for modern JDK APIs. +Dynamic Java Compiler wraps the standard Java compiler, an in-memory file +manager, and a dedicated class loader behind a small API. Source and generated +bytecode stay in memory, so callers do not need to manage temporary `.java` or +`.class` files. -## How does it work? +## Features -The dynamic compilation task is very simple with this library. Imagine we want to compile this source code introduced dynamically as a text string by the user: +- Compiles Java source at runtime using the JDK compiler +- Loads generated classes without writing bytecode to disk +- Supports packages, imports, and nested classes +- Returns structured compiler diagnostics on failure +- Has no runtime dependencies outside the JDK + +## Requirements + +- Java 17 or later +- A full JDK that includes the system Java compiler + +The project is compiled with `maven.compiler.release=17`, so its bytecode +remains compatible with Java 17 while builds can run on newer JDKs. A minimal +runtime image without the `jdk.compiler` module cannot perform dynamic +compilation. + +## Installation + +The current project version is `0.2-SNAPSHOT`. To install it in your local +Maven repository: + +```shell +git clone https://github.com/raulgomis/dynamic-java-compiler.git +cd dynamic-java-compiler +mvn install +``` + +Then add the dependency to your Maven project: + +```xml + + com.raulgomis + dynamic-java-compiler + 0.2-SNAPSHOT + +``` + +## Quick start + +Create the source code to compile: ```java -public class Test01 implements Runnable { - public void run() { - System.out.println("Hello World!"); +String source = """ + public class GreetingTask implements Runnable { + @Override + public void run() { + System.out.println("Hello from dynamically compiled code!"); + } } -} + """; ``` -Now instantiate the compiler and compile the code: +Compile, instantiate, and execute it: ```java +import com.raulgomis.djc.DynamicCompiler; + DynamicCompiler compiler = new DynamicCompiler<>(); -// Read source code as String -Class clazz = compiler.compile(null, "Test01", source); -Runnable runnable = clazz.getDeclaredConstructor().newInstance(); -runnable.run(); +Class taskClass = compiler.compile( + null, + "GreetingTask", + source +); + +Runnable task = taskClass.getDeclaredConstructor().newInstance(); +task.run(); ``` -The final result will be: +The first argument to `compile` is the package name. Pass `null` or an empty +string for the default package. The second argument is the simple class name +and must agree with the class declared by the source. + +## Compiling a packaged class + +The package declared in the source must match the package passed to +`compile`: +```java +String source = """ + package example.tasks; + + public class GreetingTask implements Runnable { + @Override + public void run() { + System.out.println("Hello!"); + } + } + """; + +DynamicCompiler compiler = new DynamicCompiler<>(); +Class taskClass = compiler.compile( + "example.tasks", + "GreetingTask", + source +); ``` -Hello World! + +The returned class has the binary name `example.tasks.GreetingTask`. + +## Handling compilation errors + +Compilation failures throw `DynamicCompilerException`. Use +`getDiagnostics()` for structured JDK diagnostics or +`getDiagnosticsError()` for a readable summary: + +```java +import com.raulgomis.djc.DynamicCompilerException; + +try { + DynamicCompiler compiler = new DynamicCompiler<>(); + compiler.compile(null, "BrokenTask", brokenSource); +} catch (DynamicCompilerException exception) { + System.err.print(exception.getDiagnosticsError()); + + exception.getDiagnostics().forEach(diagnostic -> { + System.err.printf( + "line %d, column %d: %s%n", + diagnostic.getLineNumber(), + diagnostic.getColumnNumber(), + diagnostic.getMessage(null) + ); + }); +} ``` -## Contribution +Diagnostic wording is produced by the active JDK and may differ between JDK +versions. + +## Security + +This library compiles code; it does not sandbox it. Once loaded and invoked, +dynamically compiled code runs with the same permissions as the host +application. Do not compile or execute untrusted source without a separate, +appropriately isolated execution environment. + +## Building and testing + +Run the full verification build: + +```shell +mvn verify +``` + +This runs the JUnit 5 test suite and generates a JaCoCo coverage report at +`target/site/jacoco/index.html`. + +For a clean build: + +```shell +mvn clean verify +``` + +## Contributing + +Pull requests are welcome. Please include focused tests for behavioral changes +and run `mvn verify` before submitting. + +Use the [issue tracker](https://github.com/raulgomis/dynamic-java-compiler/issues) +to report bugs or propose features. -You are welcome to contribute to the project using pull requests on GitHub. +## License -If you find a bug or want to request a feature, please use the [issue tracker](https://github.com/raulgomis/dynamic-java-compiler/issues) on GitHub. +This project is available under the [MIT License](LICENSE). diff --git a/pom.xml b/pom.xml index 169cef3..e14c44e 100644 --- a/pom.xml +++ b/pom.xml @@ -29,7 +29,8 @@ UTF-8 - 25 + 17 + 0.8.15 5.12.2 @@ -57,6 +58,25 @@ maven-surefire-plugin 3.5.4 + + org.jacoco + jacoco-maven-plugin + ${jacoco.version} + + + + prepare-agent + + + + report + verify + + report + + + + diff --git a/src/main/java/com/raulgomis/djc/DynamicCompiler.java b/src/main/java/com/raulgomis/djc/DynamicCompiler.java index ea562e9..d426c30 100644 --- a/src/main/java/com/raulgomis/djc/DynamicCompiler.java +++ b/src/main/java/com/raulgomis/djc/DynamicCompiler.java @@ -14,7 +14,6 @@ public final class DynamicCompiler { private final JavaCompiler compiler; private final DynamicExtendedFileManager dynamicExtendedFileManager; private final DynamicClassLoader classLoader; - private final DiagnosticCollector diagnostics; public DynamicCompiler() throws DynamicCompilerException { compiler = ToolProvider.getSystemJavaCompiler(); @@ -23,14 +22,14 @@ public DynamicCompiler() throws DynamicCompilerException { } classLoader = new DynamicClassLoader(Thread.currentThread().getContextClassLoader()); - diagnostics = new DiagnosticCollector<>(); - final StandardJavaFileManager standardFileManager = compiler.getStandardFileManager(diagnostics, null, null); + final StandardJavaFileManager standardFileManager = compiler.getStandardFileManager(null, null, null); dynamicExtendedFileManager = new DynamicExtendedFileManager(standardFileManager, classLoader); } @SuppressWarnings("unchecked") public synchronized Class compile(String packageName, String className, String javaSource) throws DynamicCompilerException { + final DiagnosticCollector diagnostics = new DiagnosticCollector<>(); try { final String qualifiedClassName = DynamicCompilerUtils.getQualifiedClassName(packageName, className); final DynamicStringObject sourceObj = new DynamicStringObject(className, javaSource); diff --git a/src/main/java/com/raulgomis/djc/DynamicCompilerException.java b/src/main/java/com/raulgomis/djc/DynamicCompilerException.java index c0c3709..b8078af 100644 --- a/src/main/java/com/raulgomis/djc/DynamicCompilerException.java +++ b/src/main/java/com/raulgomis/djc/DynamicCompilerException.java @@ -26,7 +26,7 @@ public DynamicCompilerException(Throwable e, List sb.append( - String.format("Error on line %d: %s%n", diagnostic.getLineNumber(), diagnostic.getMessage(null)) + String.format("Error on line %d: %s\n", diagnostic.getLineNumber(), diagnostic.getMessage(null)) )); return sb.toString(); } diff --git a/src/test/java/com/raulgomis/djc/DynamicClassLoaderTest.java b/src/test/java/com/raulgomis/djc/DynamicClassLoaderTest.java new file mode 100644 index 0000000..7549d03 --- /dev/null +++ b/src/test/java/com/raulgomis/djc/DynamicClassLoaderTest.java @@ -0,0 +1,44 @@ +package com.raulgomis.djc; + +import org.junit.jupiter.api.Test; + +import java.io.InputStream; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class DynamicClassLoaderTest { + + @Test + void exposesCompiledClassesAsClassResources() throws Exception { + DynamicClassLoader classLoader = new DynamicClassLoader(getClass().getClassLoader()); + DynamicByteObject byteObject = new DynamicByteObject("example.Generated", javax.tools.JavaFileObject.Kind.CLASS); + byte[] bytecode = {1, 2, 3, 4}; + byteObject.openOutputStream().write(bytecode); + classLoader.addClass(byteObject); + + try (InputStream resource = classLoader.getResourceAsStream("example/Generated.class")) { + assertNotNull(resource); + assertArrayEquals(bytecode, resource.readAllBytes()); + } + } + + @Test + void delegatesUnknownResourcesToItsParent() throws Exception { + DynamicClassLoader classLoader = new DynamicClassLoader(getClass().getClassLoader()); + + try (InputStream resource = classLoader.getResourceAsStream("Test01.java")) { + assertNotNull(resource); + } + assertNull(classLoader.getResourceAsStream("missing/Type.class")); + } + + @Test + void reportsUnknownClasses() { + DynamicClassLoader classLoader = new DynamicClassLoader(null); + + assertThrows(ClassNotFoundException.class, () -> classLoader.findClass("missing.Type")); + } +} diff --git a/src/test/java/com/raulgomis/djc/DynamicCompilerExceptionTest.java b/src/test/java/com/raulgomis/djc/DynamicCompilerExceptionTest.java new file mode 100644 index 0000000..223068f --- /dev/null +++ b/src/test/java/com/raulgomis/djc/DynamicCompilerExceptionTest.java @@ -0,0 +1,64 @@ +package com.raulgomis.djc; + +import org.junit.jupiter.api.Test; + +import javax.tools.Diagnostic; +import javax.tools.JavaFileObject; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class DynamicCompilerExceptionTest { + + @Test + void messageOnlyExceptionHasNoDiagnostics() { + DynamicCompilerException exception = new DynamicCompilerException("Compiler not found"); + + assertEquals("Compiler not found", exception.getMessage()); + assertTrue(exception.getDiagnostics().isEmpty()); + assertEquals("", exception.getDiagnosticsError()); + assertEquals("", exception.toString()); + assertTrue(new DynamicCompilerException("Compilation failure", null).getDiagnostics().isEmpty()); + } + + @Test + void snapshotsDiagnosticsAndFormatsThem() { + List> diagnostics = new ArrayList<>(); + diagnostics.add(diagnostic(7, "bad source")); + + DynamicCompilerException exception = new DynamicCompilerException("Compilation failure", diagnostics); + diagnostics.clear(); + + assertEquals("Error on line 7: bad source\n", exception.getDiagnosticsError()); + assertEquals(exception.getDiagnosticsError(), exception.toString()); + assertThrows(UnsupportedOperationException.class, () -> exception.getDiagnostics().clear()); + } + + @Test + void throwableConstructorPreservesCauseAndHandlesNullDiagnostics() { + IllegalStateException cause = new IllegalStateException("closed"); + + DynamicCompilerException exception = new DynamicCompilerException(cause, null); + + assertSame(cause, exception.getCause()); + assertTrue(exception.getDiagnostics().isEmpty()); + } + + private static Diagnostic diagnostic(long line, String message) { + return new Diagnostic<>() { + @Override public Kind getKind() { return Kind.ERROR; } + @Override public JavaFileObject getSource() { return null; } + @Override public long getPosition() { return NOPOS; } + @Override public long getStartPosition() { return NOPOS; } + @Override public long getEndPosition() { return NOPOS; } + @Override public long getLineNumber() { return line; } + @Override public long getColumnNumber() { return NOPOS; } + @Override public String getCode() { return "test"; } + @Override public String getMessage(java.util.Locale locale) { return message; } + }; + } +} diff --git a/src/test/java/com/raulgomis/djc/DynamicCompilerTest.java b/src/test/java/com/raulgomis/djc/DynamicCompilerTest.java index 42c7d55..cad4165 100644 --- a/src/test/java/com/raulgomis/djc/DynamicCompilerTest.java +++ b/src/test/java/com/raulgomis/djc/DynamicCompilerTest.java @@ -4,8 +4,10 @@ import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; class DynamicCompilerTest { @@ -42,4 +44,66 @@ void compilesRunnableWithImports() throws Exception { runnable.run(); assertNotNull(clazz); } + + @Test + void keepsDiagnosticsIndependentAcrossCompilationFailures() throws Exception { + DynamicCompiler compiler = new DynamicCompiler<>(); + + DynamicCompilerException firstFailure = assertThrows( + DynamicCompilerException.class, + () -> compiler.compile( + null, + "BrokenOne", + "public class BrokenOne implements Runnable { public void run() { nope } }" + ) + ); + int firstFailureDiagnosticCount = firstFailure.getDiagnostics().size(); + + DynamicCompilerException secondFailure = assertThrows( + DynamicCompilerException.class, + () -> compiler.compile( + null, + "BrokenTwo", + "public class BrokenTwo implements Runnable { public void run() { nope } }" + ) + ); + + assertTrue(firstFailureDiagnosticCount > 0); + assertEquals(firstFailureDiagnosticCount, firstFailure.getDiagnostics().size()); + assertEquals(firstFailureDiagnosticCount, secondFailure.getDiagnostics().size()); + assertTrue(secondFailure.getDiagnostics().stream().allMatch( + diagnostic -> diagnostic.getSource().getName().contains("BrokenTwo") + )); + } + + @Test + void compilesAClassInANamedPackage() throws Exception { + DynamicCompiler compiler = new DynamicCompiler<>(); + + Class clazz = compiler.compile( + "example.generated", + "PackagedTask", + "package example.generated; public class PackagedTask implements Runnable { public void run() {} }" + ); + + assertEquals("example.generated.PackagedTask", clazz.getName()); + assertNotNull(clazz.getDeclaredConstructor().newInstance()); + } + + @Test + void wrapsClassLoadingFailuresWithTheirCause() throws Exception { + DynamicCompiler compiler = new DynamicCompiler<>(); + + DynamicCompilerException exception = assertThrows( + DynamicCompilerException.class, + () -> compiler.compile( + "expected.package", + "UnexpectedPackage", + "public class UnexpectedPackage implements Runnable { public void run() {} }" + ) + ); + + assertInstanceOf(ClassNotFoundException.class, exception.getCause()); + assertTrue(exception.getDiagnostics().isEmpty()); + } } diff --git a/src/test/java/com/raulgomis/djc/DynamicExtendedFileManagerTest.java b/src/test/java/com/raulgomis/djc/DynamicExtendedFileManagerTest.java new file mode 100644 index 0000000..65695b3 --- /dev/null +++ b/src/test/java/com/raulgomis/djc/DynamicExtendedFileManagerTest.java @@ -0,0 +1,60 @@ +package com.raulgomis.djc; + +import org.junit.jupiter.api.Test; + +import javax.tools.JavaCompiler; +import javax.tools.JavaFileObject; +import javax.tools.StandardJavaFileManager; +import javax.tools.StandardLocation; +import javax.tools.ToolProvider; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +class DynamicExtendedFileManagerTest { + + @Test + void returnsRegisteredInputsAndDelegatesPlatformLookups() throws Exception { + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + try (StandardJavaFileManager standardManager = compiler.getStandardFileManager(null, null, null); + DynamicExtendedFileManager manager = new DynamicExtendedFileManager( + standardManager, + new DynamicClassLoader(getClass().getClassLoader()) + )) { + DynamicStringObject source = new DynamicStringObject("Example", "class Example {}"); + manager.putFileForInput(StandardLocation.SOURCE_PATH, "example", "Example.java", source); + + assertSame(source, manager.getFileForInput(StandardLocation.SOURCE_PATH, "example", "Example.java")); + assertNotNull(manager.getFileForInput( + StandardLocation.PLATFORM_CLASS_PATH, + "java.lang", + "String.class" + )); + } + } + + @Test + void createsOutputsAndInfersBinaryNamesForBothFileKinds() throws Exception { + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + DynamicClassLoader classLoader = new DynamicClassLoader(getClass().getClassLoader()); + try (StandardJavaFileManager standardManager = compiler.getStandardFileManager(null, null, null); + DynamicExtendedFileManager manager = new DynamicExtendedFileManager(standardManager, classLoader)) { + JavaFileObject output = manager.getJavaFileForOutput( + StandardLocation.CLASS_OUTPUT, + "example.Generated", + JavaFileObject.Kind.CLASS, + null + ); + JavaFileObject stringClass = standardManager.getJavaFileForInput( + StandardLocation.PLATFORM_CLASS_PATH, + "java.lang.String", + JavaFileObject.Kind.CLASS + ); + + assertEquals("example.Generated", manager.inferBinaryName(StandardLocation.CLASS_OUTPUT, output)); + assertEquals("java.lang.String", manager.inferBinaryName(StandardLocation.PLATFORM_CLASS_PATH, stringClass)); + assertSame(classLoader, manager.getClassLoader(StandardLocation.CLASS_PATH)); + } + } +} From 5999c1a93de028eb0d94f2d8a3b1fb32cf4b2428 Mon Sep 17 00:00:00 2001 From: Raul Gomis Date: Sat, 18 Jul 2026 23:15:45 +1000 Subject: [PATCH 3/4] Remove snapshot installation instructions --- README.md | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/README.md b/README.md index 4918302..a6fb30d 100644 --- a/README.md +++ b/README.md @@ -28,27 +28,6 @@ remains compatible with Java 17 while builds can run on newer JDKs. A minimal runtime image without the `jdk.compiler` module cannot perform dynamic compilation. -## Installation - -The current project version is `0.2-SNAPSHOT`. To install it in your local -Maven repository: - -```shell -git clone https://github.com/raulgomis/dynamic-java-compiler.git -cd dynamic-java-compiler -mvn install -``` - -Then add the dependency to your Maven project: - -```xml - - com.raulgomis - dynamic-java-compiler - 0.2-SNAPSHOT - -``` - ## Quick start Create the source code to compile: From f8f76572a0364e36baf509716e8ee3381aa5d701 Mon Sep 17 00:00:00 2001 From: Raul Gomis Date: Sat, 18 Jul 2026 23:19:17 +1000 Subject: [PATCH 4/4] Remove pull request description file --- PULL_REQUEST.md | 41 ----------------------------------------- 1 file changed, 41 deletions(-) delete mode 100644 PULL_REQUEST.md diff --git a/PULL_REQUEST.md b/PULL_REQUEST.md deleted file mode 100644 index 15e258d..0000000 --- a/PULL_REQUEST.md +++ /dev/null @@ -1,41 +0,0 @@ -# Improve Java compatibility, diagnostics, coverage, and documentation - -## Summary - -This change strengthens the library's compatibility and quality checks, fixes -diagnostic state leaking between compilations, expands test coverage, and -provides clearer documentation for users and contributors. - -## Changes - -- Target Java 17 bytecode so the library remains usable on Java 17 and later. -- Test the project in CI on Java 17, 21, 25, and 26. -- Run the Maven `verify` lifecycle in CI and upload the generated JaCoCo report - for each tested JDK. -- Create a fresh diagnostic collector for every compilation so failures do not - retain diagnostics from earlier attempts. -- Make diagnostic error output use a stable newline across operating systems. -- Add focused tests for: - - independent diagnostics across compilation failures - - named-package compilation - - wrapped class-loading failures - - dynamic class and resource loading - - file-manager lookup, delegation, output, and binary-name handling - - exception formatting, defensive copies, null diagnostics, and causes -- Expand the README with requirements, installation, usage, package and error - examples, security guidance, and contributor instructions. -- Add repository-specific guidance for coding agents in `AGENTS.md`. - -## Verification - -- [x] `mvn clean verify` -- [x] 26 tests pass -- [x] 97.7% line coverage -- [x] 91.7% branch coverage -- [x] 97.7% instruction coverage -- [x] `git diff --check` - -## Notes - -Dynamically compiled code is not sandboxed and runs with the permissions of the -host application. The updated README now calls this out explicitly.