Skip to content

Commit e868cdf

Browse files
authored
Merge pull request #9876 from smowton/smowton/feature/interface-forwarding
Kotlin: implement default interface forwarding
2 parents fd5f678 + 14b8892 commit e868cdf

File tree

26 files changed

+474
-35
lines changed

26 files changed

+474
-35
lines changed

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

Lines changed: 71 additions & 7 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
@@ -845,10 +846,68 @@ open class KotlinFileExtractor(
845846
}
846847
}
847848

849+
private fun isKotlinDefinedInterface(cls: IrClass?) =
850+
cls != null && cls.isInterface && cls.origin != IrDeclarationOrigin.IR_EXTERNAL_JAVA_DECLARATION_STUB
851+
852+
private fun needsInterfaceForwarder(f: IrFunction) =
853+
// 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.
854+
// Otherwise, for a Kotlin-defined method inheriting a Kotlin-defined default, we need to create a synthetic method like
855+
// `int f(int x) { return super.InterfaceWithDefault.f(x); }`, because kotlinc will generate a public method and Java callers may directly target it.
856+
// (NB. kotlinc's actual implementation strategy is different -- it makes an inner class called InterfaceWithDefault$DefaultImpls and stores the default methods
857+
// there to allow default method usage in Java < 8, but this is hopefully niche.
858+
!pluginContext.languageVersionSettings.getFlag(JvmAnalysisFlags.jvmDefaultMode).forAllMethodsWithBody &&
859+
f.parentClassOrNull.let { it != null && it.origin != IrDeclarationOrigin.IR_EXTERNAL_JAVA_DECLARATION_STUB && it.modality != Modality.ABSTRACT } &&
860+
f.realOverrideTarget.let { it != f && (it as? IrSimpleFunction)?.modality != Modality.ABSTRACT && isKotlinDefinedInterface(it.parentClassOrNull) }
861+
862+
private fun makeInterfaceForwarder(f: IrFunction, parentId: Label<out DbReftype>, extractBody: Boolean, extractMethodAndParameterTypeAccesses: Boolean, typeSubstitution: TypeSubstitution?, classTypeArgsIncludingOuterClasses: List<IrTypeArgument>?) =
863+
forceExtractFunction(f, parentId, extractBody = false, extractMethodAndParameterTypeAccesses, typeSubstitution, classTypeArgsIncludingOuterClasses, overriddenAttributes = OverriddenFunctionAttributes(visibility = DescriptorVisibilities.PUBLIC)).also { functionId ->
864+
tw.writeCompiler_generated(functionId, CompilerGeneratedKinds.INTERFACE_FORWARDER.kind)
865+
if (extractBody) {
866+
val realFunctionLocId = tw.getLocation(f)
867+
val inheritedDefaultFunction = f.realOverrideTarget
868+
val directlyInheritedSymbol =
869+
when(f) {
870+
is IrSimpleFunction ->
871+
f.overriddenSymbols.find { it.owner === inheritedDefaultFunction }
872+
?: f.overriddenSymbols.find { it.owner.realOverrideTarget === inheritedDefaultFunction }
873+
?: inheritedDefaultFunction.symbol
874+
else -> inheritedDefaultFunction.symbol // This is strictly invalid, since we shouldn't use A.super.f(...) where A may not be a direct supertype, but this path should also be unreachable.
875+
}
876+
val defaultDefiningInterfaceType = (directlyInheritedSymbol.owner.parentClassOrNull ?: return functionId).typeWith()
877+
878+
extractExpressionBody(functionId, realFunctionLocId).also { returnId ->
879+
extractRawMethodAccess(
880+
f,
881+
realFunctionLocId,
882+
f.returnType,
883+
functionId,
884+
returnId,
885+
0,
886+
returnId,
887+
f.valueParameters.size,
888+
{ argParentId, idxOffset ->
889+
f.valueParameters.mapIndexed { idx, param ->
890+
val syntheticParamId = useValueParameter(param, functionId)
891+
extractVariableAccess(syntheticParamId, param.type, realFunctionLocId, argParentId, idxOffset + idx, functionId, returnId)
892+
}
893+
},
894+
f.dispatchReceiverParameter?.type,
895+
{ callId ->
896+
extractSuperAccess(defaultDefiningInterfaceType, functionId, callId, -1, returnId, realFunctionLocId)
897+
},
898+
null
899+
)
900+
}
901+
}
902+
}
903+
848904
private fun extractFunction(f: IrFunction, parentId: Label<out DbReftype>, extractBody: Boolean, extractMethodAndParameterTypeAccesses: Boolean, typeSubstitution: TypeSubstitution?, classTypeArgsIncludingOuterClasses: List<IrTypeArgument>?) =
849-
if (isFake(f))
850-
null
851-
else {
905+
if (isFake(f)) {
906+
if (needsInterfaceForwarder(f))
907+
makeInterfaceForwarder(f, parentId, extractBody, extractMethodAndParameterTypeAccesses, typeSubstitution, classTypeArgsIncludingOuterClasses)
908+
else
909+
null
910+
} else {
852911
forceExtractFunction(f, parentId, extractBody, extractMethodAndParameterTypeAccesses, typeSubstitution, classTypeArgsIncludingOuterClasses).also {
853912
// 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.
854913
if (classTypeArgsIncludingOuterClasses.isNullOrEmpty())
@@ -1163,7 +1222,7 @@ open class KotlinFileExtractor(
11631222
extractBody(body, id)
11641223
}
11651224

1166-
extractVisibility(f, id, f.visibility)
1225+
extractVisibility(f, id, overriddenAttributes?.visibility ?: f.visibility)
11671226

11681227
if (f.isInline) {
11691228
addModifiers(id, "inline")
@@ -1227,7 +1286,9 @@ open class KotlinFileExtractor(
12271286

12281287
private fun extractProperty(p: IrProperty, parentId: Label<out DbReftype>, extractBackingField: Boolean, extractFunctionBodies: Boolean, extractPrivateMembers: Boolean, typeSubstitution: TypeSubstitution?, classTypeArgsIncludingOuterClasses: List<IrTypeArgument>?) {
12291288
with("property", p) {
1230-
if (isFake(p)) return
1289+
fun needsInterfaceForwarderQ(f: IrFunction?) = f?.let { needsInterfaceForwarder(f) } ?: false
1290+
1291+
if (isFake(p) && !needsInterfaceForwarderQ(p.getter) && !needsInterfaceForwarderQ(p.setter)) return
12311292

12321293
DeclarationStackAdjuster(p).use {
12331294

@@ -5375,7 +5436,9 @@ open class KotlinFileExtractor(
53755436
val sourceLoc: Label<DbLocation>? = null,
53765437
val valueParameters: List<IrValueParameter>? = null,
53775438
val typeParameters: List<IrTypeParameter>? = null,
5378-
val isStatic: Boolean? = null)
5439+
val isStatic: Boolean? = null,
5440+
val visibility: DescriptorVisibility? = null,
5441+
)
53795442

53805443
private fun peekDeclStackAsDeclarationParent(elementToReportOn: IrElement): IrDeclarationParent? {
53815444
val trapWriter = tw
@@ -5400,6 +5463,7 @@ open class KotlinFileExtractor(
54005463
DELEGATED_PROPERTY_SETTER(7),
54015464
JVMSTATIC_PROXY_METHOD(8),
54025465
JVMOVERLOADS_METHOD(9),
5403-
DEFAULT_ARGUMENTS_METHOD(10)
5466+
DEFAULT_ARGUMENTS_METHOD(10),
5467+
INTERFACE_FORWARDER(11),
54045468
}
54055469
}

java/ql/integration-tests/posix-only/kotlin/gradle_kotlinx_serialization/PrintAst.expected

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,15 @@ app/src/main/kotlin/testProject/App.kt:
425425
# 7| -1: [ThisAccess] $serializer.this
426426
# 7| 0: [TypeAccess] $serializer
427427
# 7| 1: [VarAccess] tmp0_serialDesc
428+
# 7| 7: [Method] typeParametersSerializers
429+
# 7| 3: [TypeAccess] KSerializer<?>[]
430+
# 7| 0: [TypeAccess] KSerializer<?>
431+
# 7| 0: [WildcardTypeAccess] ? ...
432+
# 7| 5: [BlockStmt] { ... }
433+
# 7| 0: [ReturnStmt] return ...
434+
# 7| 0: [MethodAccess] typeParametersSerializers(...)
435+
# 7| -1: [SuperAccess] GeneratedSerializer.super
436+
# 7| 0: [TypeAccess] GeneratedSerializer
428437
# 7| 11: [Class] Companion
429438
# 0| 1: [Method] serializer
430439
# 0| 3: [TypeAccess] KSerializer<Project>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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+
// RealIndirect is similar to Real, except it inherits its methods indirectly via MiddleInterface.
9+
public static void test(Real r1, RealNoForwards r2, RealIndirect r3) {
10+
11+
sink(r1.f());
12+
sink(r1.g(2));
13+
sink(r1.getX());
14+
15+
sink(r2.f());
16+
sink(r2.g(5));
17+
sink(r2.getX());
18+
19+
sink(r3.f());
20+
sink(r3.g(8));
21+
sink(r3.getX());
22+
23+
}
24+
25+
}
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: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
callables
2+
| User.java:1:14:1:17 | User | User.java:1:14:1:17 | User | from source |
3+
| User.java:3:22:3:25 | sink | User.java:1:14:1:17 | User | from source |
4+
| User.java:9:22:9:25 | test | User.java:1:14:1:17 | User | from source |
5+
| noforwards.kt:3:3:3:13 | f | noforwards.kt:1:1:10:1 | NoForwards | from source |
6+
| noforwards.kt:5:3:5:19 | g | noforwards.kt:1:1:10:1 | NoForwards | from source |
7+
| noforwards.kt:8:5:8:13 | getX | noforwards.kt:1:1:10:1 | NoForwards | from source |
8+
| noforwards.kt:12:1:12:37 | RealNoForwards | noforwards.kt:12:1:12:37 | RealNoForwards | from source |
9+
| test.kt:3:3:3:13 | f | test.kt:1:1:10:1 | Test | from source |
10+
| test.kt:5:3:5:19 | g | test.kt:1:1:10:1 | Test | from source |
11+
| test.kt:8:5:8:13 | getX | test.kt:1:1:10:1 | Test | from source |
12+
| test.kt:12:1:12:21 | Real | test.kt:12:1:12:21 | Real | from source |
13+
| test.kt:12:1:12:21 | f | test.kt:12:1:12:21 | Real | Forwarder for a Kotlin class inheriting an interface default method |
14+
| test.kt:12:1:12:21 | g | test.kt:12:1:12:21 | Real | Forwarder for a Kotlin class inheriting an interface default method |
15+
| test.kt:12:1:12:21 | getX | test.kt:12:1:12:21 | Real | Forwarder for a Kotlin class inheriting an interface default method |
16+
| test.kt:16:1:16:40 | RealIndirect | test.kt:16:1:16:40 | RealIndirect | from source |
17+
| test.kt:16:1:16:40 | f | test.kt:16:1:16:40 | RealIndirect | Forwarder for a Kotlin class inheriting an interface default method |
18+
| test.kt:16:1:16:40 | g | test.kt:16:1:16:40 | RealIndirect | Forwarder for a Kotlin class inheriting an interface default method |
19+
| test.kt:16:1:16:40 | getX | test.kt:16:1:16:40 | RealIndirect | Forwarder for a Kotlin class inheriting an interface default method |
20+
superAccesses
21+
| test.kt:12:1:12:21 | Test.super | test.kt:12:1:12:21 | Real | test.kt:12:1:12:21 | f | test.kt:12:1:12:21 | Test |
22+
| test.kt:12:1:12:21 | Test.super | test.kt:12:1:12:21 | Real | test.kt:12:1:12:21 | g | test.kt:12:1:12:21 | Test |
23+
| test.kt:12:1:12:21 | Test.super | test.kt:12:1:12:21 | Real | test.kt:12:1:12:21 | getX | test.kt:12:1:12:21 | Test |
24+
| test.kt:16:1:16:40 | MiddleInterface.super | test.kt:16:1:16:40 | RealIndirect | test.kt:16:1:16:40 | f | test.kt:16:1:16:40 | MiddleInterface |
25+
| test.kt:16:1:16:40 | MiddleInterface.super | test.kt:16:1:16:40 | RealIndirect | test.kt:16:1:16:40 | g | test.kt:16:1:16:40 | MiddleInterface |
26+
| test.kt:16:1:16:40 | MiddleInterface.super | test.kt:16:1:16:40 | RealIndirect | test.kt:16:1:16:40 | getX | test.kt:16:1:16:40 | MiddleInterface |
27+
#select
28+
| User.java:12:15:12:15 | 2 | User.java:12:10:12:16 | g(...) |
29+
| User.java:16:15:16:15 | 5 | User.java:16:10:16:16 | g(...) |
30+
| User.java:20:15:20:15 | 8 | User.java:20:10:20:16 | g(...) |
31+
| noforwards.kt:3:13:3:13 | 4 | User.java:15:10:15:15 | f(...) |
32+
| noforwards.kt:8:13:8:13 | 6 | User.java:17:10:17:18 | getX(...) |
33+
| test.kt:3:13:3:13 | 1 | User.java:11:10:11:15 | f(...) |
34+
| test.kt:3:13:3:13 | 1 | User.java:19:10:19:15 | f(...) |
35+
| test.kt:8:13:8:13 | 3 | User.java:13:10:13:18 | getX(...) |
36+
| test.kt:8:13:8:13 | 3 | User.java:21:10:21:18 | getX(...) |
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
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 { }
13+
14+
interface MiddleInterface : Test { }
15+
16+
class RealIndirect : MiddleInterface { }
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: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import java
2+
import semmle.code.java.dataflow.DataFlow
3+
4+
query predicate callables(Callable c, RefType declType, string kind) {
5+
c.fromSource() and
6+
declType = c.getDeclaringType() and
7+
(
8+
kind = c.compilerGeneratedReason()
9+
or
10+
not exists(c.compilerGeneratedReason()) and kind = "from source"
11+
)
12+
}
13+
14+
query predicate superAccesses(
15+
SuperAccess sa, RefType enclosingType, Callable enclosingCallable, Expr qualifier
16+
) {
17+
sa.getQualifier() = qualifier and
18+
enclosingCallable = sa.getEnclosingCallable() and
19+
enclosingType = enclosingCallable.getDeclaringType()
20+
}
21+
22+
class Config extends DataFlow::Configuration {
23+
Config() { this = "testconfig" }
24+
25+
override predicate isSource(DataFlow::Node x) {
26+
x.asExpr() instanceof IntegerLiteral and x.getEnclosingCallable().fromSource()
27+
}
28+
29+
override predicate isSink(DataFlow::Node x) {
30+
x.asExpr().(Argument).getCall().getCallee().getName() = "sink"
31+
}
32+
}
33+
34+
from Config c, DataFlow::Node source, DataFlow::Node sink
35+
where c.hasFlow(source, sink)
36+
select source, sink

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

Lines changed: 14 additions & 1 deletion
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
}
@@ -77,7 +79,18 @@ class Element extends @element, Top {
7779
private predicate hasChildElement(Element parent, Element e) {
7880
cupackage(e, parent)
7981
or
80-
enclInReftype(e, parent)
82+
enclInReftype(e, parent) and
83+
not e instanceof LocalClassOrInterface
84+
or
85+
// Reasoning: any specialised instance of a local class is supposed to belong to the general
86+
// case of its enclosing method because we don't instantiate specialised variants of either generic
87+
// functions or function bodies, and therefore the local class cannot be specialised with respect
88+
// to its enclosing reftypes.
89+
e.(LocalClassOrInterface)
90+
.getSourceDeclaration()
91+
.(LocalClassOrInterface)
92+
.getLocalTypeDeclStmt()
93+
.getEnclosingCallable() = parent
8194
or
8295
not enclInReftype(e, _) and
8396
e.(Class).getCompilationUnit() = parent

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)