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.
This commit is contained in:
Chris Smowton
2022-07-20 10:01:16 +01:00
parent 040d72e7f1
commit e8a35983ee
9 changed files with 137 additions and 6 deletions

View File

@@ -0,0 +1,20 @@
public class User {
public static void sink(int x) { }
// Real is compiled with synthetic interface method forwarders, so it appears from a Java perspective to override the interface Test.
// RealNoForwards is compiled with -Xjvm-default=all, meaning real Java 8 default interface methods are used, no synthetic forwarders
// are created, and call resolution should go straight to the default.
public static void test(Real r1, RealNoForwards r2) {
sink(r1.f());
sink(r1.g(2));
sink(r1.getX());
sink(r2.f());
sink(r2.g(5));
sink(r2.getX());
}
}

View File

@@ -0,0 +1,12 @@
interface NoForwards {
fun f() = 4
fun g(x: Int) = x
val x : Int
get() = 6
}
class RealNoForwards : NoForwards { }

View File

@@ -0,0 +1,6 @@
| User.java:11:15:11:15 | 2 | User.java:11:10:11:16 | g(...) |
| User.java:15:15:15:15 | 5 | User.java:15:10:15:16 | g(...) |
| noforwards.kt:3:13:3:13 | 4 | User.java:14:10:14:15 | f(...) |
| noforwards.kt:8:13:8:13 | 6 | User.java:16:10:16:18 | getX(...) |
| test.kt:3:13:3:13 | 1 | User.java:10:10:10:15 | f(...) |
| test.kt:8:13:8:13 | 3 | User.java:12:10:12:18 | getX(...) |

View File

@@ -0,0 +1,12 @@
interface Test {
fun f() = 1
fun g(x: Int) = x
val x : Int
get() = 3
}
class Real : Test { }

View File

@@ -0,0 +1,6 @@
from create_database_utils import *
import glob
os.mkdir('build')
run_codeql_database_create(["kotlinc test.kt -d build", "kotlinc noforwards.kt -d build -Xjvm-default=all", "javac User.java -cp build"], lang="java")

View File

@@ -0,0 +1,18 @@
import java
import semmle.code.java.dataflow.DataFlow
class Config extends DataFlow::Configuration {
Config() { this = "testconfig" }
override predicate isSource(DataFlow::Node x) {
x.asExpr() instanceof IntegerLiteral and x.getEnclosingCallable().fromSource()
}
override predicate isSink(DataFlow::Node x) {
x.asExpr().(Argument).getCall().getCallee().getName() = "sink"
}
}
from Config c, DataFlow::Node source, DataFlow::Node sink
where c.hasFlow(source, sink)
select source, sink

View File

@@ -67,6 +67,8 @@ class Element extends @element, Top {
i = 9 and result = "Forwarder for a @JvmOverloads-annotated function"
or
i = 10 and result = "Forwarder for Kotlin calls that need default arguments filling in"
or
i = 11 and result = "Forwarder for a Kotlin class inheriting an interface default method"
)
}
}

View File

@@ -471,7 +471,12 @@ class Method extends Callable, @method {
}
override predicate isAbstract() {
Callable.super.isAbstract()
// The combination `abstract default` isn't legal in Java,
// but it occurs when the Kotlin extractor records a default
// body, but the output class file in fact uses an abstract
// method and an associated static helper, which we don't
// extract as an implementation detail.
Callable.super.isAbstract() and not this.isDefault()
or
// JLS 9.4: An interface method lacking a `private`, `default`, or `static` modifier
// is implicitly abstract.