This is an automated email from the ASF dual-hosted git repository. sunlan pushed a commit to branch GROOVY-10381 in repository https://gitbox.apache.org/repos/asf/groovy.git
commit ab845735c910eab60fbf53794e58ef1a3ca2b295 Author: Daniel Sun <[email protected]> AuthorDate: Sat Nov 23 23:02:24 2024 +0800 GROOVY-10381: No compiler error for class that implements interfaces with duplicated default methods --- .../org/codehaus/groovy/classgen/Verifier.java | 30 ++++ src/test/groovy/bugs/Groovy10381.groovy | 199 +++++++++++++++++++++ 2 files changed, 229 insertions(+) diff --git a/src/main/java/org/codehaus/groovy/classgen/Verifier.java b/src/main/java/org/codehaus/groovy/classgen/Verifier.java index f7aeb501f2..6f2c3d338e 100644 --- a/src/main/java/org/codehaus/groovy/classgen/Verifier.java +++ b/src/main/java/org/codehaus/groovy/classgen/Verifier.java @@ -234,6 +234,7 @@ public class Verifier implements GroovyClassVisitor, Opcodes { } checkForDuplicateInterfaces(node); + checkForDuplicateDefaultMethods(node); if (node.isInterface() || Traits.isTrait(node)) { // interfaces have no constructors but this expects one, @@ -276,6 +277,35 @@ public class Verifier implements GroovyClassVisitor, Opcodes { checkFinalVariables(node); } + private static void checkForDuplicateDefaultMethods(ClassNode node) { + if (node.getInterfaces().length < 2) return; + + Map<String, MethodNode> defaultMethods = new HashMap<>(8); + node.getAllInterfaces().stream() + .flatMap(i -> i.getAllDeclaredMethods().stream()) + .filter(MethodNode::isDefault) + .forEach(m -> { + String signature = methodDescriptorWithoutReturnType(m); + MethodNode existing = defaultMethods.get(signature); + if (existing == null) { + defaultMethods.put(signature, m); + return; + } + + ClassNode existingDeclaringClass = existing.getDeclaringClass(); + ClassNode currentDeclaringClass = m.getDeclaringClass(); + if (!(existingDeclaringClass.equals(currentDeclaringClass) + || existingDeclaringClass.implementsInterface(currentDeclaringClass) + || currentDeclaringClass.implementsInterface(existingDeclaringClass))) { + throw new RuntimeParserException( + (node.isInterface() ? "interface" : "class") + " " + node.getName() + + " inherits unrelated defaults for " + m.getTypeDescriptor() + + " from types " + existingDeclaringClass.getName() + + " and " + currentDeclaringClass.getName(), sourceOf(m)); + } + }); + } + private static final String[] INVALID_COMPONENTS = {"clone", "finalize", "getClass", "hashCode", "notify", "notifyAll", "toString", "wait"}; private static void detectInvalidRecordComponentNames(final ClassNode node) { diff --git a/src/test/groovy/bugs/Groovy10381.groovy b/src/test/groovy/bugs/Groovy10381.groovy new file mode 100644 index 0000000000..a49867a722 --- /dev/null +++ b/src/test/groovy/bugs/Groovy10381.groovy @@ -0,0 +1,199 @@ +/* + * 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.codehaus.groovy.tools.javac.JavaAwareCompilationUnit +import org.junit.Test + +import static groovy.test.GroovyAssert.shouldFail + +final class Groovy10381 { + @Test + void testDuplicateDefaultMethodsFromGroovyClasses_implements1() { + def err = shouldFail ''' + interface A { + default void m(String n) {} + } + interface B { + default void m(String n) {} + } + class C implements A, B { + void test() { + m('Hi') + } + } + ''' + assert err =~ /class C inherits unrelated defaults for void m\(java.lang.String\) from types A and B/ + } + + @Test + void testDuplicateDefaultMethodsFromGroovyClasses_implements2() { + def err = shouldFail ''' + interface BaseA { + default void m(String n) {} + } + interface A extends BaseA { + } + interface BaseB { + default void m(String n) {} + } + interface B extends BaseB { + } + class C implements A, B { + void test() { + m('Hi') + } + } + ''' + assert err =~ /class C inherits unrelated defaults for void m\(java.lang.String\) from types BaseA and BaseB/ + } + + @Test + void testDuplicateDefaultMethodsFromGroovyClasses_override() { + def err = shouldFail ''' + interface BaseA { + default void m(String n) {} + } + interface A extends BaseA { + @Override + default void m(String n) {} + } + interface BaseB { + default void m(String n) {} + } + interface B extends BaseB { + @Override + default void m(String n) {} + } + class C implements A, B { + void test() { + m('Hi') + } + } + ''' + assert err =~ /class C inherits unrelated defaults for void m\(java.lang.String\) from types A and B/ + } + + @Test + void testDuplicateDefaultMethodsFromGroovyClasses_extends() { + def err = shouldFail ''' + public interface A extends List { + default void m(String n) {} + } + public interface B { + default void m(String n) {} + } + interface C extends A, B { + default void test() { + m('Hi') + } + } + ''' + assert err =~ /interface C inherits unrelated defaults for void m\(java.lang.String\) from types A and B/ + } + + @Test + void testDuplicateDefaultMethodsFromJavaClasses_implements() { + def config = new CompilerConfiguration().tap { + jointCompilationOptions = [memStub: true] + targetDirectory = File.createTempDir() + } + File parentDir = File.createTempDir() + try { + def a = new File(parentDir, 'A.java') + a.write ''' + public interface A { + default void m(String n) {} + } + ''' + + def b = new File(parentDir, 'B.java') + b.write ''' + public interface B { + default void m(String n) {} + } + ''' + + def c = new File(parentDir, 'C.groovy') + c.write ''' + class C implements A, B { + void test() { + m("test") + } + } + ''' + + def loader = new GroovyClassLoader(this.class.classLoader) + def cu = new JavaAwareCompilationUnit(config, loader) + cu.addSources(a, b, c) + cu.compile() + assert false + } catch (Exception e) { + assert e =~ /class C inherits unrelated defaults for m\(String\) from types A and B/ + } finally { + parentDir.deleteDir() + config.targetDirectory.deleteDir() + } + } + + @Test + void testDuplicateDefaultMethodsFromJavaClasses_extends() { + def config = new CompilerConfiguration().tap { + jointCompilationOptions = [memStub: true] + targetDirectory = File.createTempDir() + } + File parentDir = File.createTempDir() + try { + def a = new File(parentDir, 'A.java') + a.write ''' + public interface A extends java.util.List { + default void m(String n) {} + } + ''' + + def b = new File(parentDir, 'B.java') + b.write ''' + public interface B { + default void m(String n) {} + } + ''' + + def c = new File(parentDir, 'C.groovy') + c.write ''' + interface C extends A, B { + default void test() { + m("test") + } + } + ''' + + def loader = new GroovyClassLoader(this.class.classLoader) + def cu = new JavaAwareCompilationUnit(config, loader) + cu.addSources(a, b, c) + cu.compile() + assert false + } catch (Exception e) { + assert e =~ /interface C inherits unrelated defaults for m\(String\) from types A and B/ + } finally { + parentDir.deleteDir() + config.targetDirectory.deleteDir() + } + } +}
