From 093d1b00a8793f7eeb46f0928df6f3e53e543860 Mon Sep 17 00:00:00 2001 From: Paul King Date: Thu, 6 Aug 2026 21:18:59 +1000 Subject: [PATCH 1/2] GROOVY-9585: CompilerConfiguration copy constructor does not copy compilation customizers --- COMPATIBILITY.md | 42 +++++++++++++++++++ .../groovy/control/CompilerConfiguration.java | 28 ++++++++++++- .../stc/StaticTypeCheckingSupport.java | 6 ++- .../control/CompilerConfigurationTest.java | 27 +++++++++++- 4 files changed, 97 insertions(+), 6 deletions(-) diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index 99aa6aff0c7..4b7a3cae005 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -206,6 +206,48 @@ planned for deprecation/removal in a future Groovy version. Formal `@Deprecated` may be restored before 6 GA once beta feedback confirms indy remains acceptable for those use cases. +### Groovy 6 — `CompilerConfiguration` copy constructor copies customizers (GROOVY-9585) + +`CompilerConfiguration(CompilerConfiguration)` now copies the source +configuration's compilation customizers along with every other setting. +Before Groovy 6 it copied everything *except* customizers. + +**Who is affected.** Code that derives a configuration from an existing +one and then registers its own customizers: + +```java +CompilerConfiguration child = new CompilerConfiguration(parent); +child.addCompilationCustomizers(mine); // now: parent's customizers *and* mine +``` + +The change is silent — no exception, just customizers running that +previously did not. An `ImportCustomizer` adds its imports twice; an +`ASTTransformationCustomizer`, which carries mutable `applied` state, is +invoked for a second compilation. + +**To restore the old behaviour**, use the two-argument copy constructor added in +6.0.0: + +```java +CompilerConfiguration child = new CompilerConfiguration(parent, false); +``` + +**Nested compilation.** A copied customizer is invoked for every primary +class node of the child compilation, including nodes added via +`CompilationUnit.addClassNode`, which have no `SourceUnit` — as that +method's javadoc warns. A customizer that dereferences the `SourceUnit` +it is handed will therefore throw `NullPointerException`; Gradle's +incremental-compilation customizer is one such, and Groovy's own +`SourceAwareCustomizer` is another. A customizer may equally assume the +class nodes it sees belong to the compilation it was registered for. +Groovy's own nested compilations — +`StaticTypeCheckingSupport.evaluateExpression`, which compiles a +synthetic expression holder, and `GroovyTypeCheckingExtensionSupport`, +which compiles a type checking DSL script — therefore pass `false`. +**Prefer `new CompilerConfiguration(parent, false)` whenever you derive a +configuration for a nested compilation**, and null-check the `SourceUnit` +in any customizer that might be applied to one. + ## The binary-compatibility check The [`subprojects/binary-compatibility/`](subprojects/binary-compatibility) diff --git a/src/main/java/org/codehaus/groovy/control/CompilerConfiguration.java b/src/main/java/org/codehaus/groovy/control/CompilerConfiguration.java index 32c5f39949d..31f89f89303 100644 --- a/src/main/java/org/codehaus/groovy/control/CompilerConfiguration.java +++ b/src/main/java/org/codehaus/groovy/control/CompilerConfiguration.java @@ -605,10 +605,33 @@ private void handleOptimizationOption(String key, String val) { * CompilerConfiguration myConfiguration = new CompilerConfiguration(CompilerConfiguration.DEFAULT); * myConfiguration.setDebug(true); * + *

