@@ -6,6 +6,8 @@ import org.jetbrains.kotlin.library.abi.AbiClassifierReference
66import org.jetbrains.kotlin.library.abi.AbiDeclarationContainer
77import org.jetbrains.kotlin.library.abi.AbiCompoundName
88import org.jetbrains.kotlin.library.abi.AbiFunction
9+ import org.jetbrains.kotlin.library.abi.AbiModality
10+ import org.jetbrains.kotlin.library.abi.AbiProperty
911import org.jetbrains.kotlin.library.abi.AbiQualifiedName
1012import org.jetbrains.kotlin.library.abi.AbiType
1113import org.jetbrains.kotlin.library.abi.AbiTypeArgument
@@ -187,6 +189,7 @@ internal object KlibScanner {
187189 return emptyList()
188190 }
189191
192+ val classes = classesOf(abi.topLevelDeclarations)
190193 val candidates = abi.topLevelDeclarations.declarations
191194 .filterIsInstance<AbiFunction >()
192195 .filter { matchesInclude(it.qualifiedName, includePrefixes) }
@@ -196,10 +199,132 @@ internal object KlibScanner {
196199 // receiver handle to be worth anything. Filtered rather than declined for the same reason
197200 // `ArtifactScanner` never builds a model for `<init>`.
198201 .filter { ! it.isConstructor }
199- .mapNotNull { candidateOrNull(it, classesOf(abi.topLevelDeclarations) ) }
202+ .mapNotNull { candidateOrNull(it, classes ) }
200203
201- // Same rule, same implementation as the JVM producer -- see this object's KDoc.
202- return ArtifactScanner .disambiguateOverloads(candidates)
204+ // Issue #237: the member functions of the klib's classes, as #215 binds them from a jar.
205+ val members = classes.values
206+ .filter { matchesInclude(it.qualifiedName, includePrefixes) }
207+ .filter { ! ArtifactScanner .isExcludedPackage(it.qualifiedName.packageName.toString(), excludePackages) }
208+ .flatMap { memberCandidatesOf(it, classes) }
209+
210+ // Same rule, same implementation as the JVM producer -- see this object's KDoc. A member's key
211+ // is `Owner.name`, so its overload set is its class's alone.
212+ return ArtifactScanner .disambiguateOverloads(candidates + members)
213+ }
214+
215+ /* *
216+ * The member functions of [cls] (issue #237), as `METHOD` entries `Owner.name` -- the shape #215
217+ * gives a jar's members and KSP gives its own: `args[0]` is the receiver, not counted in `arity`,
218+ * and the generated call is `(args[0] as Owner).name(...)`. The binding layer serves them as
219+ * methods of the receiver's proxy, and never as a module attribute.
220+ *
221+ * The class must be able to cross as a handle ([handleClassOrNull]); a generic one's members are
222+ * declined, as on the jar side. A member is declined, with its reason, when it is `suspend`,
223+ * `@Deprecated`, `@PublishedApi`, `@Composable`, a member extension or one with context receivers,
224+ * named like a property of its class (the property's name is the attribute), or typed by something
225+ * without a klib boundary type. Constructors are not read here (stage 3).
226+ */
227+ private fun memberCandidatesOf (cls : AbiClass , classes : Map <String , AbiClass >): List <Candidate > {
228+ val owner = cls.qualifiedName.dotted()
229+ val ownerType = KotlinTypeModel (owner)
230+ val generic = cls.typeParameters.isNotEmpty()
231+ if (! generic && cls.kind != AbiClassKind .OBJECT && handleClassOrNull(ownerType, classes) == null ) return emptyList()
232+ val propertyNames = cls.declarations.filterIsInstance<AbiProperty >()
233+ .mapTo(HashSet ()) { it.qualifiedName.relativeName.toString().substringAfterLast(' .' ) }
234+ // The pinned compiler's klib ABI records modality but no visibility, and lists the `protected`
235+ // members of an open, abstract or sealed class beside its public ones; source outside a subclass
236+ // cannot call a `protected` one. An interface has none, and a final class's are effectively
237+ // private, so only those members bind. An `object`'s functions are statics on the jar side (#53),
238+ // called without a receiver, which this `METHOD` shape is not.
239+ val unbindable = when {
240+ cls.kind == AbiClassKind .OBJECT -> " a function of an object: bound as a static on the jar side (#53), not yet from a klib"
241+ cls.kind == AbiClassKind .CLASS && cls.modality != AbiModality .FINAL ->
242+ " a member of a non-final class: this klib ABI does not say whether it is protected"
243+ else -> null
244+ }
245+ val enum = cls.kind == AbiClassKind .ENUM_CLASS
246+ return cls.declarations.filterIsInstance<AbiFunction >()
247+ .filter { ! it.isConstructor }
248+ // `Any`'s members are not the class's API, as on the jar side; an enum class's
249+ // `valueOf`/`values`/`entries` are statics the ABI lists among its members, and no
250+ // instance call reaches them.
251+ .filter { function -> ! isAnyMember(function) && ! (enum && function.leafName() in ENUM_STATICS ) }
252+ .mapNotNull { function -> memberCandidateOrNull(function, owner, ownerType, generic, unbindable, propertyNames, classes) }
253+ }
254+
255+ private fun memberCandidateOrNull (
256+ function : AbiFunction ,
257+ owner : String ,
258+ ownerType : KotlinTypeModel ,
259+ generic : Boolean ,
260+ unbindable : String? ,
261+ propertyNames : Set <String >,
262+ classes : Map <String , AbiClass >,
263+ ): Candidate ? {
264+ val name = function.leafName()
265+ val model = declarationModelOf(function)?.copy(
266+ simpleName = name,
267+ owner = owner,
268+ ownerIsClass = true ,
269+ receiver = ownerType,
270+ receiverBoundaryTag = OBJECT_TAG ,
271+ kind = METHOD_KIND ,
272+ ) ? : return null
273+ val declined = { reason: String -> Candidate (null , model.copy(declineReason = reason)) }
274+ when {
275+ function.isSuspend -> return declined(" suspend" )
276+ generic -> return declined(" a member of a generic class: not bound yet" )
277+ unbindable != null -> return declined(unbindable)
278+ function.hasAnnotation(DEPRECATED ) ->
279+ return declined(" @Deprecated: its level is not in this klib ABI, and a HIDDEN one cannot be called from source" )
280+ function.hasAnnotation(PUBLISHED_API ) ->
281+ return declined(" @PublishedApi internal -- binary-visible for inlining, not part of the module's public Kotlin API" )
282+ function.hasAnnotation(COMPOSABLE ) -> return declined(" a @Composable member function: not bound on a proxy" )
283+ function.hasExtensionReceiverParameter -> return declined(" a member extension: it needs two receivers" )
284+ function.contextReceiverParametersCount > 0 ->
285+ return declined(" ${function.contextReceiverParametersCount} context receiver(s), whose types this klib ABI does not expose" )
286+ name in propertyNames -> return declined(" shares its name with a property of the class, which keeps the attribute" )
287+ // A data class's `copy`/`componentN` are synthetics the jar side never binds (U-4), and this
288+ // ABI cannot tell them from a hand-written member of the same name.
289+ name == " copy" || DATA_COMPONENT .matches(name) ->
290+ return declined(" a data-class synthetic (copy/componentN), or a member this klib ABI cannot tell from one" )
291+ }
292+ val returnType = klibBoundaryTypeOf(model.returnType, classes)
293+ ? : return declined(" no boundary type for return ${model.returnType.render()} " )
294+ val boundaryTypes = model.parameters.map { parameter ->
295+ klibBoundaryTypeOf(parameter.type, classes)
296+ ? : return declined(" no boundary type for parameter ${parameter.type.render()} " )
297+ }
298+ if (boundaryTypes.any { it.isReturnOnly }) return declined(" a parameter of a return-only boundary type" )
299+
300+ val key = " $owner .$name "
301+ val argumentExpressions = boundaryTypes.mapIndexed { index, type -> type.read(" args[${index + 1 } ]" ) }
302+ val body = returnType.wrapReturn(" (args[0] as $owner ).$name (${argumentExpressions.joinToString(" , " )} )" )
303+ val callable = ArtifactCallable (
304+ name = key,
305+ arity = boundaryTypes.size,
306+ paramTags = boundaryTypes.map { it.tag },
307+ returnTag = returnType.tag,
308+ // The receiver is read even with no declared parameter, so the body always takes `args`.
309+ lambdaBody = " { args -> $body }" ,
310+ receiverTypeName = owner,
311+ paramTypeNames = model.parameters.map { it.type.qualifiedName },
312+ returnTypeName = model.returnType.qualifiedName,
313+ returnSupertypes = if (returnType.tag == OBJECT_TAG ) supertypesOf(model.returnType.qualifiedName, classes) else emptyList(),
314+ // Every argument is passed, as for a top-level klib entry (see [candidateOrNull]).
315+ paramHasDefault = List (boundaryTypes.size) { false },
316+ kind = METHOD_KIND ,
317+ )
318+ return Candidate (
319+ callable = callable,
320+ declaration = model.copy(
321+ bindingName = key,
322+ returnBoundaryTag = returnType.tag,
323+ parameters = model.parameters.mapIndexed { index, parameter ->
324+ parameter.copy(boundaryTag = boundaryTypes[index].tag, declaresDefault = false )
325+ },
326+ ),
327+ )
203328 }
204329
205330 /* *
@@ -471,6 +596,21 @@ internal object KlibScanner {
471596
472597 private const val OBJECT_TAG = " OBJECT"
473598
599+ private const val METHOD_KIND = " METHOD"
600+
601+ private val ENUM_STATICS = setOf (" valueOf" , " values" , " entries" )
602+
603+ private val DATA_COMPONENT = Regex (" component[1-9][0-9]*" )
604+
605+ private fun AbiFunction.leafName (): String = qualifiedName.relativeName.toString().substringAfterLast(' .' )
606+
607+ /* * `toString()`, `hashCode()` and `equals(other)`: what every class has from `Any`. */
608+ private fun isAnyMember (function : AbiFunction ): Boolean = when (function.leafName()) {
609+ " toString" , " hashCode" -> function.valueParameters.isEmpty()
610+ " equals" -> function.valueParameters.size == 1
611+ else -> false
612+ }
613+
474614 /* *
475615 * The walked klib's class [type] names, when a handle of it can cross (issue #234): a public
476616 * (the ABI lists nothing else) `class`, `interface`, `enum class` or `object` that is not a value
0 commit comments