Skip to content

Commit 4fbb006

Browse files
committed
fix: stop Kotlin internal members leaking into metadata and typings
Kotlin compiles internal declarations down to public bytecode, so the @kotlin.Metadata annotation is the only way to tell them apart. The metadata generator consulted it for functions only. Property accessors are described under `properties` rather than `functions`, so an internal property's getter and setter were treated as public and stayed callable from JS under their mangled names - names that also differ between debug and release builds, which left nothing able to depend on them safely. Typings gain the same filtering through the android-dts-generator submodule, which now reads the annotation too and can therefore also drop internal classes. Three build fixes come along with it: - buildMetadata and generateTypescriptDefinitions declare their generator jars as inputs, so editing a generator regenerates its output instead of leaving the task up to date. - generateTypescriptDefinitions passes an absolute path to dts-generator.jar the way the other two build tools already do. It previously only resolved from a packaged framework and failed from a source checkout. - copyTypings no longer reports success after a failed generation, and copies only when there is a project to copy into. USER_PROJECT_ROOT sits two levels above the gradle root, which outside an app lands beyond the repository. Typings generated from android.jar are unchanged.
1 parent f36b01c commit 4fbb006

5 files changed

Lines changed: 121 additions & 7 deletions

File tree

test-app/app/build.gradle

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -742,6 +742,9 @@ task buildMetadata(type: BuildToolTask) {
742742
if (!findProject(':android-metadata-generator').is(null)) {
743743
rootPath = Paths.get(project(':android-metadata-generator').projectDir.path, "build/libs").toString()
744744
dependsOn ':android-metadata-generator:jar'
745+
// Without this the task stays up-to-date after the generator changes, so edits to it
746+
// silently produce no new metadata.
747+
inputs.files(tasks.getByPath(':android-metadata-generator:jar'))
745748
}
746749

747750

@@ -1031,8 +1034,11 @@ task buildMetadata(type: BuildToolTask) {
10311034
}
10321035

10331036
task generateTypescriptDefinitions(type: BuildToolTask) {
1037+
def rootPath = ""
10341038
if (!findProject(':dts-generator').is(null)) {
1039+
rootPath = Paths.get(project(':dts-generator').projectDir.path, "build/libs").toString()
10351040
dependsOn ':dts-generator:jar'
1041+
inputs.files(tasks.getByPath(':dts-generator:jar'))
10361042
}
10371043

10381044
def paramz = new ArrayList<String>()
@@ -1044,7 +1050,9 @@ task generateTypescriptDefinitions(type: BuildToolTask) {
10441050
doFirst {
10451051
delete "$TYPINGS_PATH"
10461052

1047-
paramz.add("dts-generator.jar")
1053+
// Resolved against the project when building from source; an empty rootPath leaves it
1054+
// relative to workingDir, which is where the packaged framework keeps the jar.
1055+
paramz.add(Paths.get(rootPath, "dts-generator.jar").toString())
10481056
paramz.add("-input")
10491057

10501058
for (String jarPath : allJarLibraries) {
@@ -1095,7 +1103,21 @@ task 'copyTypings' {
10951103
}
10961104
}
10971105

1098-
copyTypings.onlyIf { generateTypescriptDefinitions.didWork }
1106+
// USER_PROJECT_ROOT is two levels above the gradle root, which only lands on an app when that
1107+
// root is <app>/platforms/android. In a runtime checkout it points outside the repository, so
1108+
// require some evidence of a real project before copying anything there.
1109+
def hasUserProject = { ->
1110+
file("$USER_PROJECT_ROOT/$PACKAGE_JSON").exists() ||
1111+
rootDir.toString().replace(File.separator, "/").endsWith(PLATFORMS_ANDROID)
1112+
}
1113+
1114+
// didWork only means the task ran, so on a failed generation this would still report success
1115+
// and copy an empty typings directory over the user's project.
1116+
copyTypings.onlyIf {
1117+
generateTypescriptDefinitions.didWork &&
1118+
generateTypescriptDefinitions.state.failure == null &&
1119+
hasUserProject()
1120+
}
10991121
generateTypescriptDefinitions.finalizedBy(copyTypings)
11001122

11011123
task 'validateAppIdMatch' {

test-app/build-tools/android-metadata-generator/src/src/com/telerik/metadata/parsing/kotlin/methods/KotlinMethodDescriptor.kt

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import com.telerik.metadata.parsing.bytecode.methods.NativeMethodBytecodeDescrip
44
import com.telerik.metadata.parsing.kotlin.classes.KotlinClassDescriptor
55
import kotlin.metadata.KmDeclarationContainer
66
import kotlin.metadata.Visibility
7+
import kotlin.metadata.jvm.JvmMethodSignature
78
import kotlin.metadata.jvm.KotlinClassMetadata
9+
import kotlin.metadata.jvm.getterSignature
10+
import kotlin.metadata.jvm.setterSignature
811
import kotlin.metadata.jvm.signature
912
import kotlin.metadata.visibility
1013
import org.apache.bcel.classfile.Method
@@ -44,9 +47,28 @@ class KotlinMethodDescriptor(private val method: Method, private val originClass
4447
private fun checkIfMethodIsInternal(method: Method, kotlinDeclarationContainer: KmDeclarationContainer): Boolean {
4548
val function = kotlinDeclarationContainer
4649
.functions
47-
.firstOrNull {
48-
it.signature != null && it.signature!!.name == method.name && it.signature!!.descriptor == method.signature
49-
}
50-
return if (function != null) function.visibility == Visibility.INTERNAL else false
50+
.firstOrNull { it.signature.matches(method) }
51+
if (function != null) {
52+
return function.visibility == Visibility.INTERNAL
53+
}
54+
55+
// Property accessors are absent from `functions`, so without this an internal property's
56+
// getter and setter are treated as public and stay reachable from JS under their mangled
57+
// names. An accessor can also narrow the property's visibility on its own.
58+
for (property in kotlinDeclarationContainer.properties) {
59+
if (property.getterSignature.matches(method)) {
60+
return property.visibility == Visibility.INTERNAL || property.getter.visibility == Visibility.INTERNAL
61+
}
62+
63+
if (property.setterSignature.matches(method)) {
64+
return property.visibility == Visibility.INTERNAL || property.setter?.visibility == Visibility.INTERNAL
65+
}
66+
}
67+
68+
return false
69+
}
70+
71+
private fun JvmMethodSignature?.matches(method: Method): Boolean {
72+
return this != null && name == method.name && descriptor == method.signature
5173
}
5274
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.telerik.metadata.parsing.kotlin.methods
2+
3+
@Suppress("unused")
4+
class KotlinInternalMembersFixture {
5+
6+
var publicProperty: String = ""
7+
8+
internal var internalProperty: String = ""
9+
10+
var internalSetterProperty: String = ""
11+
internal set
12+
13+
fun publicFunction(): String = publicProperty
14+
15+
internal fun internalFunction(): String = internalProperty
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package com.telerik.metadata.parsing.kotlin.methods
2+
3+
import com.telerik.metadata.parsing.kotlin.classes.KotlinClassDescriptor
4+
import com.telerik.metadata.parsing.kotlin.metadata.bytecode.BytecodeMetadataAnnotation
5+
import org.apache.bcel.classfile.ClassParser
6+
import org.junit.Assert.assertFalse
7+
import org.junit.Assert.assertTrue
8+
import org.junit.Test
9+
10+
class KotlinMethodDescriptorInternalTest {
11+
12+
private val descriptor = descriptorFor(KotlinInternalMembersFixture::class.java)
13+
14+
@Test
15+
fun `internal property accessors are internal`() {
16+
assertTrue(isInternal("getInternalProperty"))
17+
assertTrue(isInternal("setInternalProperty"))
18+
}
19+
20+
@Test
21+
fun `public property accessors are not internal`() {
22+
assertFalse(isInternal("getPublicProperty"))
23+
assertFalse(isInternal("setPublicProperty"))
24+
}
25+
26+
@Test
27+
fun `an internal setter on a public property only hides the setter`() {
28+
assertFalse(isInternal("getInternalSetterProperty"))
29+
assertTrue(isInternal("setInternalSetterProperty"))
30+
}
31+
32+
@Test
33+
fun `internal functions are still detected`() {
34+
assertTrue(isInternal("internalFunction"))
35+
assertFalse(isInternal("publicFunction"))
36+
}
37+
38+
// Kotlin mangles internal members as name$module, and the module segment depends on the
39+
// compiling project, so tests match on the declared name instead of the full JVM name.
40+
private fun isInternal(declaredName: String): Boolean {
41+
val matches = descriptor.methods.filter { it.name == declaredName || it.name.startsWith("$declaredName$") }
42+
assertTrue("No method found for '$declaredName'", matches.isNotEmpty())
43+
return matches.single().isInternal
44+
}
45+
46+
private fun descriptorFor(clazz: Class<*>): KotlinClassDescriptor {
47+
val resource = clazz.name.replace('.', '/') + ".class"
48+
val javaClass = clazz.classLoader.getResourceAsStream(resource).use {
49+
ClassParser(it, resource).parse()
50+
}
51+
val metadataEntry = javaClass.annotationEntries.single { it.annotationType == "Lkotlin/Metadata;" }
52+
return KotlinClassDescriptor(javaClass, BytecodeMetadataAnnotation(metadataEntry), false)
53+
}
54+
}

0 commit comments

Comments
 (0)