mirror of
https://github.com/github/codeql.git
synced 2026-04-28 10:15:14 +02:00
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:
@@ -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());
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
interface NoForwards {
|
||||
|
||||
fun f() = 4
|
||||
|
||||
fun g(x: Int) = x
|
||||
|
||||
val x : Int
|
||||
get() = 6
|
||||
|
||||
}
|
||||
|
||||
class RealNoForwards : NoForwards { }
|
||||
@@ -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(...) |
|
||||
@@ -0,0 +1,12 @@
|
||||
interface Test {
|
||||
|
||||
fun f() = 1
|
||||
|
||||
fun g(x: Int) = x
|
||||
|
||||
val x : Int
|
||||
get() = 3
|
||||
|
||||
}
|
||||
|
||||
class Real : Test { }
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user