This is an automated email from the ASF dual-hosted git repository.

daniellansun pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git


The following commit(s) were added to refs/heads/master by this push:
     new 5d09bb508f GROOVY-12026: Graduate JavaShell from incubating to stable
5d09bb508f is described below

commit 5d09bb508f916c6c2cc6a0d239d2c947d6191cff
Author: Paul King <[email protected]>
AuthorDate: Fri May 22 10:13:20 2026 +1000

    GROOVY-12026: Graduate JavaShell from incubating to stable
---
 .../java/org/apache/groovy/util/JavaShell.java     |  7 +-
 src/spec/doc/core-testing-guide.adoc               | 41 ++++++++++++
 src/spec/doc/guide-integrating.adoc                | 55 ++++++++++++++++
 src/spec/test/IntegrationTest.groovy               | 55 ++++++++++++++++
 .../test/testingguide/JavaShellExampleTests.groovy | 75 ++++++++++++++++++++++
 .../main/groovy/groovy/console/ui/Console.groovy   |  2 +-
 .../src/spec/doc/groovy-console.adoc               | 17 +++++
 7 files changed, 250 insertions(+), 2 deletions(-)

diff --git a/src/main/java/org/apache/groovy/util/JavaShell.java 
b/src/main/java/org/apache/groovy/util/JavaShell.java
index 00892147f8..5155cac6dd 100644
--- a/src/main/java/org/apache/groovy/util/JavaShell.java
+++ b/src/main/java/org/apache/groovy/util/JavaShell.java
@@ -81,7 +81,6 @@ import java.util.concurrent.ConcurrentHashMap;
  * additionally written to disk by {@code compileAllTo}). See the {@code 
javac} documentation
  * for the complete list of supported flags.
  */
