Skip to content

Commit e8a3598

Browse files
committed
Implement Kotlin default interface method forwarding
Kotlin's implementation of defaults depends on the -Xjvm-default setting (or the @JvmDefault deprecated annotation, not implemented here): by default, actual interface class files don't use default method, and any class that would inherit one instead implements the interface calling a static method defined on TheInterface$DefaultImpls. With -Xjvm-default=all or =all-compatibility, real interface default methods are emitted, with the latter retaining the DefaultImpls methods so that other Kotlin can use it. Here I adopt a hybrid solution: create a real default method implementation, but also emit a forwarding method like `@override int f(int x) { return super.TheInterface.f(x); }`, because the Java extractor will see `MyClass.f` in the emitted class file and try to dispatch directly to it. The only downside is that we emit a default interface method body for a prototype that will appear to be `abstract` to the Java extractor and which it will extract as such. I work around this by tolerating the combination `default abstract` in QL. The alternative would be to fully mimic the DefaultImpls approach, giving 100% fidelity to kotlinc's strategy and therefore no clash with the Java extractor's view of the world.
1 parent 040d72e commit e8a3598

File tree

9 files changed

+137
-6
lines changed

9 files changed

+137
-6
lines changed

java/kotlin-extractor/src/main/kotlin/KotlinFileExtractor.kt

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
99
import org.jetbrains.kotlin.backend.common.lower.parents
1010
import org.jetbrains.kotlin.backend.common.pop
1111
import org.jetbrains.kotlin.builtins.functions.BuiltInFunctionArity
12+
import org.jetbrains.kotlin.config.JvmAnalysisFlags
1213
import org.jetbrains.kotlin.descriptors.*
1314
import org.jetbrains.kotlin.descriptors.java.JavaVisibilities
1415
import org.jetbrains.kotlin.ir.IrElement
@@ -828,10 +829,56 @@ open class KotlinFileExtractor(
828829
}
829830
}
830831