+ * The supplied configuration's {@link #getCompilationCustomizers() compilation customizers} + * are copied along with everything else. Prior to Groovy 6, they were not; code which copies a + * configuration and then adds its own customizers will now see the copied ones as well as its + * own. Use {@link #CompilerConfiguration(CompilerConfiguration, boolean)} with {@code false} if + * you want only your own. * * @param configuration The configuration to copy. */ public CompilerConfiguration(final CompilerConfiguration configuration) { + this(configuration, true); + } + + /** + * Copy constructor which optionally omits the supplied configuration's + * {@link #getCompilationCustomizers() compilation customizers}. + *

+ * Pass {@code false} when deriving a configuration for a nested compilation, such as + * compiling a synthetic class node or a DSL script encountered while compiling something else. + * A customizer is registered for a particular compilation and commonly assumes its source units + * and class nodes, so applying one to a nested compilation may misbehave or fail outright. + * + * @param configuration The configuration to copy. + * @param copyCustomizers whether to also copy the compilation customizers + * @since 6.0.0 + */ + public CompilerConfiguration(final CompilerConfiguration configuration, final boolean copyCustomizers) { setWarningLevel(configuration.getWarningLevel()); setTargetDirectory(configuration.getTargetDirectory()); setClasspathList(configuration.getClasspath()); @@ -637,8 +660,9 @@ public CompilerConfiguration(final CompilerConfiguration configuration) { Map jointCompilationOptions = configuration.getJointCompilationOptions(); setJointCompilationOptions(null != jointCompilationOptions ? new HashMap<>(jointCompilationOptions) : jointCompilationOptions); - // TODO GROOVY-9585: add line below once gradle build issues fixed -// compilationCustomizers.addAll(configuration.getCompilationCustomizers()); + if (copyCustomizers) { + compilationCustomizers.addAll(configuration.getCompilationCustomizers()); + } } /** diff --git a/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingSupport.java b/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingSupport.java index f054b725562..e7405611bed 100644 --- a/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingSupport.java +++ b/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingSupport.java @@ -2390,8 +2390,10 @@ public static Object evaluateExpression(final Expression expr, final CompilerCon ClassNode classNode = new ClassNode(className, Opcodes.ACC_PUBLIC, OBJECT_TYPE); addGeneratedMethod(classNode, "eval", Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, OBJECT_TYPE, Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, new ReturnStatement(expr)); - // adjust configuration so class can be executed by this JVM - CompilerConfiguration cc = new CompilerConfiguration(config); + // adjust configuration so class can be executed by this JVM; the class node below is + // added without a source unit, so a customizer expecting one would fail, and customizers + // have no business running on an internal, throw-away expression holder in any case + CompilerConfiguration cc = new CompilerConfiguration(config, false); cc.setPreviewFeatures(false); cc.setScriptBaseClass(null); cc.setTargetBytecode(CompilerConfiguration.DEFAULT.getTargetBytecode()); diff --git a/src/test/groovy/org/codehaus/groovy/control/CompilerConfigurationTest.java b/src/test/groovy/org/codehaus/groovy/control/CompilerConfigurationTest.java index 34e35e81ad0..872455b394b 100644 --- a/src/test/groovy/org/codehaus/groovy/control/CompilerConfigurationTest.java +++ b/src/test/groovy/org/codehaus/groovy/control/CompilerConfigurationTest.java @@ -181,8 +181,31 @@ public void testCopyConstructor1() { assertEquals(pluginFactory, config.getPluginFactory()); assertTrue(config.isLogClassgen()); assertEquals(100, config.getLogClassgenStackTraceMaxDepth()); - // TODO GROOVY-9585: re-enable below assertion once prod code is fixed -// assertEquals(1, config.getCompilationCustomizers().size()); + assertEquals(1, config.getCompilationCustomizers().size()); + } + + @Test + public void testCopyConstructorWithoutCustomizers() { + CompilerConfiguration init = new CompilerConfiguration(); + init.setScriptBaseClass("blarg.foo.WhatSit"); + init.setSourceEncoding("LEAD-123"); + init.setTargetBytecode(CompilerConfiguration.JDK17); + init.addCompilationCustomizers(new ImportCustomizer().addStarImports("groovy.transform")); + assertEquals(1, init.getCompilationCustomizers().size()); + + CompilerConfiguration withCustomizers = new CompilerConfiguration(init, true); + assertEquals(1, withCustomizers.getCompilationCustomizers().size()); + + CompilerConfiguration withoutCustomizers = new CompilerConfiguration(init, false); + assertTrue(withoutCustomizers.getCompilationCustomizers().isEmpty()); + + // everything other than the customizers is copied either way + assertEquals("blarg.foo.WhatSit", withoutCustomizers.getScriptBaseClass()); + assertEquals("LEAD-123", withoutCustomizers.getSourceEncoding()); + assertEquals(CompilerConfiguration.JDK17, withoutCustomizers.getTargetBytecode()); + + // the source configuration is left alone + assertEquals(1, init.getCompilationCustomizers().size()); } @Test From 52adbeae4ec7ad980575f95c056d83945775ff55 Mon Sep 17 00:00:00 2001 From: Paul King Date: Thu, 6 Aug 2026 22:41:24 +1000 Subject: [PATCH 2/2] GROOVY-9585 follow-on: type-checking extension config inheritance --- .../GroovyTypeCheckingExtensionSupport.java | 7 ++- .../stc/TypeCheckingExtensionsTest.groovy | 43 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/codehaus/groovy/transform/stc/GroovyTypeCheckingExtensionSupport.java b/src/main/java/org/codehaus/groovy/transform/stc/GroovyTypeCheckingExtensionSupport.java index babd1ba9630..4741ed64d1f 100644 --- a/src/main/java/org/codehaus/groovy/transform/stc/GroovyTypeCheckingExtensionSupport.java +++ b/src/main/java/org/codehaus/groovy/transform/stc/GroovyTypeCheckingExtensionSupport.java @@ -204,7 +204,12 @@ public void setup() { "org.codehaus.groovy.ast.ClassHelper", "org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport"); - CompilerConfiguration config = new CompilerConfiguration().addCompilationCustomizers(ic); + // inherit the enclosing compilation's settings (bytecode target, preview features, + // optimization options, encoding, ...) but not its customizers: those are registered + // for the outer compilation and commonly assume its source units and class nodes + CompilerConfiguration config = new CompilerConfiguration( + typeCheckingVisitor.getSourceUnit().getConfiguration(), false) + .addCompilationCustomizers(ic); config.setScriptBaseClass("org.codehaus.groovy.transform.stc.GroovyTypeCheckingExtensionSupport$TypeCheckingDSL"); final GroovyClassLoader transformLoader = compilationUnit!=null?compilationUnit.getTransformLoader():typeCheckingVisitor.getSourceUnit().getClassLoader(); diff --git a/src/test/groovy/groovy/transform/stc/TypeCheckingExtensionsTest.groovy b/src/test/groovy/groovy/transform/stc/TypeCheckingExtensionsTest.groovy index 757cb245962..75b8f382028 100644 --- a/src/test/groovy/groovy/transform/stc/TypeCheckingExtensionsTest.groovy +++ b/src/test/groovy/groovy/transform/stc/TypeCheckingExtensionsTest.groovy @@ -18,8 +18,14 @@ */ package groovy.transform.stc +import org.codehaus.groovy.ast.ClassNode +import org.codehaus.groovy.classgen.GeneratorContext +import org.codehaus.groovy.control.BytecodeProcessor +import org.codehaus.groovy.control.CompilePhase import org.codehaus.groovy.control.MultipleCompilationErrorsException +import org.codehaus.groovy.control.SourceUnit import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer +import org.codehaus.groovy.control.customizers.CompilationCustomizer import org.junit.jupiter.api.Test import static groovy.test.GroovyAssert.shouldFail @@ -576,4 +582,41 @@ final class TypeCheckingExtensionsTest extends StaticTypeCheckingTestCase { ''', 'Error thrown from extension in onMethodSelection' } + + @Test + void testExtensionScriptInheritsConfigurationButNotCustomizers() { + Set postprocessed = [] + Set customized = [] + + // not a customizer, so it reaches the extension script only if that script's + // compiler configuration was derived from the enclosing compilation's + config.bytecodePostprocessor = { String name, byte[] bytes -> + postprocessed << name + bytes + } as BytecodeProcessor + + // a customizer, registered for the enclosing compilation: it must not reach + // the extension script, since customizers commonly assume the source units + // and class nodes of the compilation they were registered for + config.addCompilationCustomizers(new CompilationCustomizer(CompilePhase.CANONICALIZATION) { + @Override + void call(SourceUnit source, GeneratorContext context, ClassNode classNode) { + customized << classNode.name + } + }) + + extension = 'groovy/transform/stc/SetupTestExtension.groovy' + assertScript ''' + class A {} + new A() + ''' + + // the enclosing compilation is named TestScript; the extension script is + // compiled separately by GroovyTypeCheckingExtensionSupport and named Script + assert customized.contains('A'), 'customizer should see the enclosing compilation' + assert postprocessed.any { it.startsWith('Script') }, + 'extension script should inherit the enclosing compiler configuration' + assert !customized.any { it.startsWith('Script') }, + 'extension script should not inherit the enclosing compilation customizers' + } }