diff --git a/jacodb-core/src/main/kotlin/org/jacodb/impl/bytecode/JcClassLookupImpl.kt b/jacodb-core/src/main/kotlin/org/jacodb/impl/bytecode/JcClassLookupImpl.kt index 5b26d4da9..37e7f13da 100644 --- a/jacodb-core/src/main/kotlin/org/jacodb/impl/bytecode/JcClassLookupImpl.kt +++ b/jacodb-core/src/main/kotlin/org/jacodb/impl/bytecode/JcClassLookupImpl.kt @@ -17,6 +17,9 @@ package org.jacodb.impl.bytecode import org.jacodb.api.jvm.* +import org.jacodb.api.jvm.ext.CONSTRUCTOR +import org.jacodb.api.jvm.ext.allSuperHierarchy +import org.jacodb.api.jvm.ext.isSubClassOf import org.jacodb.api.jvm.ext.packageName class JcClassLookupImpl(val clazz: JcClassOrInterface) : JcLookup { @@ -34,7 +37,25 @@ class JcClassLookupImpl(val clazz: JcClassOrInterface) : JcLookup + val overriddenBySubinterface = interfaceMethods.any { other -> + other.enclosingClass != candidate.enclosingClass && + other.enclosingClass.isSubClassOf(candidate.enclosingClass) + } + !overriddenBySubinterface + } + + val defaultMethods = maximallySpecific.filterNot { it.isAbstract } + return if (defaultMethods.isEmpty()) maximallySpecific.firstOrNull() else defaultMethods.singleOrNull() } } @@ -84,10 +105,14 @@ internal abstract class JcClassLookup(clazz: JcClassOrInt description: String, ) : JcMethodLookup(clazz, name, description) { - override fun JcClassOrInterface.next() = emptyList() + private val isConstructorLookup = name == CONSTRUCTOR + + override fun JcClassOrInterface.next(): List { + return if (isConstructorLookup) emptyList() else listOfNotNull(superClass) + } override val predicate: (JcMethod) -> Boolean - get() = { it.name == name && it.isStatic && it.description == description } + get() = { it.name == name && !it.isStatic && it.description == description } } diff --git a/jacodb-core/src/main/kotlin/org/jacodb/impl/cfg/TypedMethodRefImpl.kt b/jacodb-core/src/main/kotlin/org/jacodb/impl/cfg/TypedMethodRefImpl.kt index 8e0e1c572..f6cd32f7e 100644 --- a/jacodb-core/src/main/kotlin/org/jacodb/impl/cfg/TypedMethodRefImpl.kt +++ b/jacodb-core/src/main/kotlin/org/jacodb/impl/cfg/TypedMethodRefImpl.kt @@ -71,8 +71,10 @@ abstract class MethodSignatureRef( append("#") append(name) append("(") - argumentTypes.joinToString(", ") { it.typeName } - append(")'") + append(argumentTypes.joinToString(", ") { it.typeName }) + append(")' (descriptor: ") + append(description) + append(")") } } diff --git a/jacodb-core/src/main/kotlin/org/jacodb/impl/types/JcClassTypeLookup.kt b/jacodb-core/src/main/kotlin/org/jacodb/impl/types/JcClassTypeLookup.kt index c1f00f550..2aa1e4aad 100644 --- a/jacodb-core/src/main/kotlin/org/jacodb/impl/types/JcClassTypeLookup.kt +++ b/jacodb-core/src/main/kotlin/org/jacodb/impl/types/JcClassTypeLookup.kt @@ -36,7 +36,24 @@ class JcClassTypeLookupImpl(val type: JcClassType) : JcLookup() + val visitedClasses = hashSetOf() + pendingTypes.add(type) + + while (pendingTypes.isNotEmpty()) { + val current = pendingTypes.removeFirst() + if (!visitedClasses.add(current.jcClass)) continue + + if (current.jcClass == method.enclosingClass) { + return current.declaredMethods.firstOrNull { it.method == method } + } + + current.superType?.let { pendingTypes.add(it) } + pendingTypes.addAll(current.interfaces) + } + + return null } } @@ -81,19 +98,6 @@ abstract class JcClassTypeLookup(clazz: JcClassType) : } - internal class JcSpecialTypedMethodLookup( - type: JcClassType, - name: String, - description: String, - ) : JcTypedMethodLookup(type, name, description) { - - override fun JcClassType.next() = listOfNotNull(superType) - - override val predicate: (JcTypedMethod) -> Boolean - get() = { it.name == name && it.method.description == description } - - } - internal class JcTypedFieldLookup( type: JcClassType, private val name: String, diff --git a/jacodb-core/src/test/kotlin/org/jacodb/testing/lookup/SpecialMethodLookupTest.kt b/jacodb-core/src/test/kotlin/org/jacodb/testing/lookup/SpecialMethodLookupTest.kt new file mode 100644 index 000000000..610f3c8a4 --- /dev/null +++ b/jacodb-core/src/test/kotlin/org/jacodb/testing/lookup/SpecialMethodLookupTest.kt @@ -0,0 +1,180 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * Licensed 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 org.jacodb.testing.lookup + +import org.jacodb.api.jvm.JcClassOrInterface +import org.jacodb.api.jvm.JcClassType +import org.jacodb.api.jvm.MethodNotFoundException +import org.jacodb.api.jvm.ext.findClass +import org.jacodb.api.jvm.ext.toType +import org.jacodb.impl.cfg.TypedSpecialMethodRefImpl +import org.jacodb.impl.types.TypeNameImpl +import org.jacodb.testing.BaseTest +import org.jacodb.testing.WithGlobalDbImmutable +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Assumptions.assumeTrue +import org.junit.jupiter.api.Test + +class SpecialMethodLookupTest : BaseTest() { + companion object : WithGlobalDbImmutable() + + @Test + fun `super call resolves default inherited by superclass`() { + val child = cp.findClass() + + assertSpecialOwner( + owner = child, + name = "value", + descriptor = "()I", + expectedOwner = SpecialMethodFixtures.DefaultValue::class.java, + ) + + val caller = child.declaredMethods.single { it.name == "callSuper" } + assertFalse(caller.flowGraph().instructions.isEmpty()) + } + + @Test + fun `class chain wins over interface default`() { + val child = cp.findClass() + + assertSpecialOwner( + owner = child, + name = "value", + descriptor = "()I", + expectedOwner = SpecialMethodFixtures.ClassValue::class.java, + ) + } + + @Test + fun `most specific interface declaration wins`() { + val child = cp.findClass() + + assertSpecialOwner( + owner = child, + name = "value", + descriptor = "()I", + expectedOwner = SpecialMethodFixtures.MoreSpecificValue::class.java, + ) + } + + @Test + fun `abstract redeclaration suppresses inherited default`() { + val child = cp.findClass() + + assertSpecialOwner( + owner = child, + name = "value", + descriptor = "()I", + expectedOwner = SpecialMethodFixtures.AbstractValue::class.java, + ) + assertTrue(child.lookup.specialMethod(name = "value", description = "()I")!!.isAbstract) + } + + @Test + fun `inherited default preserves generic substitution`() { + val child = cp.findClass() + + assertSpecialOwner( + owner = child, + name = "get", + descriptor = "()Ljava/lang/Object;", + expectedOwner = SpecialMethodFixtures.GenericValue::class.java, + ) + val method = child.toType().lookup.specialMethod(name = "get", description = "()Ljava/lang/Object;") + + assertNotNull(method) + assertEquals("java.lang.String", method!!.returnType.typeName) + assertFalse(child.declaredMethods.single { it.name == "callSuper" }.flowGraph().instructions.isEmpty()) + } + + @Test + fun `constructors are not inherited and static methods are not special`() { + val child = cp.findClass() + val defaultBase = cp.findClass() + + assertNull(child.lookup.specialMethod(name = "", description = "(I)V")) + assertNull(child.toType().lookup.specialMethod(name = "", description = "(I)V")) + assertNull(child.lookup.specialMethod(name = "staticValue", description = "()I")) + assertNull(child.toType().lookup.specialMethod(name = "staticValue", description = "()I")) + assertNull(defaultBase.lookup.specialMethod(name = "staticValue", description = "()I")) + assertSpecialOwner( + owner = child, + name = "privateValue", + descriptor = "()I", + expectedOwner = SpecialMethodFixtures.ConstructorChild::class.java, + ) + } + + @Test + fun `missing method diagnostic includes argument types and descriptor`() { + val child = cp.findClass().toType() + val reference = TypedSpecialMethodRefImpl( + type = child, + name = "missing", + argTypes = listOf( + TypeNameImpl.fromTypeName(typeName = "int"), + TypeNameImpl.fromTypeName(typeName = "java.lang.String"), + ), + returnType = TypeNameImpl.fromTypeName(typeName = "void"), + ) + + val error = assertThrows(MethodNotFoundException::class.java) { reference.method } + + assertTrue(error.message!!.contains("missing(int, java.lang.String)")) + assertTrue(error.message!!.contains("(ILjava/lang/String;)V")) + } + + @Test + fun `JDK Random super call reaches RandomGenerator default`() { + val randomGenerator = cp.findClassOrNull("java.util.random.RandomGenerator") + assumeTrue(randomGenerator != null, "RandomGenerator requires JDK 17 or newer") + val random = cp.findClass("java.util.Random") + val threadLocalRandom = cp.findClass("java.util.concurrent.ThreadLocalRandom") + val nextInt = threadLocalRandom.declaredMethods.single { it.name == "nextInt" && it.description == "(II)I" } + + val graph = nextInt.flowGraph() + val raw = random.lookup.specialMethod(name = "nextInt", description = "(II)I") + val typed = random.toType().lookup.specialMethod(name = "nextInt", description = "(II)I") + + assertNotNull(raw) + assertNotNull(typed) + assertEquals(randomGenerator, raw!!.enclosingClass) + assertEquals(raw, typed!!.method) + assertFalse(graph.instructions.isEmpty()) + } + + private fun assertSpecialOwner( + owner: JcClassOrInterface, + name: String, + descriptor: String, + expectedOwner: Class<*>, + ) { + val raw = owner.lookup.specialMethod(name, descriptor) + val typed = owner.toType().lookup.specialMethod(name, descriptor) + + assertNotNull(raw) + assertNotNull(typed) + assertEquals(expectedOwner.name, raw!!.enclosingClass.name) + assertEquals(raw, typed!!.method) + assertEquals(expectedOwner.name, (typed.enclosingType as JcClassType).jcClass.name) + } +} diff --git a/jacodb-core/src/testFixtures/java/org/jacodb/testing/lookup/SpecialMethodFixtures.java b/jacodb-core/src/testFixtures/java/org/jacodb/testing/lookup/SpecialMethodFixtures.java new file mode 100644 index 000000000..7dcc18930 --- /dev/null +++ b/jacodb-core/src/testFixtures/java/org/jacodb/testing/lookup/SpecialMethodFixtures.java @@ -0,0 +1,102 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * Licensed 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 org.jacodb.testing.lookup; + +public class SpecialMethodFixtures { + public interface DefaultValue { + default int value() { + return 1; + } + + static int staticValue() { + return 2; + } + } + + public interface MoreSpecificValue extends DefaultValue { + @Override + default int value() { + return 3; + } + } + + public interface AbstractValue extends DefaultValue { + @Override + int value(); + } + + public static class DefaultBase implements DefaultValue { + } + + public static class DefaultChild extends DefaultBase { + public int callSuper() { + return super.value(); + } + } + + public static class ClassValue { + public int value() { + return 4; + } + } + + public static class ClassValueBase extends ClassValue { + } + + public static class ClassValueChild extends ClassValueBase implements DefaultValue { + } + + public static class SpecificValue implements DefaultValue, MoreSpecificValue { + } + + public abstract static class AbstractUser implements AbstractValue { + } + + public interface GenericValue { + default T get() { + return null; + } + } + + public static class GenericBase implements GenericValue { + } + + public static class GenericChild extends GenericBase { + public String callSuper() { + return super.get(); + } + } + + public static class ConstructorBase { + public ConstructorBase(int value) { + } + } + + public static class ConstructorChild extends ConstructorBase { + public ConstructorChild() { + super(1); + } + + public static int staticValue() { + return 5; + } + + private int privateValue() { + return 6; + } + } +}