diff --git a/src/main/java/org/codehaus/groovy/classgen/AsmClassGenerator.java b/src/main/java/org/codehaus/groovy/classgen/AsmClassGenerator.java index 6c6e2afe67e..3bf8e5e7bdb 100644 --- a/src/main/java/org/codehaus/groovy/classgen/AsmClassGenerator.java +++ b/src/main/java/org/codehaus/groovy/classgen/AsmClassGenerator.java @@ -1693,11 +1693,12 @@ public void visitVariableExpression(final VariableExpression expression) { */ protected void createInterfaceSyntheticStaticFields() { var icl = controller.getInterfaceClassLoadingClass(); - // GROOVY-11982: also materialise the helper when there are call sites - // (e.g. dynamic code in default methods under indy=false), otherwise - // CallSiteWriter routes INVOKESTATIC at a class that was never emitted - boolean hasCallSites = !controller.getCallSiteWriter().getCallSites().isEmpty(); - if (referencedClasses.isEmpty() && !hasCallSites) return; + // GROOVY-11982: also materialise the helper when a call-site array + // prologue was emitted (every default/static method body under + // indy=false, even one that registers no call sites -- GROOVY-12235), + // otherwise CallSiteWriter routes INVOKESTATIC at a class never emitted + boolean hasCallSitePrologue = controller.getCallSiteWriter().isPrologueEmitted(); + if (referencedClasses.isEmpty() && !hasCallSitePrologue) return; addInnerClass(icl); for (Map.Entry entry : referencedClasses.entrySet()) { // generate a field node diff --git a/src/main/java/org/codehaus/groovy/classgen/asm/CallSiteWriter.java b/src/main/java/org/codehaus/groovy/classgen/asm/CallSiteWriter.java index 453c110fff2..04af7299d96 100644 --- a/src/main/java/org/codehaus/groovy/classgen/asm/CallSiteWriter.java +++ b/src/main/java/org/codehaus/groovy/classgen/asm/CallSiteWriter.java @@ -113,6 +113,7 @@ private static String getCreateArraySignature(int numberOfArguments) { public static final String CONSTRUCTOR = "<$constructor$>"; private final List callSites = new ArrayList(32); private int callSiteArrayVarIndex = -1; + private boolean prologueEmitted; private final WriterController controller; /** @@ -141,9 +142,18 @@ public void makeSiteEntry() { mv.visitMethodInsn(INVOKESTATIC, controller.getClassName(), GET_CALLSITE_METHOD, GET_CALLSITE_DESC, false); controller.getOperandStack().push(CALLSITE_ARRAY_TYPE); callSiteArrayVarIndex = controller.getCompileStack().defineTemporaryVariable("$local$callSiteArray", CALLSITE_ARRAY_TYPE, true); + prologueEmitted = true; } } + /** + * Returns true if a call-site array loading prologue has been emitted for + * at least one method of the current class. + */ + public boolean isPrologueEmitted() { + return prologueEmitted; + } + /** * Generates the call site array field and accessor methods. */ diff --git a/src/test/groovy/bugs/Groovy12235.groovy b/src/test/groovy/bugs/Groovy12235.groovy new file mode 100644 index 00000000000..8cfea49eb94 --- /dev/null +++ b/src/test/groovy/bugs/Groovy12235.groovy @@ -0,0 +1,66 @@ +/* + * 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 bugs + +import org.codehaus.groovy.control.CompilerConfiguration +import org.junit.jupiter.api.Test + +/** + * Follow-up to GROOVY-11982: under {@code indy=false} the call-site array + * prologue ({@code INVOKESTATIC $getCallSiteArray()}) is emitted at the top + * of every method body before the body is visited, so an interface method + * whose body registers no call sites (e.g. {@code return null}) still + * references the synthetic helper class ({@code MyInterface$1}) that owns + * the prologue for interfaces. The helper must be materialised whenever a + * prologue was emitted — not only when named call sites were registered — + * otherwise the first invocation throws + * {@code NoClassDefFoundError: MyInterface$1}. + */ +final class Groovy12235 { + + @Test + void testInterfaceDefaultMethodWithoutDynamicCodeNonIndy() { + CompilerConfiguration config = new CompilerConfiguration() + config.optimizationOptions.put('indy', false) + new GroovyShell(config).evaluate ''' + interface MyInterface { + default Object defaultValue() { + return null + } + } + class MyImpl implements MyInterface { + } + assert new MyImpl().defaultValue() == null + ''' + } + + @Test + void testInterfaceStaticMethodWithoutDynamicCodeNonIndy() { + CompilerConfiguration config = new CompilerConfiguration() + config.optimizationOptions.put('indy', false) + new GroovyShell(config).evaluate ''' + interface Util { + static Object nothing() { + return null + } + } + assert Util.nothing() == null + ''' + } +}