Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ All in `PM/commonTest`, so they run wherever the interpreter loads.
for a public setter with every class type parameter unbounded, `SETTER` entries `Owner.prop=`; and
top-level extension property getters (`pkg.prop`, receiver in `receiverTypeName`). Properties take no
part in overload naming or in a constructor's name check; a property key another binding already
holds is declined, and so is a property whose receiver has a supertype missing from the consumer's compile classpath (kotlinc cannot build its member scope). `Status: implemented` — `GP/artifact/PropertyBindingTest.kt`.
holds is declined, and so is a property or an extension function whose receiver has a supertype missing from the consumer's compile classpath (kotlinc cannot build its member scope; the reason names the missing supertype, #66). `Status: implemented` — `GP/artifact/PropertyBindingTest.kt` (properties and, #66, extension functions).
The public instance functions of a Kotlin-public `object` bind under the object's name like a
static (`Arrangement.spacedBy`, #53), except `Any`'s members, ones that also exist as a
`@JvmStatic` static, and `@Composable` ones (declined); an `internal` or file-private object is
Expand Down Expand Up @@ -268,7 +268,7 @@ All in `PM/commonTest`, so they run wherever the interpreter loads.
a comment with the same marker says so (`GP/stubs/PropertyStubTest.kt`). An object constant is annotated with its
declared type even when that type is nested in the object (`Alignment.End: Horizontal`, a class of the
object's own module; elsewhere `androidx.compose.ui.Alignment.Horizontal`); the object's own type
stays `Any` (#53, `GP/stubs/ObjectStubTest.kt`).
stays `Any` (#53, `GP/stubs/ObjectStubTest.kt`). An extension property whose receiver is a type nested in such an object is a `@property` of that nested class in the object's module (`Icons.Default: Filled`, `Filled.Add: ImageVector`, #68), not a marker comment in the package module.
- **B-8** CI generates the stubs over the Compose version the build resolves and publishes them
(`.github/workflows/stubs.yml`): workflow artifact `kotlin-stubs` on every push to `develop`, with a
README naming the Compose version and the commit, and `kotlin-stubs.zip` on every `v*` tag's release.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1911,6 +1911,17 @@ internal object ArtifactScanner {
if (function.hasUnsubstitutableTypeParameters) {
return declined("a bounded or reified type parameter: kotlin.Any? cannot stand for it")
}
// Issue #66: `receiver.alias(...)` resolves against the receiver's member scope, which needs
// every supertype of it -- the rule an extension property's receiver already gets.
if (function.isExtension) {
val receiverName = (function.receiverType?.classifier as? KmClassifier.Class)?.name
if (receiverName != null) {
val missing = classpath.unreachableSupertypeOf(receiverName.replace('.', '$'))
if (missing != null) {
return declined(unreachableReceiverReason(missing, receiverName.replace('/', '.')))
}
}
}

// Read before `resolveKotlinType` is asked anything, because for a function-typed parameter
// it is the one that would answer -- and would answer "no". See [functionSlotOrNull].
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,32 @@ class PropertyBindingTest {
assertTrue("fixture.artifactproperty.reach" in withSupertype, withSupertype.toString())
}

/**
* #66: an extension *function* on the same receiver goes through the same cast, so it is declined
* the same way and bound once the classpath carries the supertype.
*
* Red before the fix: `probe` is bound (the first assertion fails).
*/
@Test
fun aReceiverWithASupertypeOffTheClasspathHasItsExtensionFunctionsDeclined() {
val names = entries.map { it.name }
assertTrue("fixture.artifactproperty.probe" !in names, names.toString())

val probe = ArtifactScanner.scanDeclarations(fixtureClasses, includePrefixes = listOf("fixture.artifactproperty"))
.single { it.simpleName == "probe" }
assertEquals(null, probe.bindingName, probe.toString())
val reason = assertNotNull(probe.declineReason, probe.toString())
assertTrue("org.objectweb.asm.Opcodes" in reason && "classpath" in reason, reason)

val asm = File(org.objectweb.asm.Opcodes::class.java.protectionDomain.codeSource.location.toURI())
val withSupertype = ArtifactScanner.scanJar(
fixtureClasses,
includePrefixes = listOf("fixture.artifactproperty"),
classpath = listOf(fixtureClasses, asm),
).map { it.name }
assertTrue("fixture.artifactproperty.probe" in withSupertype, withSupertype.toString())
}

// -------------------------------------------------------------------------- the fragment

/** A property is not an extension slot at the boundary: `isExtension = false`, receiver still named. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,6 @@ class Detached : org.objectweb.asm.Opcodes {

/** The same receiver, read through an extension property. */
val Detached.reach: Int get() = level

/** The same receiver, called through an extension function (#66). */
fun Detached.probe(): Int = level
Original file line number Diff line number Diff line change
Expand Up @@ -128,4 +128,40 @@ class ObjectStubTest {
assertTrue("Start: Horizontal" in text, text)
assertFalse("_t.Any" in text, text)
}

// ------------------------------------------- an extension property on a type nested in an object

/**
* #68: `Icons.Default.Add`. `Default` is an object constant of `Icons` typed `Icons.Filled`, and `Add`
* is an extension property declared in the package `...icons.filled` on that nested type. The stub
* class is `Filled` in the object's own module `...icons.Icons` (#53), and `Add` is its property.
*
* Not red against develop at fa2558e3: #53 already resolves the receiver into the object's module,
* and the regenerated Compose stubs (CI run 37098191598) carry `Default: Filled` and
* `Filled.Add`. The marker comment of #68 came from the stubs built before #53. This pins it.
*/
@Test
fun anExtensionPropertyOnATypeNestedInAnObjectIsAttributeOfThatTypesClass() {
val icons = "androidx.compose.material.icons.Icons"
val filled = KotlinTypeModel("$icons.Filled")
val default = constant("$icons.Default", filled)
val add = DeclarationModel(
simpleName = "Add",
owner = "androidx.compose.material.icons.filled",
ownerIsClass = false,
receiver = filled,
receiverBoundaryTag = "OBJECT",
parameters = emptyList(),
returnType = KotlinTypeModel("androidx.compose.ui.graphics.vector.ImageVector"),
returnBoundaryTag = "OBJECT",
bindingName = "androidx.compose.material.icons.filled.Add",
kind = "GETTER",
)
val files = renderKotlinFqnStubs(listOf(default, add))
val text = file(files, icons)
assertTrue(Regex("""^Default: Filled$""", RegexOption.MULTILINE).containsMatchIn(text), text)
assertTrue("class Filled:" in text, text)
assertTrue(" def Add(self) -> androidx.compose.ui.graphics.vector.ImageVector:" in text, text)
files.values.forEach { assertFalse("is not stubbed" in it, it) }
}
}
11 changes: 10 additions & 1 deletion tools/stubs/consumer.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@
and mypy runs with `warn_unused_ignores`, so a stub that stopped rejecting one fails the check too.
"""

from typing import assert_type

from androidx.compose.foundation.layout import Column, Row, fillMaxWidth, padding__Dp
from androidx.compose.material3 import Checkbox, Text
from androidx.compose.material.icons import Icons
from androidx.compose.material3 import Checkbox, Icon, Text
from androidx.compose.ui import Modifier
from androidx.compose.ui.graphics.vector import ImageVector

# A chain started from the class, the way Kotlin writes `Modifier.padding(16.dp)`.
chain: Modifier = Modifier.fillMaxWidth().padding(16.0).size(24.0)
Expand Down Expand Up @@ -40,6 +44,11 @@ def content() -> None:
Column(modifier=chain, content=lambda scope: None)
Row(content=lambda scope: None)

# An extension property on a type nested in an object (issue #68): `Icons.Default` is `Icons.Filled`.
# `assert_type`, not an annotation: an `Any` (the stub before #53/#67) would satisfy an annotation.
assert_type(Icons.Default.Add, ImageVector)
Icon(Icons.Default.Add, contentDescription=None)

# Rejected on purpose. (`padding("sixteen")` is not among them: one `padding` overload takes a
# `PaddingValues`, a type the stubs cannot name -- `PaddingValues(...)` the factory owns the name -- so
# it is `Any` and accepts anything.)
Expand Down
Loading