Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MP-5395 in between interface #976

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 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
@@ -1,3 +1,6 @@
/*
* Copyright 2000-2023 JetBrains s.r.o. and other contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
*/
package com.jetbrains.pluginverifier.verifiers.method

import com.google.common.cache.Cache
Expand All @@ -7,6 +10,8 @@ import com.jetbrains.pluginverifier.verifiers.resolution.Method
import org.objectweb.asm.Opcodes
import org.objectweb.asm.tree.AbstractInsnNode
import org.objectweb.asm.tree.MethodInsnNode
import org.objectweb.asm.tree.TypeInsnNode
import org.objectweb.asm.tree.VarInsnNode

object KotlinMethods {
private const val CAPACITY = 100L
Expand Down Expand Up @@ -61,14 +66,15 @@ object KotlinMethods {
}

private fun Method.isKotlinMethodInvokingDefaultImpls(method: Method): Boolean {
// filter non kotlin classes
if (!method.containingClassFile.annotations.any { it.desc == "Lkotlin/Metadata;" }) {
return false
}

// Sanity check : if the method does not have bytecode
// this heuristic cannot run
if (instructions.isEmpty()) {
if (
// filter non kotlin classes
!method.containingClassFile.annotations.any { it.desc == "Lkotlin/Metadata;" }
// Sanity check: if the method does not have bytecode, or it's a constructor or class intializer
// this heuristic cannot run
|| instructions.isEmpty()
|| method.isConstructor
|| method.isClassInitializer
) {
return false
}

Expand All @@ -93,24 +99,28 @@ object KotlinMethods {
candidateOpcodes.add(currentInsnNode)
} while (++i < instructions.size)

val isDefaultCallingDefaultOfParentInterface = isDefaultCallingDefaultOfParentInterface(method, candidateOpcodes) // checkcast this

val expectedOpcodes = (
3 // aload this + invokestatic + (return or areturn)
+ method.methodParameters.size // aload for each parameter
if (isDefaultCallingDefaultOfParentInterface)
4 // aload this + checkcast + invokestatic + (return or areturn)
+ method.methodParameters.size // aload for each parameter
- 1 // Skip first parameter, it's `this` or the interface (this)
else
3 // aload this + invokestatic + (return or areturn)
+ method.methodParameters.size // aload for each parameter
)

val nonThisStartParameterIndex = if (isDefaultCallingDefaultOfParentInterface)
2 // before: aload this + checkcast
else
1 // before: aload this

if (candidateOpcodes.size != expectedOpcodes
|| candidateOpcodes[0].opcode != Opcodes.ALOAD // aload this
|| candidateOpcodes.slice(1..method.methodParameters.size).any() { it.opcode != Opcodes.ALOAD } // parameters
|| candidateOpcodes[0].opcode != Opcodes.ALOAD // aload this, always a reference
|| candidateOpcodes.slice(nonThisStartParameterIndex..method.methodParameters.size).any() { it.opcode !in loadOpCodes } // parameters
|| candidateOpcodes[candidateOpcodes.lastIndex - 1].opcode != Opcodes.INVOKESTATIC
|| candidateOpcodes.last().opcode !in intArrayOf(
Opcodes.RETURN,
Opcodes.ARETURN,
Opcodes.DRETURN,
Opcodes.FRETURN,
Opcodes.IRETURN,
Opcodes.LRETURN
)
|| candidateOpcodes.last().opcode !in returnOpCodes
) {
return false
}
Expand All @@ -122,6 +132,14 @@ object KotlinMethods {

val actualKotlinOwner = methodInsnNode.owner.substringBeforeLast("\$DefaultImpls")

// If the current class is the default implementation of a child interface
// then kotlin make it the inner class of 2 inner classes
// * `ParentInterface`
// * `ChildInterface`
// There can be more than 2 inner classes if the interface extends multiple interfaces.
val isImplementingAParentInterface = containingClassFile.innerClasses.size > 1
&& isDefaultCallingDefaultOfParentInterface

// Walking the whole class hierarchy is not necessary because
// these methods always delegate to the immediate superinterface. E.g.
// ```
Expand All @@ -137,11 +155,64 @@ object KotlinMethods {
val isAParent = method.containingClassFile.interfaces.any {
it == actualKotlinOwner
}
if (!isAParent) {
@Suppress("RedundantIf") // for reading clarity
if (!isAParent && !isImplementingAParentInterface) {
return false
}

// The method is a kotlin default method
return true
}
}

/**
* Check when an interface extends another the Idea interface that has private types.
*
* In this case, Kotlin generates a default implementation `MyInterface$DefaultImpls`
* that calls the `ParentInterface$DefaultImpls` of the parent interface
*
* ```
* // access flags 0x9
* public static topInternal(Lmock/plugin/internal/defaultMethod/NoInternalTypeUsageInterface;)Linternal/defaultMethod/AnInternalType;
* @Lorg/jetbrains/annotations/Nullable;() // invisible
* // annotable parameter count: 1 (invisible)
* @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0
* L0
* LINENUMBER 5 L0
* ALOAD 0
* CHECKCAST internal/defaultMethod/InterfaceWithDefaultMethodUsingInternalAPI
* INVOKESTATIC internal/defaultMethod/InterfaceWithDefaultMethodUsingInternalAPI$DefaultImpls.topInternal (Linternal/defaultMethod/InterfaceWithDefaultMethodUsingInternalAPI;)Linternal/defaultMethod/AnInternalType;
* L1
* LINENUMBER 11 L1
* ARETURN
* L2
* LOCALVARIABLE $this Lmock/plugin/internal/defaultMethod/NoInternalTypeUsageInterface; L0 L2 0
* MAXSTACK = 1
* MAXLOCALS = 1
* ```
*/
private fun isDefaultCallingDefaultOfParentInterface(method: Method, candidateOpcodes: MutableList<AbstractInsnNode>) =
method.isStatic
&& candidateOpcodes[0].opcode == Opcodes.ALOAD
&& (candidateOpcodes[0] as VarInsnNode).`var` == 0 // aload 0
&& candidateOpcodes[1].opcode == Opcodes.CHECKCAST
&& method.containingClassFile.innerClasses.any {
(candidateOpcodes[1] as TypeInsnNode).desc == it.outerName
}

private val returnOpCodes = intArrayOf(
Opcodes.RETURN,
Opcodes.ARETURN,
Opcodes.DRETURN,
Opcodes.FRETURN,
Opcodes.IRETURN,
Opcodes.LRETURN
)

private val loadOpCodes = intArrayOf(
Opcodes.ALOAD,
Opcodes.ILOAD,
Opcodes.DLOAD,
Opcodes.FLOAD,
Opcodes.ILOAD,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package com.jetbrains.pluginverifier.verifiers.resolution
import com.jetbrains.plugin.structure.classes.resolvers.FileOrigin
import com.jetbrains.pluginverifier.results.location.ClassLocation
import org.objectweb.asm.tree.AnnotationNode
import org.objectweb.asm.tree.InnerClassNode

interface ClassFile : ClassFileMember {
override val location: ClassLocation
Expand All @@ -24,6 +25,7 @@ interface ClassFile : ClassFileMember {
val javaVersion: Int
val enclosingClassName: String?

val innerClasses: List<InnerClassNode>
override val annotations: List<AnnotationNode>

val isAbstract: Boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import com.jetbrains.pluginverifier.results.modifiers.Modifiers
import com.jetbrains.pluginverifier.verifiers.getAccessType
import org.objectweb.asm.Opcodes
import org.objectweb.asm.tree.ClassNode
import org.objectweb.asm.tree.InnerClassNode

class ClassFileAsm(private val asmNode: ClassNode, override val classFileOrigin: FileOrigin) : ClassFile {
override val location
Expand Down Expand Up @@ -66,6 +67,11 @@ class ClassFileAsm(private val asmNode: ClassNode, override val classFileOrigin:
return asmNode.innerClasses.find { it.name == name }?.outerName
}

override val innerClasses: List<InnerClassNode>
get() {
return asmNode.innerClasses
}

override val annotations
get() = asmNode.invisibleAnnotations.orEmpty() + asmNode.visibleAnnotations.orEmpty()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ interface InterfaceWithDefaultMethodUsingInternalAPI : TopInterfaceWithDefaultMe
fun returningInternal() : AnInternalType? = null
fun internalArgsReturningInternal(anInternalType: AnInternalType, s: String) : AnInternalType? = null
fun internalArgsReturningVoid(anInternalType: AnInternalType, s: String) {}
fun internalArgsAndPrimitiveArgs(anInternalType: AnInternalType, i: Int, b: Boolean, ui: UInt, objectArray: Array<String>, intArray: Array<Int>) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ interface InterfaceWithDefaultMethodUsingInternalAPI : TopInterfaceWithDefaultMe
fun returningInternal() : AnInternalType? = null
fun internalArgsReturningInternal(anInternalType: AnInternalType, s: String) : AnInternalType? = null
fun internalArgsReturningVoid(anInternalType: AnInternalType, s: String) {}
fun internalArgsAndPrimitiveArgs(anInternalType: AnInternalType, i: Int, b: Boolean, ui: UInt, objectArray: Array<String>, intArray: Array<Int>) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package mock.plugin.internal.defaultMethod

import internal.defaultMethod.InterfaceWithDefaultMethodUsingInternalAPI

// this interface when compiled with kotlin could have a nested DefaultImpls
// that may refer to internal types, this should not raise warnings.
interface NoInternalTypeUsageInterface : InterfaceWithDefaultMethodUsingInternalAPI {
// Kotlin should generate a default implementation for the interface
// default methods. E.g. a `NoInternalTypeUsageInterface$DefaultImpls` class
// with the default implementations of the interface methods. E.g. :
//
// // access flags 0x9
// public static topInternal(Lmock/plugin/internal/defaultMethod/NoInternalTypeUsageInterface;)Linternal/defaultMethod/AnInternalType;
// @Lorg/jetbrains/annotations/Nullable;() // invisible
// // annotable parameter count: 1 (invisible)
// @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0
// L0
// LINENUMBER 5 L0
// ALOAD 0
// CHECKCAST internal/defaultMethod/InterfaceWithDefaultMethodUsingInternalAPI
// INVOKESTATIC internal/defaultMethod/InterfaceWithDefaultMethodUsingInternalAPI$DefaultImpls.topInternal (Linternal/defaultMethod/InterfaceWithDefaultMethodUsingInternalAPI;)Linternal/defaultMethod/AnInternalType;
// L1
// LINENUMBER 11 L1
// ARETURN
// L2
// LOCALVARIABLE $this Lmock/plugin/internal/defaultMethod/NoInternalTypeUsageInterface; L0 L2 0
// MAXSTACK = 1
// MAXLOCALS = 1
}