Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<JcField, JcMethod> {
Expand All @@ -34,7 +37,25 @@ class JcClassLookupImpl(val clazz: JcClassOrInterface) : JcLookup<JcField, JcMet
}

override fun specialMethod(name: String, description: String): JcMethod? {
return JcClassLookup.JcSpecialMethodLookup(clazz, name, description).lookup()
val classMethod = JcClassLookup.JcSpecialMethodLookup(clazz, name, description).lookup()
if (classMethod != null || name == CONSTRUCTOR) return classMethod

// A declaration anywhere in the class chain takes precedence over an interface default.
val interfaceMethods = clazz.allSuperHierarchy
.filter { it.isInterface }
.flatMap { it.declaredMethods }
.filter { it.name == name && it.description == description && !it.isStatic && !it.isPrivate }

val maximallySpecific = interfaceMethods.filter { candidate ->
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()
}

}
Expand Down Expand Up @@ -84,10 +105,14 @@ internal abstract class JcClassLookup<Result : JcAccessible>(clazz: JcClassOrInt
description: String,
) : JcMethodLookup(clazz, name, description) {

override fun JcClassOrInterface.next() = emptyList<JcClassOrInterface>()
private val isConstructorLookup = name == CONSTRUCTOR

override fun JcClassOrInterface.next(): List<JcClassOrInterface> {
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 }

}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(")")
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,24 @@ class JcClassTypeLookupImpl(val type: JcClassType) : JcLookup<JcTypedField, JcTy
}

override fun specialMethod(name: String, description: String): JcTypedMethod? {
return JcClassTypeLookup.JcSpecialTypedMethodLookup(type, name, description).lookup()
val method = type.jcClass.lookup.specialMethod(name, description) ?: return null
val pendingTypes = ArrayDeque<JcClassType>()
val visitedClasses = hashSetOf<JcClassOrInterface>()
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
}
}

Expand Down Expand Up @@ -81,19 +98,6 @@ abstract class JcClassTypeLookup<Result : JcAccessible>(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,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
/*
* Copyright 2022 UnitTestBot contributors (utbot.org)
* <p>
* 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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<SpecialMethodFixtures.DefaultChild>()

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<SpecialMethodFixtures.ClassValueChild>()

assertSpecialOwner(
owner = child,
name = "value",
descriptor = "()I",
expectedOwner = SpecialMethodFixtures.ClassValue::class.java,
)
}

@Test
fun `most specific interface declaration wins`() {
val child = cp.findClass<SpecialMethodFixtures.SpecificValue>()

assertSpecialOwner(
owner = child,
name = "value",
descriptor = "()I",
expectedOwner = SpecialMethodFixtures.MoreSpecificValue::class.java,
)
}

@Test
fun `abstract redeclaration suppresses inherited default`() {
val child = cp.findClass<SpecialMethodFixtures.AbstractUser>()

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<SpecialMethodFixtures.GenericChild>()

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<SpecialMethodFixtures.ConstructorChild>()
val defaultBase = cp.findClass<SpecialMethodFixtures.DefaultBase>()

assertNull(child.lookup.specialMethod(name = "<init>", description = "(I)V"))
assertNull(child.toType().lookup.specialMethod(name = "<init>", 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<SpecialMethodFixtures.DefaultChild>().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)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* Copyright 2022 UnitTestBot contributors (utbot.org)
* <p>
* 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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<T> {
default T get() {
return null;
}
}

public static class GenericBase implements GenericValue<String> {
}

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;
}
}
}
Loading