832+
private fun needsInterfaceForwarder(f: IrFunction) =
833+
// forAllMethodsWithBody means -Xjvm-default=all or all-compatibility, in which case real Java default interfaces are used, and we don't need to do anything.
834+
// Otherwise, for a method inheriting a default, we need to create a synthetic method like `int f(int x) { return super.InterfaceWithDefault.f(x); }`, because
835+
// kotlinc will generate a public method and Java callers may directly target it.
836+
// (NB. kotlinc's actual implementation strategy is different -- it makes an inner class called InterfaceWithDefault$DefaultImpls and stores the default methods
837+
// there to allow default method usage in Java < 8, but this is hopefully niche.
838+
!pluginContext.languageVersionSettings.getFlag(JvmAnalysisFlags.jvmDefaultMode).forAllMethodsWithBody &&
839+
f.realOverrideTarget.let { it != f && it.parentClassOrNull?.isInterface == true }
840+
841+
private fun makeInterfaceForwarder(f: IrFunction, parentId: Label<out DbReftype>, extractBody: Boolean, extractMethodAndParameterTypeAccesses: Boolean, typeSubstitution: TypeSubstitution?, classTypeArgsIncludingOuterClasses: List<IrTypeArgument>?) =
842+
forceExtractFunction(f, parentId, extractBody = false, extractMethodAndParameterTypeAccesses, typeSubstitution, classTypeArgsIncludingOuterClasses).also { functionId ->
843+
tw.writeCompiler_generated(functionId, CompilerGeneratedKinds.INTERFACE_FORWARDER.kind)
844+
if (extractBody) {
845+
val realFunctionLocId = tw.getLocation(f)
846+
val inheritedDefaultFunction = f.realOverrideTarget
847+
val defaultDefiningInterfaceType = (inheritedDefaultFunction.parentClassOrNull ?: return functionId).typeWith()
848+
849+
extractExpressionBody(functionId, realFunctionLocId).also { returnId ->
850+
extractRawMethodAccess(
851+
f,
852+
realFunctionLocId,
853+
f.returnType,
854+
functionId,
855+
returnId,
856+
0,
857+
returnId,
858+
f.valueParameters.size,
859+
{ argParentId, idxOffset ->
860+
f.valueParameters.mapIndexed { idx, param ->
861+
val syntheticParamId = useValueParameter(param, functionId)
862+
extractVariableAccess(syntheticParamId, param.type, realFunctionLocId, argParentId, idxOffset + idx, functionId, returnId)
863+
}
864+
},
865+
f.dispatchReceiverParameter?.type,
866+
{ callId ->
867+
extractSuperAccess(defaultDefiningInterfaceType, functionId, callId, -1, returnId, realFunctionLocId)
868+
},
869+
null
870+
)
871+
}
872+
}
873+
}
874+
831875
private fun extractFunction(f: IrFunction, parentId: Label<out DbReftype>, extractBody: Boolean, extractMethodAndParameterTypeAccesses: Boolean, typeSubstitution: TypeSubstitution?, classTypeArgsIncludingOuterClasses: List<IrTypeArgument>?) =
832-
if (isFake(f))
833-
null
834-
else {
876+
if (isFake(f)) {
877+
if (needsInterfaceForwarder(f))
878+
makeInterfaceForwarder(f, parentId, extractBody, extractMethodAndParameterTypeAccesses, typeSubstitution, classTypeArgsIncludingOuterClasses)
879+
else
880+
null
881+
} else {
835882
forceExtractFunction(f, parentId, extractBody, extractMethodAndParameterTypeAccesses, typeSubstitution, classTypeArgsIncludingOuterClasses).also {
836883
// The defaults-forwarder function is a static utility, not a member, so we only need to extract this for the unspecialised instance of this class.
837884
if (classTypeArgsIncludingOuterClasses.isNullOrEmpty())
@@ -1202,7 +1249,9 @@ open class KotlinFileExtractor(
12021249

12031250
private fun extractProperty(p: IrProperty, parentId: Label<out DbReftype>, extractBackingField: Boolean, extractFunctionBodies: Boolean, extractPrivateMembers: Boolean, typeSubstitution: TypeSubstitution?, classTypeArgsIncludingOuterClasses: List<IrTypeArgument>?) {
12041251
with("property", p) {
1205-
if (isFake(p)) return
1252+
fun needsInterfaceForwarderQ(f: IrFunction?) = f?.let { needsInterfaceForwarder(f) } ?: false
1253+
1254+
if (isFake(p) && !needsInterfaceForwarderQ(p.getter) && !needsInterfaceForwarderQ(p.setter)) return
12061255

12071256
DeclarationStackAdjuster(p).use {
12081257

@@ -5373,6 +5422,7 @@ open class KotlinFileExtractor(
53735422
DELEGATED_PROPERTY_SETTER(7),
53745423
JVMSTATIC_PROXY_METHOD(8),
53755424
JVMOVERLOADS_METHOD(9),
5376-
DEFAULT_ARGUMENTS_METHOD(10)
5425+
DEFAULT_ARGUMENTS_METHOD(10),
5426+
INTERFACE_FORWARDER(11),
53775427
}
53785428
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
public class User {
2+
3+
public static void sink(int x) { }
4+
5+
// Real is compiled with synthetic interface method forwarders, so it appears from a Java perspective to override the interface Test.
6+
// RealNoForwards is compiled with -Xjvm-default=all, meaning real Java 8 default interface methods are used, no synthetic forwarders
7+
// are created, and call resolution should go straight to the default.
8+
public static void test(Real r1, RealNoForwards r2) {
9+
10+
sink(r1.f());
11+
sink(r1.g(2));
12+
sink(r1.getX());
13+
14+
sink(r2.f());
15+
sink(r2.g(5));
16+
sink(r2.getX());
17+
18+
}
19+
20+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
interface NoForwards {
2+
3+
fun f() = 4
4+
5+
fun g(x: Int) = x
6+
7+
val x : Int
8+
get() = 6
9+
10+
}
11+
12+
class RealNoForwards : NoForwards { }
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
| User.java:11:15:11:15 | 2 | User.java:11:10:11:16 | g(...) |
2+
| User.java:15:15:15:15 | 5 | User.java:15:10:15:16 | g(...) |
3+
| noforwards.kt:3:13:3:13 | 4 | User.java:14:10:14:15 | f(...) |
4+
| noforwards.kt:8:13:8:13 | 6 | User.java:16:10:16:18 | getX(...) |
5+
| test.kt:3:13:3:13 | 1 | User.java:10:10:10:15 | f(...) |
6+
| test.kt:8:13:8:13 | 3 | User.java:12:10:12:18 | getX(...) |
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
interface Test {
2+
3+
fun f() = 1
4+
5+
fun g(x: Int) = x
6+
7+
val x : Int
8+
get() = 3
9+
10+
}
11+
12+
class Real : Test { }
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from create_database_utils import *
2+
import glob
3+
4+
os.mkdir('build')
5+
run_codeql_database_create(["kotlinc test.kt -d build", "kotlinc noforwards.kt -d build -Xjvm-default=all", "javac User.java -cp build"], lang="java")
6+
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import java
2+
import semmle.code.java.dataflow.DataFlow
3+
4+
class Config extends DataFlow::Configuration {
5+
Config() { this = "testconfig" }
6+
7+
override predicate isSource(DataFlow::Node x) {
8+
x.asExpr() instanceof IntegerLiteral and x.getEnclosingCallable().fromSource()
9+
}
10+
11+
override predicate isSink(DataFlow::Node x) {
12+
x.asExpr().(Argument).getCall().getCallee().getName() = "sink"
13+
}
14+
}
15+
16+
from Config c, DataFlow::Node source, DataFlow::Node sink
17+
where c.hasFlow(source, sink)
18+
select source, sink

java/ql/lib/semmle/code/java/Element.qll

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ class Element extends @element, Top {
6767
i = 9 and result = "Forwarder for a @JvmOverloads-annotated function"
6868
or
6969
i = 10 and result = "Forwarder for Kotlin calls that need default arguments filling in"
70+
or
71+
i = 11 and result = "Forwarder for a Kotlin class inheriting an interface default method"
7072
)
7173
}
7274
}

java/ql/lib/semmle/code/java/Member.qll

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -471,7 +471,12 @@ class Method extends Callable, @method {
471471
}
472472

473473
override predicate isAbstract() {
474-
Callable.super.isAbstract()
474+
// The combination `abstract default` isn't legal in Java,
475+
// but it occurs when the Kotlin extractor records a default
476+
// body, but the output class file in fact uses an abstract
477+
// method and an associated static helper, which we don't
478+
// extract as an implementation detail.
479+
Callable.super.isAbstract() and not this.isDefault()
475480
or
476481
// JLS 9.4: An interface method lacking a `private`, `default`, or `static` modifier
477482
// is implicitly abstract.

0 commit comments

Comments
 (0)