-@Incubating
 public class JavaShell {
     private static final String MAIN_METHOD_NAME = "main";
     private static final URL[] EMPTY_URL_ARRAY = new URL[0];
@@ -194,6 +193,10 @@ public class JavaShell {
 
     private void doCompile(String className, String src, Iterable<String> 
options) throws IOException {
         JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
+        if (compiler == null) {
+            throw new IllegalStateException("JavaShell requires a JDK at 
runtime; "
+                    + "ToolProvider.getSystemJavaCompiler() returned null 
(running on a JRE?).");
+        }
         try (BytesJavaFileManager bjfm = new 
BytesJavaFileManager(compiler.getStandardFileManager(null, locale, charset))) {
             StringBuilderWriter out = new StringBuilderWriter();
             JavaCompiler.CompilationTask task =
@@ -240,6 +243,7 @@ public class JavaShell {
      * @throws JavaShellCompilationException if the source fails to compile
      * @since 6.0.0
      */
+    @Incubating
     public Map<String, Path> compileAllTo(String className, Iterable<String> 
options,
                                            String src, Path outputDir)
         throws IOException {
@@ -269,6 +273,7 @@ public class JavaShell {
      * @throws JavaShellCompilationException if the source fails to compile
      * @since 6.0.0
      */
+    @Incubating
     public Map<String, Path> compileAllTo(String className, String src, Path 
outputDir)
         throws IOException {
         return compileAllTo(className, Collections.emptyList(), src, 
outputDir);
diff --git a/src/spec/doc/core-testing-guide.adoc 
b/src/spec/doc/core-testing-guide.adoc
index 1d3f128755..685a57b72b 100644
--- a/src/spec/doc/core-testing-guide.adoc
+++ b/src/spec/doc/core-testing-guide.adoc
@@ -21,6 +21,10 @@
 
 = Testing Guide
 
+ifndef::guide-integrating[]
+:guide-integrating: guide-integrating.adoc
+endif::[]
+
 == Introduction
 
 The Groovy programming language comes with great support for writing tests. In 
addition to the language
@@ -355,6 +359,43 @@ cobertura {
 Several output formats can be chosen for Cobertura coverage reports and test 
code coverage reports can be added to
 continuous integration build tasks.
 
+==== JavaShell
+
+The `org.apache.groovy.util.JavaShell` class (covered in detail in the
+<<{guide-integrating}#integ-javashell,integration guide>>) is also useful in 
tests when you need
+real Java classes alongside your Groovy code. It avoids two recurring 
frictions:
+
+* you can declare Java fixture classes _inside_ the test that uses them, 
instead of adding
+  `src/test/java/...` files that exist only to support one Groovy test;
+* you can capture genuine `javac`-emitted bytecode for use cases such as 
comparing what the
+  Groovy compiler produces against what Java produces for an equivalent source 
— useful when
+  asserting joint-compilation behaviour, annotation-processing output, or 
`@CompileStatic`
+  codegen equivalence.
+
+A short fixture example:
+
+[source,groovy]
+----
+include::../test/testingguide/JavaShellExampleTests.groovy[tags=javashell_test_fixture,indent=0]
+----
+<1> compile a Java POJO in-memory; no `.java` file is added to the test source 
tree
+<2> attach `JavaShell`'s class loader to a `GroovyShell` so the test's Groovy 
code can resolve
+    the freshly-compiled class
+<3> exercise the fixture from Groovy
+
+For a bytecode-equivalence assertion, `compileAllTo` writes the generated 
`.class` files to a
+temporary directory from which the bytes can be read and fed into a 
bytecode-diff tool of choice
+(ASM, `javap` output, etc.). The corresponding Groovy-side bytecode can be 
obtained through the
+`GroovyClassLoader` / `CompilationUnit` paths used elsewhere in Groovy's own 
test suite.
+
+[source,groovy]
+----
+include::../test/testingguide/JavaShellExampleTests.groovy[tags=javashell_bytecode_capture,indent=0]
+----
+<1> compile and write the Java class to a temp directory
+<2> read the class bytes back from disk
+<3> compare against equivalent Groovy bytes obtained however best suits your 
test
+
 == Testing with JUnit
 
 Groovy simplifies JUnit testing in the following ways:
diff --git a/src/spec/doc/guide-integrating.adoc 
b/src/spec/doc/guide-integrating.adoc
index 4266c26c68..eedc3074f4 100644
--- a/src/spec/doc/guide-integrating.adoc
+++ b/src/spec/doc/guide-integrating.adoc
@@ -314,6 +314,61 @@ Hello, dependency 2!
 Hello, dependency 2!
 ----
 
+[[integ-javashell]]
+=== JavaShell
+
+While `GroovyShell` and `GroovyClassLoader` compile Groovy source, 
`org.apache.groovy.util.JavaShell`
+compiles _Java_ source in-memory using the platform 
`javax.tools.JavaCompiler`. It is useful when a
+Groovy or polyglot application needs to compile and load Java classes at 
runtime: code generation,
+plug-in loaders, scripting-style Java endpoints, or mixed-language testing 
setups.
+
+NOTE: `JavaShell` requires a JDK at runtime, not just a JRE — 
`ToolProvider.getSystemJavaCompiler()`
+returns `null` on a JRE.
+
+The simplest usage compiles a single source string and returns the loaded 
`Class`:
+
+[source,groovy]
+----
+include::../test/IntegrationTest.groovy[tags=javashell_compile,indent=0]
+----
+<1> create a new `JavaShell` (an optional `ClassLoader` argument sets the 
parent loader)
+<2> compile the source; `className` is the fully-qualified binary name and 
must match the
+    `package` declaration — see the `JavaShell` javadoc for the full naming 
rules
+<3> invoke the compiled method reflectively
+
+`compileAll` returns _every_ class produced from the source unit — useful when 
the source declares
+auxiliary or nested classes:
+
+[source,groovy]
+----
+include::../test/IntegrationTest.groovy[tags=javashell_compile_all,indent=0]
+----
+<1> compile a primary public class together with its package-private helper
+<2> the returned map is keyed by binary class name
+
+To additionally persist the generated class files to disk (for tools that 
consume `.class` files
+or to seed a classpath outside the JVM), use `compileAllTo`. It accepts a 
`Path` output directory
+and writes each class to its conventional package subdirectory:
+
+[source,groovy]
+----
+include::../test/IntegrationTest.groovy[tags=javashell_compile_all_to,indent=0]
+----
+<1> compile and write to disk in one call; the second argument passes `javac` 
options (here
+    `-parameters` to retain formal parameter names)
+<2> packaged classes land under `<outputDir>/<package as 
path>/<ClassName>.class`; default-package
+    classes land directly under `outputDir`
+
+The compiled classes remain available through `js.getClassLoader()` after 
`compileAllTo` returns,
+so the same instance covers both "compile to memory" and "compile to memory 
and disk".
+
+When the source fails to compile, `JavaShell` throws 
`JavaShellCompilationException` carrying the
+`javac` diagnostics. See the `JavaShell` javadoc for the full set of overloads 
(with/without an
+`Iterable<String>` options argument) and the expected `className`/`options` 
formats.
+
+NOTE: `compileAllTo` and its no-options overload are marked `@Incubating` — 
their signatures may
+evolve in a subsequent 6.x release based on usage feedback.
+
 === CompilationUnit
 
 Ultimately, it is possible to perform more operations during compilation by 
relying directly on the
diff --git a/src/spec/test/IntegrationTest.groovy 
b/src/spec/test/IntegrationTest.groovy
index ca2e71c9c9..0d08cf5944 100644
--- a/src/spec/test/IntegrationTest.groovy
+++ b/src/spec/test/IntegrationTest.groovy
@@ -254,6 +254,61 @@ try {
 } finally {
     tmpDir.deleteDir()
 }
+'''
+    }
+
+    @Test
+    void testJavaShell() {
+        assertScript '''// tag::javashell_compile[]
+import org.apache.groovy.util.JavaShell
+
+def js = new JavaShell()                                                    // 
<1>
+def src = """
+    package demo;
+    public class Foo {
+        public static String greet(String who) { return "hello, " + who; }
+    }
+"""
+Class<?> foo = js.compile('demo.Foo', src)                                  // 
<2>
+assert foo.getDeclaredMethod('greet', String).invoke(null, 'world') == 'hello, 
world'  // <3>
+// end::javashell_compile[]
+'''
+
+        assertScript '''// tag::javashell_compile_all[]
+import org.apache.groovy.util.JavaShell
+
+def js = new JavaShell()
+def src = """
+    package demo;
+    public class Box { public static int hidden() { return Helper.value(); } }
+    class Helper { static int value() { return 42; } }
+"""
+Map<String, Class<?>> classes = js.compileAll('demo.Box', src)              // 
<1>
+assert classes.keySet() == ['demo.Box', 'demo.Helper'] as Set               // 
<2>
+assert classes['demo.Box'].getDeclaredMethod('hidden').invoke(null) == 42
+// end::javashell_compile_all[]
+'''
+
+        assertScript '''// tag::javashell_compile_all_to[]
+import org.apache.groovy.util.JavaShell
+import java.nio.file.Files
+
+def out = Files.createTempDirectory('javashell-')
+try {
+    def js = new JavaShell()
+    def src = """
+        package demo;
+        public class Deep { public static String tag(String label) { return 
label; } }
+    """
+    def written = js.compileAllTo('demo.Deep', ['-parameters'], src, out)   // 
<1>
+    def classFile = out.resolve('demo/Deep.class')                          // 
<2>
+    assert written['demo.Deep'] == classFile && Files.exists(classFile)
+    // classes also remain available through js.classLoader, same as compileAll
+    assert js.classLoader.loadClass('demo.Deep').getDeclaredMethod('tag', 
String).invoke(null, 'x') == 'x'
+} finally {
+    out.toFile().deleteDir()
+}
+// end::javashell_compile_all_to[]
 '''
     }
 }
diff --git a/src/spec/test/testingguide/JavaShellExampleTests.groovy 
b/src/spec/test/testingguide/JavaShellExampleTests.groovy
new file mode 100644
index 0000000000..87333a526c
--- /dev/null
+++ b/src/spec/test/testingguide/JavaShellExampleTests.groovy
@@ -0,0 +1,75 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package testingguide
+
+import org.junit.jupiter.api.Test
+
+import static groovy.test.GroovyAssert.assertScript
+
+final class JavaShellExampleTests {
+
+    @Test
+    void testJavaFixture() {
+        assertScript '''// tag::javashell_test_fixture[]
+import org.apache.groovy.util.JavaShell
+
+def js = new JavaShell()
+js.compile('fixtures.PojoFixture', """
+    package fixtures;
+    public class PojoFixture {
+        private final String name;
+        public PojoFixture(String name) { this.name = name; }
+        public String getName() { return name; }
+    }
+""")                                                                           
 // <1>
+
+// share JavaShell\'s class loader so Groovy code under test sees the fixture
+def shell = new GroovyShell(js.classLoader)                                    
 // <2>
+shell.evaluate """
+    def bean = fixtures.PojoFixture.newInstance('Groovy')
+    assert bean.name == 'Groovy'
+"""                                                                            
 // <3>
+// end::javashell_test_fixture[]
+'''
+    }
+
+    @Test
+    void testBytecodeCapture() {
+        assertScript '''// tag::javashell_bytecode_capture[]
+import org.apache.groovy.util.JavaShell
+import java.nio.file.Files
+
+def out = Files.createTempDirectory('bc-')
+try {
+    new JavaShell().compileAllTo('bc.Calc', """
+        package bc;
+        public class Calc { public static int add(int a, int b) { return a + 
b; } }
+    """, out)                                                                  
 // <1>
+    byte[] javaBytes = Files.readAllBytes(out.resolve('bc/Calc.class'))        
 // <2>
+    // Feed javaBytes (and bytes for the equivalent Groovy source) into ASM,
+    // javap, or your bytecode-diff tool of choice for an equivalence 
assertion. // <3>
+    assert javaBytes.length > 0
+    assert javaBytes[0..3] == [(byte)0xCA, (byte)0xFE, (byte)0xBA, (byte)0xBE]
+} finally {
+    out.toFile().deleteDir()
+}
+// end::javashell_bytecode_capture[]
+'''
+    }
+}
diff --git 
a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/Console.groovy 
b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/Console.groovy
index cce1b481ed..4958354781 100644
--- 
a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/Console.groovy
+++ 
b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/Console.groovy
@@ -1628,7 +1628,7 @@ class Console implements CaretListener, 
HyperlinkListener, ComponentListener, Fo
             Optional<String> optionalPrimaryClassName = 
findPrimaryClassName(src)
             if (optionalPrimaryClassName.isPresent()) {
                 def js = new 
JavaShell(Thread.currentThread().contextClassLoader)
-                js.run(optionalPrimaryClassName.get(), src)
+                js.run(optionalPrimaryClassName.get(), src, 
Console.this.scriptArgsArray)
             } else {
                 System.err.println('Initial parsing successful but no public 
class found. Compile/run will not proceed.')
             }
diff --git a/subprojects/groovy-console/src/spec/doc/groovy-console.adoc 
b/subprojects/groovy-console/src/spec/doc/groovy-console.adoc
index 1ec3267a98..e8b19811b8 100644
--- a/subprojects/groovy-console/src/spec/doc/groovy-console.adoc
+++ b/subprojects/groovy-console/src/spec/doc/groovy-console.adoc
@@ -83,6 +83,23 @@ executed.
 * You can turn the System.out capture on and off by selecting `Capture
 System.out` from the `Actions` menu
 
+[[GroovyConsole-RunningAsJava]]
+=== Running and compiling as Java
+
+The console can also treat the editor contents as Java source rather than 
Groovy:
+
+* `Script > Run as Java` (shortcut `Alt+R`) compiles the input as Java and 
invokes its `main`
+  method, with any program arguments taken from `Script > Set Script 
Arguments`.
+* `Script > Run Selection as Java` does the same for just the highlighted text.
+* `Script > Compile as Java` compiles the input without running it — useful as 
a quick syntax
+  check.
+
+The source must declare a single top-level public class whose name matches a 
fully-qualified
+binary name in any `package` declaration; the console parses the source to 
locate that class
+and reports if none is found. Compilation and execution go through the
+`org.apache.groovy.util.JavaShell` API, so the same `javac` diagnostics 
surface in the output
+area.
+
 [[GroovyConsole-EditingFiles]]
 === Editing Files
 

Reply via email to