From 88761fe0cac2bd84c2fca608cfbdb2b5b0e73dfd Mon Sep 17 00:00:00 2001 From: Oleg Yukhnevich Date: Tue, 6 Aug 2024 20:38:14 +0300 Subject: [PATCH 1/6] [KDoc] Resolution of links to extensions --- proposals/kdoc/links-to-extensions.md | 831 ++++++++++++++++++++++++++ 1 file changed, 831 insertions(+) create mode 100644 proposals/kdoc/links-to-extensions.md diff --git a/proposals/kdoc/links-to-extensions.md b/proposals/kdoc/links-to-extensions.md new file mode 100644 index 000000000..7647eb36e --- /dev/null +++ b/proposals/kdoc/links-to-extensions.md @@ -0,0 +1,831 @@ +# Resolution of links to extensions + +* **Type**: KDoc proposal +* **Author**: Oleg Yukhnevich +* **Status**: Submitted +* **Target issue**: [dokka/3555](https://github.com/Kotlin/dokka/issues/3555) +* **Discussion**: TBD + +## Summary + +Define clear logic on how to resolve links to extensions when writing documentation for Kotlin code. + +## Motivation + +References to extensions where receiver has type parameters, such as `List` or `KSerializer` currently works +differently in K1 comparing to K2 as described in [dokka/3555](https://github.com/Kotlin/dokka/issues/3555). + +```kotlin +/** + * resolved with both K1 and K2 analysis (all extensions are defined on [Iterable]) + * - [Iterable.map] + * - [Iterable.flatMap] + * - [Iterable.flatten] + * - [Iterable.min] + * resolved with K1, unresolved with K2 analysis ([List] implements [Iterable]) + * - [List.map] + * - [List.flatMap] + * unresolved with both K1 and K2 analysis + * - [List.flatten] (defined as `fun Iterable>.flatten()`) + * - [List.min] (defined as `fun > Iterable.min()`) + */ +fun testLists() {} +``` + +## Overview + +### Syntax for references to extensions + +References to extension functions/properties can be done in two ways: via simple reference like `[functionName]` or with +additional receiver `Type` like `[Type.functionName]`. The behaviour for extension properties is the same as for +extension functions and while a document will mostly use `function` wording, it could be treated as `function/property`. + +```kotlin +fun String.extension() {} + +/** + * [extension] - resolved + * [String.extension] - resolved + */ +fun testStringExtension() {} +``` + +Extension function can be referenced in the same way as just top level function (`[functionName]`), but, there are cases +when there are multiple extensions with the same name in the same package and so in this case `[Type.functionName]` can +be used to resolve reference to specific function (specific case of +overloads: [KT-15984](https://youtrack.jetbrains.com/issue/KT-15984/Quick-documentation-Kdoc-doesnt-support-specifying-a-particular-overloaded-function-or-variable-in-a-link), [dokka/80](https://github.com/Kotlin/dokka/issues/80)). +It can also be used to represent reference in comment more naturally so that reference to extensions will look the same +way as reference to members. References to extensions functions via `[Type.functionName]` was tracked (most likely) +in [KT-13299](https://youtrack.jetbrains.com/issue/KT-13299). + +Simple example for such an ambiguity is from kotlinx-coroutines `CoroutineScope.isActive` and +`CoroutineContext.isActive`: + +```kotlin +fun CoroutineScope.isActive() {} +fun CoroutineContext.isActive() {} + +/** + * [isActive] - resolved, (IDE redirects to first call) + * [CoroutineScope.isActive] - resolved + * [CoroutineContext.isActive] - resolved + */ +fun testCoroutines() {} +``` + +### Multiple ways to reference the same declaration + +References to member functions are resolved only via `Type.functionName` where `Type` could be both type where the +function is declared, and all it’s inheritors: + +```kotlin +interface Parent { + fun member() +} +interface Child : Parent + +/** + * [member] - UNRESOLVED + * [Parent.member] - resolved + * [Child.member] - resolved + * [String.member] - UNRESOLVED + */ +fun testMember() {} +``` + +As with references to member functions, it’s possible to reference extensions not only on `Type` declared as receiver in +extensions, but also on its inheritor, f.e: + +```kotlin +interface Parent +interface Child : Parent + +fun Parent.extension() {} + +/** + * [extension] - resolved + * [Parent.extension] - resolved + * [Child.extension] - resolved + * [String.extension] - UNRESOLVED + */ +fun testExtension() {} +``` + +References to extensions where receiver is generic work in the same way. In this case, implicit bound is `Any?`: + +```kotlin +interface Parent +interface Child : Parent + +fun T.genericExtension() {} + +/** + * [genericExtension] - resolved + * [Parent.genericExtension] - resolved + * [Child.genericExtension] - resolved + * [String.genericExtension] - resolved + */ +fun testGenericExtension() {} +``` + +It’s also possible to define explicit bound `Type` for generic. In this case extensions will be resolved on all types +which are matched by generic: + +```kotlin +interface Parent +interface Child : Parent + +fun T.genericBoundExtension() {} + +/** + * [genericBoundExtension] - resolved + * [Parent.genericBoundExtension] - resolved + * [Child.genericBoundExtension] - resolved + * [String.genericBoundExtension] - UNRESOLVED + */ +fun testGenericBoundExtension() {} +``` + +All of this causes the following links to be resolved: + +```kotlin +interface Parent +interface Child : Parent + +fun Any.anyExtension() {} + +/** + * [anyExtension] - resolved + * [Any.anyExtension] - resolved + * [Parent.anyExtension] - resolved + * [Child.anyExtension] - resolved + * [String.anyExtension] - resolved + */ +fun testAnyExtension() {} + +/////////////////////////////// + +// `let` is from stdlib +// inline fun T.let(block: (T) -> R): R + +/** + * [let] - resolved + * [Any.let] - resolved + * [Parent.let] - resolved + * [Child.let] - resolved + * [String.let] - resolved + */ +fun testLet() {} +``` + +## Problem description + +As stated in the beginning, on current moment references to extensions where receiver has type parameters, such as +`List` or `KSerializer` works differently in K1 comparing to K2 as there is no clear specification on how it +should work. + +The following examples will use those shared declarations: + +```kotlin +// simple root interface with generic: List, Deferred, KSerializer, etc. +interface Container { + fun containerMember(): A +} + +// intermediate interface (which can contain other methods): MutableList, CompletableDeferred, etc. +interface TContainer : Container + +// interface with added bound for generic: custom KSerializer which could encapsulate serialization logic for numbers +abstract class TNumberBoundContainer : Container + +// final implementations with fixed generic +class NumberContainer : Container +object IntContainer : Container +object StringContainer : Container +``` + +There is no difference when referencing member functions from types with or without type parameters as well as for +extensions with star-projection: + +```kotlin +// shared declarations, copied, for sample completeness +interface Container { + fun containerMember(): A +} +interface TContainer : Container +abstract class TNumberBoundContainer : Container +class NumberContainer : Container +object IntContainer : Container +object StringContainer : Container + +/** + * [Container.containerMember] - resolved + * [TContainer.containerMember] - resolved + * [TNumberBoundContainer.containerMember] - resolved + * [NumberContainer.containerMember] - resolved + * [IntContainer.containerMember] - resolved + * [StringContainer.containerMember] - resolved + */ +fun testContainerMember() {} + +/////////////////////////////// + +fun Container<*>.containerExtension() {} + +/** + * [Container.containerExtension] - resolved + * [TContainer.containerExtension] - resolved + * [TNumberBoundContainer.containerExtension] - resolved + * [NumberContainer.containerExtension] - resolved + * [IntContainer.containerExtension] - resolved + * [StringContainer.containerExtension] - resolved + */ +fun testContainerExtension() {} +``` + +In the case of extensions with fixed generics, we have several unexpected unresolved references, both with K1 and K2: + +```kotlin +// shared declarations, copied, for sample completeness +interface Container { + fun containerMember(): A +} +interface TContainer : Container +abstract class TNumberBoundContainer : Container +class NumberContainer : Container +object IntContainer : Container +object StringContainer : Container + +// type parameter is fixed to final class +fun Container.containerIntExtension() {} + +/** + * [Container.containerIntExtension] - resolved + * [TContainer.containerIntExtension] - UNRESOLVED - UNEXPECTED + * [TNumberBoundContainer.containerIntExtension] - UNRESOLVED - UNEXPECTED + * [NumberContainer.containerIntExtension] - UNRESOLVED - expected + * [IntContainer.containerIntExtension] - resolved + * [StringContainer.containerIntExtension] - UNRESOLVED - expected + */ +fun testContainerIntExtension() {} + +// type parameter is fixed to abstract class +fun Container.containerFixedNumberExtension() {} + +/** + * [Container.containerFixedNumberExtension] - resolved + * [TContainer.containerFixedNumberExtension] - UNRESOLVED - UNEXPECTED + * [TNumberBoundContainer.containerFixedNumberExtension] - UNRESOLVED - UNEXPECTED + * [NumberContainer.containerFixedNumberExtension] - resolved + * [IntContainer.containerFixedNumberExtension] - UNRESOLVED - expected + * [StringContainer.containerFixedNumberExtension] - UNRESOLVED - expected + */ +fun testContainerFixedNumberExtension() {} + +// type parameter is out projection of abstract class +fun Container.containerOutNumberExtension() {} + +/** + * [Container.containerOutNumberExtension] - resolved + * [TContainer.containerOutNumberExtension] - UNRESOLVED - UNEXPECTED + * [TNumberBoundContainer.containerOutNumberExtension] - resolved + * [NumberContainer.containerOutNumberExtension] - resolved + * [IntContainer.containerOutNumberExtension] - resolved + * [StringContainer.containerOutNumberExtension] - UNRESOLVED - expected + */ +fun testContainerOutNumberExtension() {} +``` + +We can’t resolve `TContainer.containerIntExtension` even if `TContainer` just extends `Container` propagating `T` +without any additional constraints. The same is applied for a case with bounded generic (`TNumberBoundContainer`). +Both those cases can be successfully resolved by compiler, and those functions could be called on corresponding +receivers. + +Initially it was not possible to reference in such a way functions which has generic with bound type arguments (at least +on IDEA side), it was fixed for K1 +in [https://youtrack.jetbrains.com/issue/KTIJ-24576](https://youtrack.jetbrains.com/issue/KTIJ-24576). +In K2 on the current moment the simplest solution is implemented, where only receiver of extension function is resolved +as `Type`: + +```kotlin +// shared declarations, copied, for sample completeness +interface Container { + fun containerMember(): A +} +interface TContainer : Container +abstract class TNumberBoundContainer : Container +class NumberContainer : Container +object IntContainer : Container + +fun Container.containerGenericExtension() {} + +/** + * [Container.containerGenericExtension] - resolved + * [TContainer.containerGenericExtension] - UNRESOLVED ONLY in K2 + * [TNumberBoundContainer.containerGenericExtension] - UNRESOLVED ONLY in K2 + * [NumberContainer.containerGenericExtension] - UNRESOLVED ONLY in K2 + * [IntContainer.containerGenericExtension] - UNRESOLVED ONLY in K2 + */ +fun testContainerGenericExtension() {} + +/////////////////////////////// + +fun Container.containerGenericBoundExtension() {} + +/** + * [Container.containerGenericBoundExtension] - resolved + * [TContainer.containerGenericBoundExtension] - UNRESOLVED - UNEXPECTED + * [TNumberBoundContainer.containerGenericBoundExtension] - UNRESOLVED ONLY in K2 + * [NumberContainer.containerGenericBoundExtension] - UNRESOLVED ONLY in K2 + * [IntContainer.containerGenericBoundExtension] - UNRESOLVED ONLY in K2 + */ +fun testContainerGenericBoundExtension() {} +``` + +Note: K2 compiler resolves declarations which are unresolved only in K2 when used in code. + +More complex scenarios: + +```kotlin +fun > R.modify(block: R.() -> Unit): R +fun , R : Iterable> R.modifyComparable(block: R.() -> Unit): R + +/** + * [modify] - resolved with K1 and K2 + * [Iterable.modify] - resolved with K1 and K2 + * [List.modify] - resolved with K1, UNRESOLVED with K2 + */ +fun testModify() {} + +/** + * [modifyComparable] - resolved with K1 and K2 + * [Iterable.modifyComparable] - UNRESOLVED with K1, resolved with K2 + * [List.modifyComparable] - UNRESOLVED with K1 and K2 + */ +fun testModifyComparable() {} +``` + +## Proposed solution + +**Resolve references for any `Type` which can be used as a receiver in code (resolved by compiler).** + +References to extensions in a form of `Type.functionName` should be treated in the same way as with member functions, so +it should be possible to reference extensions defined for supertypes. When functions have generics or bounds, those +functions which can be called on `Type` should be resolved. + +This is conformed to the behavior of current analysis with fixes of some issues with resolve mentioned with `UNEXPECTED` +in examples (there could be other examples not mentioned in the document), so there are no breaking changes but rather +an attempt at defining the general rules. + +Such resolution logic brings several benefits such as: + +1. References to members and extensions are defined in the same way for types and its inheritors; +2. References in KDoc and function calls use the same semantics; +3. If a member is converted to an extension (and vice versa), all KDoc references still will be resolved; + +When thinking about type parameters matching in KDoc, it's possible to infer type constraints/requirements to better see +matching rules. +The rules in compiler are more complex than that, this is just a basic idea: + +* requirements are coming from extensions: + * for `fun List.something()` - generic type requirement will be `out Any?` + * for `fun List.somethingNumber()` - generic type requirement will be `out Number` +* constraints are coming from types: + * for `interface List` - generic type constraint will be `out Any?` + * for `interface Container : List` - generic type constraint will be `out Any?` + * for `interface NContainer : List` - generic type constraint will be `out Number` + * for `interface NNContainer : List` - generic type constraint will be `out Number` + * for `interface SContainer : List` - generic type constraint will be `out String` + * for `interface NoVariance` - generic type constraint will be still `out Any?` as any type is possible + * for `interface NNoVariance : NoVariance` - generic type constraint will be `Number` (no `out` variance) +* Matching: + * `something` should match any subtype of `List` as it's a type constraint is `out Any?` + * `somethingNumber` should match only when `Type` is subtype which have matching constraint `out Number`. + So f.e `SContainer` is not available. + +Coming back to the first example, with this proposal all links should be resolved: + +```kotlin +/** + * - [Iterable.map] + * - [Iterable.flatMap] + * - [Iterable.flatten] + * - [Iterable.min] + * - [List.map] + * - [List.flatMap] + * - [List.flatten] (defined as `fun Iterable>.flatten()`) + * - [List.min] (defined as `fun > Iterable.min()`) + */ +fun testLists() {} +``` + +## Alternatives considered + +There were a few alternatives considered but were rejected because of various reasons: + +* Resolve references where `Type` is the same type which is used as receiver in an extension. If receiver is + generic, resolve only `functionName` syntax. Rejected because: + * in case of migration from member to extension or widening of a receiver type, old KDoc references will be broken. + F.e: + ```kotlin + // version 1 of the lib + fun String.doSomething() {} + + // in an app + /** + * [String.doSomething] - resolved + */ + fun testSomething() { + "".doSomething() // resolved + } + + // then in version 2 of the lib + fun CharSequence.doSomething() {} + + // in an app + /** + * [String.doSomething] - becomes unresolved + */ + fun testSomething() { + "".doSomething() // still resolved + } + ``` + * in case of type hierarchies with extensions on supertypes, it's easy to leak an abstraction when referencing + links. F.e in the following example `[HeadersBuilder.append]` will be unresolved, and so we + need to use `[MessageBuilder.append]` which looks and inconsistent and when reading requires more attention, as + you now need to know the hierarchy: + ```kotlin + interface MessageBuilder { fun build(): String } + interface HeadersBuilder: MessageBuilder + + fun MessageBuilder.append(text: String) {} + fun HeadersBuilder.appendIfAbsent(text: String) {} + + /** + * [HeadersBuilder.appendIfAbsent] and [MessageBuilder.append] can be used to configure an object + * after that [HeadersBuilder.build] can be used to build the object + */ + fun testHeaders(builder: HeadersBuilder) {} + ``` +* Resolve references only by `functionName` and do not resolve `Type.functionName` at all, so treat extension functions + as just functions. Rejected because: + * extensions in Kotlin are first-class entities, and from a call-site perspective they look like regular functions. + So it's not convenient when in KDoc you should reference them a lot differently then in code. + * big breaking change with no clear benefits + +## Other languages + +* [Java](https://docs.oracle.com/en/java/javase/22/docs/specs/javadoc/doc-comment-spec.html): + * No extensions + * Member references are referenced in the same way as in Kotlin (including inheritors) +* [Scala](https://docs.scala-lang.org/overviews/scaladoc/for-library-authors.html) + * Extensions are defined similar to Kotlin + * Referencing extensions is possible only by top-level `functionName` without relying on receiver + +```scala +class Point(val x: Int) + +extension(p: Point) +def ext: Int = p.x + +/** + * [[Point.x]] - resolved, member + * [[Point.ext]] - unresolved + * [[ext]] - resolved + */ +def test(): Unit = {} +``` + +* [Dart](https://dart.dev/language/comments\#documentation-comments) + * Extensions are defined in named `objects` + * Extensions can be referenced only via this named object + +```dart +class DateTime { + int get year; +} +extension DateTimeCopyWith on DateTime { + DateTime copyWith () { ... } +} + +/// [DateTime.year] - resolved, member +/// [DateTime.copyWith] - unresolved +/// [DateTimeCopyWith.copyWith] - resolved +void test () {} +``` + +* [Rust](https://doc.rust-lang.org/rustdoc/how-to-write-documentation.html) + * Same as dart, but extensions are defined in traits + * Extensions can be referenced only via trait + * RustRover supports resolving via the main type while `cargo doc` can’t resolve them + +```rust +trait OptionExt < A > { + fn wrap (&self) -> Option<&Self> { Some(self) } +} + +impl OptionExt < A > for A {} + +/// [Option::is_some] - resolved, member function +/// [Option::wrap] - unresolved by `cargo doc`, resolved by RustRover +/// [`Option::wrap`] - same as `Option::wrap` +/// [OptionExt::wrap] - resolved +/// [`OptionExt::wrap`] - resolved +fn main () {} +``` + +* [Swift](https://www.swift.org/documentation/docc/) + * Extensions are declared in `unnamed` blocks + * Generics have different semantics/syntax for structs and protocols + * Referencing extensions is possible in the same way as in Kotlin + * Mostly tries to resolve those references, which can be called in code (resolved by compiler) + * When there are generics involved results are not always predictable and `Docc` works inconsistently comparing to + `XCode` + +```swift +public protocol Container { + associatedtype Item +} +public protocol TContainer: Container {} +public protocol TSignedIntegerContainer: Container where Item: SignedInteger {} +public struct SignedIntegerContainer < T: SignedInteger > : Container { + public typealias Item = T +} +public struct IntContainer: Container { + public typealias Item = Int +} +public struct IntTContainer: TContainer { + public typealias Item = Int +} +public struct IntTSignedIntegerContainer: TSignedIntegerContainer { + public typealias Item = Int +} +public struct StringContainer: Container { + public typealias Item = String +} + +// extensions +// kotlin analogs are: +// - fun Container<*>.extensionOnAny() +// - fun Container.extensionOnAny() +public extension Container { + func extensionOnAny (){} +} +// kotlin analogs are: +// - fun Container.extensionOnInt() +public extension Container where Item == Int { + func extensionOnInt (){} +} +// kotlin analogs are: +// - fun Container.extensionOnSignedInteger() +// - fun Container.extensionOnSignedInteger() +public extension Container where Item: SignedInteger { + func extensionOnSignedInteger (){} +} +public extension Container where Item == String { + func extensionOnString (){} +} + +/// ``Container/extensionOnAny()`` - resolved +/// ``Container/extensionOnInt()`` - resolved +/// ``Container/extensionOnSignedInteger()`` - resolved +/// ``Container/extensionOnString()`` - resolved +/// +/// ``TContainer/extensionOnAny()`` - unresolved in docc, resolved in XCode +/// ``TContainer/extensionOnInt()`` - unresolved +/// ``TContainer/extensionOnSignedInteger()`` - unresolved +/// ``TContainer/extensionOnString()`` - unresolved +/// +/// ``TSignedIntegerContainer/extensionOnAny()`` - unresolved in docc, resolved in XCode +/// ``TSignedIntegerContainer/extensionOnInt()`` - unresolved +/// ``TSignedIntegerContainer/extensionOnSignedInteger()`` - unresolved +/// ``TSignedIntegerContainer/extensionOnString()`` - unresolved +/// +/// ``SignedIntegerContainer/extensionOnAny()`` - resolved +/// ``SignedIntegerContainer/extensionOnInt()`` - unresolved in XCode, resolved in docc +/// ``SignedIntegerContainer/extensionOnSignedInteger()`` - unresolved in XCode, resolved in docc +/// ``SignedIntegerContainer/extensionOnString()`` - unresolved in XCode, resolved in docc +/// +/// ``IntContainer/extensionOnAny()`` - resolved +/// ``IntContainer/extensionOnInt()`` - unresolved in XCode, resolved in docc +/// ``IntContainer/extensionOnSignedInteger()`` - unresolved in XCode, resolved in docc +/// ``IntContainer/extensionOnString()`` - unresolved +/// +/// ``IntTContainer/extensionOnAny()`` - resolved +/// ``IntTContainer/extensionOnInt()`` - unresolved in XCode, resolved in docc +/// ``IntTContainer/extensionOnSignedInteger()`` - unresolved in XCode, resolved in docc +/// ``IntTContainer/extensionOnString()`` - unresolved +/// +/// ``IntTSignedIntegerContainer/extensionOnAny()`` - resolved +/// ``IntTSignedIntegerContainer/extensionOnInt()`` - unresolved in XCode, resolved in docc +/// ``IntTSignedIntegerContainer/extensionOnSignedInteger()`` - unresolved in XCode, resolved in docc +/// ``IntTSignedIntegerContainer/extensionOnString()`` - unresolved +/// +/// ``StringContainer/extensionOnAny()`` - resolved +/// ``StringContainer/extensionOnInt()`` - unresolved +/// ``StringContainer/extensionOnSignedInteger()`` - unresolved +/// ``StringContainer/extensionOnString()`` - unresolved in XCode, resolved in docc +public protocol Test {} +``` + +## Appendix + +Some notes regarding references to extension functions, which are nice to know. + +### It’s not possible to use generics or nullable types as `Type` during referencing extensions + +So it’s not possible to distinguish references between such extensions (same issues as with overloading by arguments): + +```kotlin +/** + * [stringExtension] - resolved + * [String.stringExtension] - resolved + * [String?.stringExtension] - UNRESOLVED + */ +fun testString() {} + +fun String.stringExtension() {} +fun String?.stringExtension() {} + +/////////////////////////////// + +/** + * [listExtension] - resolved + * [List.listExtension] - resolved + * [List.listExtension] - UNRESOLVED + * [List.listExtension] - UNRESOLVED + */ +fun testList() {} + +fun List.listExtension() {} +fun List.listExtension() {} +``` + +### If an extension has the same name as a member, it’s possible to reference an extension only via + +`functionName` syntax + +```kotlin +interface Something { + fun doWork() +} + +fun Something.doWork(argument: String) {} + +/** + * [doWork] - resolved as extension + * [Something.doWork] - resolved as member + */ +fun testSomething() {} +``` + +It's possible to overcome this on the user side via import aliases: + +```kotlin +package org.example + +import org.example.doWork as doWorkExtension + +interface Something { + fun doWork() +} + +fun Something.doWork(argument: String) {} + +/** + * [Something.doWorkExtension] - resolved as extension + * [Something.doWork] - resolved as member + */ +fun testSomething() {} +``` + +### Absolute and relative references + +It’s possible to reference declarations both absolutely (with package and class name) and relatively (based on scope and +imports where declaration is defined). Though, with extension functions referenced by `Type` like `[Type.functionName]` +it’s possible to use absolute reference only for `Type`. While when using `[functionName]` absolute reference is +possible only for `functionName`. + +```kotlin +package com.example + +fun String.extension() + +///////////////////// + +package test + +// import com.example.extension + +/** + * [org.example.extension] - absolute, resolved + * [extension] - relative, resolved if it's imported + * [String.extension] - relative, resolved if it's imported + * [kotlin.String.extension] - absolute for the type, relative for function, resolved if it's imported + * [kotlin.String.com.example.extension] - UNRESOLVED + * [com.example.String.extension] - UNRESOLVED + */ +fun test() {} +``` + +Similar behavior exists for extensions defined in `object` (or other scope): + +```kotlin +// import Namespace.namespaceFunction +// import Namespace.namespaceExtension + +object Namespace { + fun namespaceFunction() {} + fun String.namespaceExtension() {} +} + +/** + * [namespaceFunction] - resolved if it's imported + * [namespaceExtension] - resolved if it's imported + * [Namespace.namespaceFunction] - resolved + * [Namespace.namespaceExtension] - resolved + * [String.namespaceExtension] - resolved in K2 ONLY if it's imported + */ +fun testNamespace() {} + + +/** + * [Duration.Companion.seconds] - resolved to `val ***.seconds` defined in `Duration.Companion` (overloaded by receiver) + * [Int.seconds] // resolved to `val Int.seconds` defined in `Duration.Companion` if it's imported + */ +fun testDuration() {} +``` + +The most common type which is affected by this is `kotlin.time.Duration`: + +```kotlin +import kotlin.time.* +import kotlin.time.Duration.Companion.seconds + +/** + * [Duration.Companion.seconds] - resolved to one of `seconds` function defined in `Duration.Companion` (multiple overloads by receiver) + * [Int.seconds] // resolved to `val Int.seconds` defined in `Duration.Companion` if it's imported + */ +fun testDuration() {} +``` + +Rules for the resolve of references could be described like this: + +* `[FUNCTION]` / `[PROPERTY]` / `[CLASS]` + * relative references to **top-level** declarations (including extensions) + * relative references to **imported** declarations (f.e from `companion object`) +* `[PACKAGE.FUNCTION]` / `[PACKAGE.PROPERTY]` / `[PACKAGE.CLASS]` - absolute references to top level declarations + (including extensions) +* `[CLASS.CLASS]` - relative references to inner classes +* `[PACKAGE.CLASS.CLASS]` - absolute references to inner classes +* `[CLASS.FUNCTION]` / `[CLASS.PROPERTY]`: + * relative references to **members** defined in CLASS (or its inheritors) + * relative references to **extensions** defined for CLASS (or its inheritors) +* `[PACKAGE.CLASS.FUNCTION]` / `[PACKAGE.CLASS.PROPERTY]`: + * absolute references to **members** defined in PACKAGE.CLASS (or its inheritors) + * **partially** absolute references to **extensions** defined for PACKAGE.CLASS (or its inheritors). + Partially means that we can specify PACKAGE for CLASS, but if FUNCTION is from different package, this package + should be imported + +### Context parameters + +With the introduction +of [Context parameters](https://github.com/Kotlin/KEEP/blob/context-parameters/proposals/context-parameters.md) in case +we do nothing, the situation will not change and `context` will not affect how references will be resolved. If we treat +`context parameters` (at least initially) as just other function parameters then it falls into the “function parameters +overload” problem mentioned in the begging of the document and will be discussed separately. + +```kotlin +fun function() {} +fun String.extension() {} + +context(scope: Scope) fun contextFunction() {} +context(scope: Scope, scope2: String) fun contextFunction() {} +context(scope: String) fun contextFunction() {} + +context(scope: Scope) fun String.contextExtension() {} + +/** + * [function] - resolved + * [extension] - resolved + * [String.extension] - resolved + * + * No overload by context parameters + * [contextFunction] - resolved + * [contextExtension] - resolved + * [String.contextExtension] - resolved + * + * With the possibility of overload by context parameters + * [(Scope) contextFunction] + * [(Scope) String.contextExtension] + * or + * [context(Scope) contextFunction] + * [context(Scope) String.contextExtension] + */ +fun testContext() {} +``` + +Even if we introduce the possibility of overload by context parameters, most likely it will not affect how references to +extension functions will be resolved. Context parameters just add a layer of reference overloads additionally +to extension functions. It’s out of scope of this document and should be discussed in the scope of support for +referencing specific overloads. From 83aaf8d3c089e6dcb08a88c597bc135f45a6cd95 Mon Sep 17 00:00:00 2001 From: Oleg Yukhnevich Date: Fri, 9 Aug 2024 13:09:35 +0300 Subject: [PATCH 2/6] Update motivation --- proposals/kdoc/links-to-extensions.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/proposals/kdoc/links-to-extensions.md b/proposals/kdoc/links-to-extensions.md index 7647eb36e..842d40e63 100644 --- a/proposals/kdoc/links-to-extensions.md +++ b/proposals/kdoc/links-to-extensions.md @@ -12,8 +12,12 @@ Define clear logic on how to resolve links to extensions when writing documentat ## Motivation -References to extensions where receiver has type parameters, such as `List` or `KSerializer` currently works -differently in K1 comparing to K2 as described in [dokka/3555](https://github.com/Kotlin/dokka/issues/3555). +During the migration of Dokka analysis to K2, several questions arose around KDoc reference resolution. +In particular, it was unclear how a link to an extension should be resolved in the presence of type parameters. +Reference resolution in KDoc is not fully specified, and for this reason some cases are currently implemented +differently in K1 and K2 analysis. + +The goal of this document is to try to describe consistent rules on how KDoc links to extensions should be resolved. ```kotlin /** @@ -374,8 +378,8 @@ it should be possible to reference extensions defined for supertypes. When funct functions which can be called on `Type` should be resolved. This is conformed to the behavior of current analysis with fixes of some issues with resolve mentioned with `UNEXPECTED` -in examples (there could be other examples not mentioned in the document), so there are no breaking changes but rather -an attempt at defining the general rules. +comment in examples (there could be other examples not mentioned in the document), so there are no breaking changes to +how we resolve references but rather an attempt at defining the general rules. Such resolution logic brings several benefits such as: From 0645110e7c1e5a543ae02da19d4706a16aa2bb54 Mon Sep 17 00:00:00 2001 From: Oleg Yukhnevich Date: Fri, 9 Aug 2024 14:21:31 +0300 Subject: [PATCH 3/6] Update wording: * replace using `reference` with `link` and `refer` depending on context * align terminology around function/type parameters * minor context additions --- proposals/kdoc/links-to-extensions.md | 208 ++++++++++++++------------ 1 file changed, 112 insertions(+), 96 deletions(-) diff --git a/proposals/kdoc/links-to-extensions.md b/proposals/kdoc/links-to-extensions.md index 842d40e63..002324d53 100644 --- a/proposals/kdoc/links-to-extensions.md +++ b/proposals/kdoc/links-to-extensions.md @@ -12,9 +12,9 @@ Define clear logic on how to resolve links to extensions when writing documentat ## Motivation -During the migration of Dokka analysis to K2, several questions arose around KDoc reference resolution. +During the migration of Dokka analysis to K2, several questions arose around KDoc links resolution. In particular, it was unclear how a link to an extension should be resolved in the presence of type parameters. -Reference resolution in KDoc is not fully specified, and for this reason some cases are currently implemented +Link resolution in KDoc is not fully specified, and for this reason some cases are currently implemented differently in K1 and K2 analysis. The goal of this document is to try to describe consistent rules on how KDoc links to extensions should be resolved. @@ -38,11 +38,15 @@ fun testLists() {} ## Overview -### Syntax for references to extensions +### Syntax for links to extensions -References to extension functions/properties can be done in two ways: via simple reference like `[functionName]` or with -additional receiver `Type` like `[Type.functionName]`. The behaviour for extension properties is the same as for -extension functions and while a document will mostly use `function` wording, it could be treated as `function/property`. +To refer to an extension function/property, it's possible to use one of the following syntax constructs: + +* `[functionName]` - like with top-level functions or members in class scope +* `[Type.functionName]` where `Type` is a receiver - like with member functions outside class scope + +> Note: the behaviour for links to extension properties is the same as for links to extension functions and while a +> document will mostly use `function` in text, it could be treated as `function/property`. ```kotlin fun String.extension() {} @@ -54,13 +58,14 @@ fun String.extension() {} fun testStringExtension() {} ``` -Extension function can be referenced in the same way as just top level function (`[functionName]`), but, there are cases -when there are multiple extensions with the same name in the same package and so in this case `[Type.functionName]` can -be used to resolve reference to specific function (specific case of -overloads: [KT-15984](https://youtrack.jetbrains.com/issue/KT-15984/Quick-documentation-Kdoc-doesnt-support-specifying-a-particular-overloaded-function-or-variable-in-a-link), [dokka/80](https://github.com/Kotlin/dokka/issues/80)). -It can also be used to represent reference in comment more naturally so that reference to extensions will look the same -way as reference to members. References to extensions functions via `[Type.functionName]` was tracked (most likely) -in [KT-13299](https://youtrack.jetbrains.com/issue/KT-13299). +In case there are multiple extensions with the same name in the same package but different receiver, it's possible to +refer to a specific extension via `[Type.functionName]` (this is a specific case of links to overloaded declarations: +[KT-15984](https://youtrack.jetbrains.com/issue/KT-15984/Quick-documentation-Kdoc-doesnt-support-specifying-a-particular-overloaded-function-or-variable-in-a-link), +[dokka/80](https://github.com/Kotlin/dokka/issues/80)). +Additionally, it could be used to represent a link in a KDoc more naturally, +so that the links to extensions will look in the same way as links to members. +Support for resolution of links to extensions functions via `[Type.functionName]` was tracked in scope +of [KT-13299](https://youtrack.jetbrains.com/issue/KT-13299) (most likely). Simple example for such an ambiguity is from kotlinx-coroutines `CoroutineScope.isActive` and `CoroutineContext.isActive`: @@ -77,10 +82,10 @@ fun CoroutineContext.isActive() {} fun testCoroutines() {} ``` -### Multiple ways to reference the same declaration +### Multiple ways to refer to the same declaration -References to member functions are resolved only via `Type.functionName` where `Type` could be both type where the -function is declared, and all it’s inheritors: +Links to member functions outside class scope are resolved only via `[Type.functionName]` where `Type` could be both +type where the function is declared, and all it’s inheritors: ```kotlin interface Parent { @@ -97,7 +102,7 @@ interface Child : Parent fun testMember() {} ``` -As with references to member functions, it’s possible to reference extensions not only on `Type` declared as receiver in +As with links to member functions, it’s possible to refer to an extension not only on `Type` declared as receiver in extensions, but also on its inheritor, f.e: ```kotlin @@ -115,7 +120,7 @@ fun Parent.extension() {} fun testExtension() {} ``` -References to extensions where receiver is generic work in the same way. In this case, implicit bound is `Any?`: +Links to extensions where the receiver is generic follow the same rules. In this case, implicit bound is `Any?`: ```kotlin interface Parent @@ -132,8 +137,8 @@ fun T.genericExtension() {} fun testGenericExtension() {} ``` -It’s also possible to define explicit bound `Type` for generic. In this case extensions will be resolved on all types -which are matched by generic: +It’s also possible to define explicit bound `Type` for type parameter. +In this case extensions will be resolved on all types which match this bound: ```kotlin interface Parent @@ -184,14 +189,14 @@ fun testLet() {} ## Problem description -As stated in the beginning, on current moment references to extensions where receiver has type parameters, such as -`List` or `KSerializer` works differently in K1 comparing to K2 as there is no clear specification on how it +As stated at the beginning of the document, on current moment links to extensions where receiver has type parameters, +such as `List` or `KSerializer`, works differently in K1 and K2 as there is no clear specification on how it should work. -The following examples will use those shared declarations: +To illustrate the problem, the following examples will use those shared declarations: ```kotlin -// simple root interface with generic: List, Deferred, KSerializer, etc. +// simple root interface with type parameter: List, Deferred, KSerializer, etc. interface Container { fun containerMember(): A } @@ -199,16 +204,16 @@ interface Container { // intermediate interface (which can contain other methods): MutableList, CompletableDeferred, etc. interface TContainer : Container -// interface with added bound for generic: custom KSerializer which could encapsulate serialization logic for numbers +// interface with bound for type parameter: custom KSerializer which could encapsulate serialization logic for numbers abstract class TNumberBoundContainer : Container -// final implementations with fixed generic +// final implementations with fixed type parameter class NumberContainer : Container object IntContainer : Container object StringContainer : Container ``` -There is no difference when referencing member functions from types with or without type parameters as well as for +There is no difference when referring to member functions from types with or without type parameters as well as for extensions with star-projection: ```kotlin @@ -247,7 +252,8 @@ fun Container<*>.containerExtension() {} fun testContainerExtension() {} ``` -In the case of extensions with fixed generics, we have several unexpected unresolved references, both with K1 and K2: +In the case of extensions with receivers with fixed type parameters, +we have several unexpected unresolved links, both with K1 and K2: ```kotlin // shared declarations, copied, for sample completeness @@ -260,7 +266,7 @@ class NumberContainer : Container object IntContainer : Container object StringContainer : Container -// type parameter is fixed to final class +// type parameter is fixed to the final class fun Container.containerIntExtension() {} /** @@ -273,7 +279,7 @@ fun Container.containerIntExtension() {} */ fun testContainerIntExtension() {} -// type parameter is fixed to abstract class +// type parameter is fixed to an abstract class fun Container.containerFixedNumberExtension() {} /** @@ -286,7 +292,7 @@ fun Container.containerFixedNumberExtension() {} */ fun testContainerFixedNumberExtension() {} -// type parameter is out projection of abstract class +// type parameter is out projection of an abstract class fun Container.containerOutNumberExtension() {} /** @@ -300,13 +306,14 @@ fun Container.containerOutNumberExtension() {} fun testContainerOutNumberExtension() {} ``` -We can’t resolve `TContainer.containerIntExtension` even if `TContainer` just extends `Container` propagating `T` -without any additional constraints. The same is applied for a case with bounded generic (`TNumberBoundContainer`). +We can’t resolve `[TContainer.containerIntExtension]` even if `TContainer` just extends `Container` propagating `T` +without any additional constraints. The same is applied for a case with bounded type parameter +(`TNumberBoundContainer`). Both those cases can be successfully resolved by compiler, and those functions could be called on corresponding receivers. -Initially it was not possible to reference in such a way functions which has generic with bound type arguments (at least -on IDEA side), it was fixed for K1 +Initially it was not possible to refer to functions which has receiver with bound type parameters in a such way +(at least on IDEA side), it was fixed for K1 in [https://youtrack.jetbrains.com/issue/KTIJ-24576](https://youtrack.jetbrains.com/issue/KTIJ-24576). In K2 on the current moment the simplest solution is implemented, where only receiver of extension function is resolved as `Type`: @@ -346,7 +353,7 @@ fun Container.containerGenericBoundExtension() {} fun testContainerGenericBoundExtension() {} ``` -Note: K2 compiler resolves declarations which are unresolved only in K2 when used in code. +Note: K2 compiler resolves declarations which are "UNRESOLVED ONLY in K2" when used in code. More complex scenarios: @@ -371,24 +378,25 @@ fun testModifyComparable() {} ## Proposed solution -**Resolve references for any `Type` which can be used as a receiver in code (resolved by compiler).** +**Resolve `[Type.functionName]` links with any `Type` which can be used as a receiver in code (resolved by compiler).** -References to extensions in a form of `Type.functionName` should be treated in the same way as with member functions, so -it should be possible to reference extensions defined for supertypes. When functions have generics or bounds, those -functions which can be called on `Type` should be resolved. +Links to extensions in a form of `[Type.functionName]` should be treated in the same way as with member functions, so +it should be possible to refer to extensions defined for supertypes. +When functions have type parameters, with or without bounds, those functions which can be called on `Type` should be +resolved. This is conformed to the behavior of current analysis with fixes of some issues with resolve mentioned with `UNEXPECTED` comment in examples (there could be other examples not mentioned in the document), so there are no breaking changes to -how we resolve references but rather an attempt at defining the general rules. +how we resolve links but rather an attempt at defining the general rules. Such resolution logic brings several benefits such as: -1. References to members and extensions are defined in the same way for types and its inheritors; -2. References in KDoc and function calls use the same semantics; -3. If a member is converted to an extension (and vice versa), all KDoc references still will be resolved; +1. Links to members and extensions are defined in the same way for types and its inheritors; +2. Links in KDoc and function calls use the same semantics; +3. If a member is converted to an extension (and vice versa), all KDoc links will be still resolved. -When thinking about type parameters matching in KDoc, it's possible to infer type constraints/requirements to better see -matching rules. +When thinking about type parameters matching in KDoc, it's possible to infer constraints/requirements for them to better +see matching rules. The rules in compiler are more complex than that, this is just a basic idea: * requirements are coming from extensions: @@ -427,9 +435,10 @@ fun testLists() {} There were a few alternatives considered but were rejected because of various reasons: -* Resolve references where `Type` is the same type which is used as receiver in an extension. If receiver is - generic, resolve only `functionName` syntax. Rejected because: - * in case of migration from member to extension or widening of a receiver type, old KDoc references will be broken. +* Resolve links where `Type` is the same type which is used as receiver in an extension. + If receiver is generic, resolve only `functionName` syntax. + Rejected because: + * in the case of migration from member to extension or widening of a receiver type, old KDoc links will be broken. F.e: ```kotlin // version 1 of the lib @@ -454,10 +463,11 @@ There were a few alternatives considered but were rejected because of various re "".doSomething() // still resolved } ``` - * in case of type hierarchies with extensions on supertypes, it's easy to leak an abstraction when referencing - links. F.e in the following example `[HeadersBuilder.append]` will be unresolved, and so we - need to use `[MessageBuilder.append]` which looks and inconsistent and when reading requires more attention, as - you now need to know the hierarchy: + * in the case of type hierarchies with extensions on supertypes, it's easy to leak an abstraction when referring to + both members and extensions or extensions on different super types in the same KDoc block. + F.e in the following example `[HeadersBuilder.append]` will be unresolved, and so we + need to use `[MessageBuilder.append]` which looks inconsistent and when reading requires more attention, as + you now need to know the full hierarchy: ```kotlin interface MessageBuilder { fun build(): String } interface HeadersBuilder: MessageBuilder @@ -471,20 +481,21 @@ There were a few alternatives considered but were rejected because of various re */ fun testHeaders(builder: HeadersBuilder) {} ``` -* Resolve references only by `functionName` and do not resolve `Type.functionName` at all, so treat extension functions - as just functions. Rejected because: +* Resolve links only via `[functionName]` and do not resolve `[Type.functionName]` at all, so treat extension + functions as just functions. + Rejected because: * extensions in Kotlin are first-class entities, and from a call-site perspective they look like regular functions. - So it's not convenient when in KDoc you should reference them a lot differently then in code. - * big breaking change with no clear benefits + So it's not convenient when in KDoc you should refer to them a lot differently than in code. + * major breaking change with no clear benefits ## Other languages * [Java](https://docs.oracle.com/en/java/javase/22/docs/specs/javadoc/doc-comment-spec.html): * No extensions - * Member references are referenced in the same way as in Kotlin (including inheritors) + * The semantics of referring to members are the same as in Kotlin (including inheritors) * [Scala](https://docs.scala-lang.org/overviews/scaladoc/for-library-authors.html) * Extensions are defined similar to Kotlin - * Referencing extensions is possible only by top-level `functionName` without relying on receiver + * Referring to extensions is possible only by top-level `functionName` without relying on receiver ```scala class Point(val x: Int) @@ -501,8 +512,8 @@ def test(): Unit = {} ``` * [Dart](https://dart.dev/language/comments\#documentation-comments) - * Extensions are defined in named `objects` - * Extensions can be referenced only via this named object + * Extensions are defined in named sections + * Referring to extensions is possible only via this named section ```dart class DateTime { @@ -520,7 +531,7 @@ void test () {} * [Rust](https://doc.rust-lang.org/rustdoc/how-to-write-documentation.html) * Same as dart, but extensions are defined in traits - * Extensions can be referenced only via trait + * Referring to extensions is possible only via trait * RustRover supports resolving via the main type while `cargo doc` can’t resolve them ```rust @@ -541,8 +552,8 @@ fn main () {} * [Swift](https://www.swift.org/documentation/docc/) * Extensions are declared in `unnamed` blocks * Generics have different semantics/syntax for structs and protocols - * Referencing extensions is possible in the same way as in Kotlin - * Mostly tries to resolve those references, which can be called in code (resolved by compiler) + * Referring to extensions is possible in the same way as in Kotlin + * Mostly tries to resolve those links, which can be called in code (resolved by compiler) * When there are generics involved results are not always predictable and `Docc` works inconsistently comparing to `XCode` @@ -634,11 +645,13 @@ public protocol Test {} ## Appendix -Some notes regarding references to extension functions, which are nice to know. +Some notes regarding links to extension functions, which are nice to know. -### It’s not possible to use generics or nullable types as `Type` during referencing extensions +### Extensions on receivers with type parameters and nullability -So it’s not possible to distinguish references between such extensions (same issues as with overloading by arguments): +It’s not possible to specify type parameters or nullability in KDoc links. +So it’s not possible to refer to a specific extension which differs only in receiver type parameter or nullability +(similar to overloading by function parameters): ```kotlin /** @@ -665,9 +678,10 @@ fun List.listExtension() {} fun List.listExtension() {} ``` -### If an extension has the same name as a member, it’s possible to reference an extension only via +### Extension and member with the same name -`functionName` syntax +If an extension has the same name as a member, it’s possible to refer to an extension only via `[functionName]`, as +`[Type.functionName]` will always prefer members during link resolution: ```kotlin interface Something { @@ -683,7 +697,8 @@ fun Something.doWork(argument: String) {} fun testSomething() {} ``` -It's possible to overcome this on the user side via import aliases: +It's still possible to distinguish links to member and extensions in a form of `[Type.functionName]` using import +aliases: ```kotlin package org.example @@ -703,12 +718,13 @@ fun Something.doWork(argument: String) {} fun testSomething() {} ``` -### Absolute and relative references +### Absolute and relative links -It’s possible to reference declarations both absolutely (with package and class name) and relatively (based on scope and -imports where declaration is defined). Though, with extension functions referenced by `Type` like `[Type.functionName]` -it’s possible to use absolute reference only for `Type`. While when using `[functionName]` absolute reference is -possible only for `functionName`. +It’s possible to refer to declarations both absolutely (using fully qualified names) and relatively (based on scope and +imports where the link is defined). +Though, with links to extensions with `Type` like `[Type.functionName]` it’s possible to refer via fully qualified name +only for `Type`. +While when using `[functionName]` fully qualified name is possible only for `functionName`. ```kotlin package com.example @@ -773,21 +789,21 @@ import kotlin.time.Duration.Companion.seconds fun testDuration() {} ``` -Rules for the resolve of references could be described like this: +Rules for the link resolution could be described like this: * `[FUNCTION]` / `[PROPERTY]` / `[CLASS]` - * relative references to **top-level** declarations (including extensions) - * relative references to **imported** declarations (f.e from `companion object`) -* `[PACKAGE.FUNCTION]` / `[PACKAGE.PROPERTY]` / `[PACKAGE.CLASS]` - absolute references to top level declarations + * relative links to **top-level** declarations (including extensions) + * relative links to **imported** declarations (f.e from `companion object`) +* `[PACKAGE.FUNCTION]` / `[PACKAGE.PROPERTY]` / `[PACKAGE.CLASS]` - absolute links to top level declarations (including extensions) -* `[CLASS.CLASS]` - relative references to inner classes -* `[PACKAGE.CLASS.CLASS]` - absolute references to inner classes +* `[CLASS.CLASS]` - relative links to inner classes +* `[PACKAGE.CLASS.CLASS]` - absolute links to inner classes * `[CLASS.FUNCTION]` / `[CLASS.PROPERTY]`: - * relative references to **members** defined in CLASS (or its inheritors) - * relative references to **extensions** defined for CLASS (or its inheritors) + * relative links to **members** defined in CLASS (or its inheritors) + * relative links to **extensions** defined for CLASS (or its inheritors) * `[PACKAGE.CLASS.FUNCTION]` / `[PACKAGE.CLASS.PROPERTY]`: - * absolute references to **members** defined in PACKAGE.CLASS (or its inheritors) - * **partially** absolute references to **extensions** defined for PACKAGE.CLASS (or its inheritors). + * absolute links to **members** defined in PACKAGE.CLASS (or its inheritors) + * **partially** absolute links to **extensions** defined for PACKAGE.CLASS (or its inheritors). Partially means that we can specify PACKAGE for CLASS, but if FUNCTION is from different package, this package should be imported @@ -795,17 +811,17 @@ Rules for the resolve of references could be described like this: With the introduction of [Context parameters](https://github.com/Kotlin/KEEP/blob/context-parameters/proposals/context-parameters.md) in case -we do nothing, the situation will not change and `context` will not affect how references will be resolved. If we treat -`context parameters` (at least initially) as just other function parameters then it falls into the “function parameters -overload” problem mentioned in the begging of the document and will be discussed separately. +we do nothing, the situation will not change and `context` will not affect how links will be resolved. +If we treat `context parameters` (at least initially) as just other function parameters then it falls into the “function +parameters overload” problem mentioned in the begging of the document and will be discussed separately. ```kotlin fun function() {} -fun String.extension() {} +fun Scope.extension() {} context(scope: Scope) fun contextFunction() {} -context(scope: Scope, scope2: String) fun contextFunction() {} -context(scope: String) fun contextFunction() {} +context(scope: Scope, otherScope: OtherScope) fun contextFunction() {} +context(otherScope: OtherScope) fun contextFunction() {} context(scope: Scope) fun String.contextExtension() {} @@ -819,7 +835,7 @@ context(scope: Scope) fun String.contextExtension() {} * [contextExtension] - resolved * [String.contextExtension] - resolved * - * With the possibility of overload by context parameters + * With an imaginary syntax of overload by context parameters * [(Scope) contextFunction] * [(Scope) String.contextExtension] * or @@ -829,7 +845,7 @@ context(scope: Scope) fun String.contextExtension() {} fun testContext() {} ``` -Even if we introduce the possibility of overload by context parameters, most likely it will not affect how references to -extension functions will be resolved. Context parameters just add a layer of reference overloads additionally -to extension functions. It’s out of scope of this document and should be discussed in the scope of support for -referencing specific overloads. +Even if we introduce the possibility of overload by context parameters, most likely it will not affect how links to +extensions will be resolved. +Context parameters are just an additional set of parameters to perform overload additionally to functions parameters. +It’s out of scope of this document and should be discussed in the scope of support for referring to specific overloads. From a18d10c14eeecf379e5d91b0951fe43fc2cf3dae Mon Sep 17 00:00:00 2001 From: Oleg Yukhnevich Date: Mon, 12 Aug 2024 18:22:22 +0300 Subject: [PATCH 4/6] Provide a better concept of expected KDoc link resolution rules --- proposals/kdoc/links-to-extensions.md | 165 +++++++++++++++++++------- 1 file changed, 122 insertions(+), 43 deletions(-) diff --git a/proposals/kdoc/links-to-extensions.md b/proposals/kdoc/links-to-extensions.md index 002324d53..e4fcee869 100644 --- a/proposals/kdoc/links-to-extensions.md +++ b/proposals/kdoc/links-to-extensions.md @@ -21,19 +21,19 @@ The goal of this document is to try to describe consistent rules on how KDoc lin ```kotlin /** - * resolved with both K1 and K2 analysis (all extensions are defined on [Iterable]) - * - [Iterable.map] - * - [Iterable.flatMap] - * - [Iterable.flatten] - * - [Iterable.min] - * resolved with K1, unresolved with K2 analysis ([List] implements [Iterable]) + * resolved with both K1 and K2 analysis + * - [Iterable.map] (fun Iterable.map) + * - [Iterable.flatMap] (fun Iterable.flatMap) + * - [Iterable.flatten] (fun Iterable>.flatten) + * - [Iterable.min] (fun > Iterable.min) + * resolved with K1, UNRESOLVED with K2 analysis * - [List.map] * - [List.flatMap] - * unresolved with both K1 and K2 analysis - * - [List.flatten] (defined as `fun Iterable>.flatten()`) - * - [List.min] (defined as `fun > Iterable.min()`) + * UNRESOLVED with both K1 and K2 analysis + * - [List.flatten] + * - [List.min] */ -fun testLists() {} +fun test() {} ``` ## Overview @@ -45,9 +45,6 @@ To refer to an extension function/property, it's possible to use one of the foll * `[functionName]` - like with top-level functions or members in class scope * `[Type.functionName]` where `Type` is a receiver - like with member functions outside class scope -> Note: the behaviour for links to extension properties is the same as for links to extension functions and while a -> document will mostly use `function` in text, it could be treated as `function/property`. - ```kotlin fun String.extension() {} @@ -58,6 +55,9 @@ fun String.extension() {} fun testStringExtension() {} ``` +> Note: the behaviour for links to extension properties is the same as for links to extension functions and while a +> document will mostly use `function` in text, it could be treated as `function/property`. + In case there are multiple extensions with the same name in the same package but different receiver, it's possible to refer to a specific extension via `[Type.functionName]` (this is a specific case of links to overloaded declarations: [KT-15984](https://youtrack.jetbrains.com/issue/KT-15984/Quick-documentation-Kdoc-doesnt-support-specifying-a-particular-overloaded-function-or-variable-in-a-link), @@ -395,40 +395,119 @@ Such resolution logic brings several benefits such as: 2. Links in KDoc and function calls use the same semantics; 3. If a member is converted to an extension (and vice versa), all KDoc links will be still resolved. -When thinking about type parameters matching in KDoc, it's possible to infer constraints/requirements for them to better -see matching rules. -The rules in compiler are more complex than that, this is just a basic idea: - -* requirements are coming from extensions: - * for `fun List.something()` - generic type requirement will be `out Any?` - * for `fun List.somethingNumber()` - generic type requirement will be `out Number` -* constraints are coming from types: - * for `interface List` - generic type constraint will be `out Any?` - * for `interface Container : List` - generic type constraint will be `out Any?` - * for `interface NContainer : List` - generic type constraint will be `out Number` - * for `interface NNContainer : List` - generic type constraint will be `out Number` - * for `interface SContainer : List` - generic type constraint will be `out String` - * for `interface NoVariance` - generic type constraint will be still `out Any?` as any type is possible - * for `interface NNoVariance : NoVariance` - generic type constraint will be `Number` (no `out` variance) -* Matching: - * `something` should match any subtype of `List` as it's a type constraint is `out Any?` - * `somethingNumber` should match only when `Type` is subtype which have matching constraint `out Number`. - So f.e `SContainer` is not available. - -Coming back to the first example, with this proposal all links should be resolved: +As a general rule, KDoc links to extensions in a form of `[Type.functionName]` should be resolved in all cases +when it's possible to write a [callable reference](https://kotlinlang.org/docs/reflection.html#callable-references) +in a form of `Type::functionName`. If `Type` has type parameters, e.g `interface Container>`, +then the link should be resolved if it's possibly to substitute them with some concrete types in callable reference, +e.g `Something>`, so that `functionName` is resolved. + +Here are two examples to illustrate this behaviour: + +1. Simple case, no variance: + ```kotlin + interface NoVariance + interface NumberNoVariance : NoVariance + + fun NoVariance.extension() {} + + /** + * [NoVariance.extension] - should be resolved + * [NumberNoVariance.extension] - should be resolved + */ + fun testExtension() { + NoVariance::extension // resolved + NoVariance::extension // resolved + NoVariance::extension // resolved + + NumberNoVariance::extension // resolved + } + + fun NoVariance.stringExtension() {} + + /** + * [NoVariance.stringExtension] - should be resolved + * [NumberNoVariance.stringExtension] - should be UNRESOLVED + */ + fun testStringExtension() { + NoVariance::stringExtension // UNRESOLVED: compiler produces 'Unresolved reference' error + NoVariance::stringExtension // UNRESOLVED: compiler produces 'Unresolved reference' error + NoVariance::stringExtension // resolved + + NumberNoVariance::stringExtension // UNRESOLVED: compiler produces 'Unresolved reference' error + } + ``` + +2. Lists and iterables from the initial example: + ```kotlin + // definitions of interfaces and functions from kotlin-stdlib for example completness + + interface Iterable + interface Collection : Iterable + interface List : Collection + + fun Iterable.map(transform: (T) -> R): List = TODO() + fun Iterable.flatMap(transform: (T) -> Iterable): List = TODO() + fun Iterable>.flatten(): List = TODO() + fun > Iterable.min(): T? = TODO() + + /** + * all should be resolved + * - [Iterable.map] + * - [Iterable.flatMap] + * - [Iterable.flatten] + * - [Iterable.min] + * - [List.map] + * - [List.flatMap] + * - [List.flatten] + * - [List.min] + */ + fun test() { + Iterable::map // resolved + Iterable::flatMap // resolved + Iterable>::flatten // resolved + Iterable>>::min // resolved + + List::map // resolved + List::flatMap // resolved + List>::flatten // resolved + List>>::min // resolved + } + ``` + +> Note: technically speaking, callable references to `map` and `flatMap` in this case will not compile. +> +> There will be an error from the compiler: Not enough information to infer type variable R. +> That's correct, as type parameters of functions cannot be specified in a generic callable reference and could be +> inferred only based on an expected type. +> +> Still, it doesn't affect call resolution, as in such cases those type parameters are not used in receiver, +> and so the compiler resolves the callable reference correctly: there is no 'Unresolved reference' error produced. +> This means that declarations should be resolved also in KDoc links. + +All of the above could be described like this: + +**KDoc link to an extension in a form of `[Type.functionName]` should be resolved +if and only if call `typeInstance.functionName(_)` is resolved by compiler where `typeInstance` is of type `Type<_>`:** + +> Note: `_` here are just placeholders (a.k.a. typed hole) ```kotlin /** - * - [Iterable.map] - * - [Iterable.flatMap] - * - [Iterable.flatten] - * - [Iterable.min] - * - [List.map] - * - [List.flatMap] - * - [List.flatten] (defined as `fun Iterable>.flatten()`) - * - [List.min] (defined as `fun > Iterable.min()`) + * - [Iterable.map] - should be resolved if `iterable.map(_)` is resolved + * - [Iterable.flatMap] - should be resolved if `iterable.flatMap(_)` is resolved + * - [List.map] - should be resolved if `list.map(_)` is resolved + * - [List.flatMap] - should be resolved if `list.flatMap(_)` is resolved */ -fun testLists() {} +fun testExplicit( + iterable: Iterable, + list: List +) { + iterable.map {} // resolved + iterable.flatMap { listOf(0) } // resolved + + list.map {} // resolved + list.flatMap { listOf(0) } // resolved +} ``` ## Alternatives considered From 10820b9fc714004f39bad7c9140d80a7773333fe Mon Sep 17 00:00:00 2001 From: Oleg Yukhnevich Date: Mon, 12 Aug 2024 18:26:05 +0300 Subject: [PATCH 5/6] Replace `functionName` with `extensionName` in KDoc links for clarity --- proposals/kdoc/links-to-extensions.md | 40 +++++++++++++-------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/proposals/kdoc/links-to-extensions.md b/proposals/kdoc/links-to-extensions.md index e4fcee869..e6ce509ed 100644 --- a/proposals/kdoc/links-to-extensions.md +++ b/proposals/kdoc/links-to-extensions.md @@ -42,8 +42,8 @@ fun test() {} To refer to an extension function/property, it's possible to use one of the following syntax constructs: -* `[functionName]` - like with top-level functions or members in class scope -* `[Type.functionName]` where `Type` is a receiver - like with member functions outside class scope +* `[extensionName]` - like with top-level functions or members in class scope +* `[Type.extensionName]` where `Type` is a receiver - like with member functions outside class scope ```kotlin fun String.extension() {} @@ -59,12 +59,12 @@ fun testStringExtension() {} > document will mostly use `function` in text, it could be treated as `function/property`. In case there are multiple extensions with the same name in the same package but different receiver, it's possible to -refer to a specific extension via `[Type.functionName]` (this is a specific case of links to overloaded declarations: +refer to a specific extension via `[Type.extensionName]` (this is a specific case of links to overloaded declarations: [KT-15984](https://youtrack.jetbrains.com/issue/KT-15984/Quick-documentation-Kdoc-doesnt-support-specifying-a-particular-overloaded-function-or-variable-in-a-link), [dokka/80](https://github.com/Kotlin/dokka/issues/80)). Additionally, it could be used to represent a link in a KDoc more naturally, so that the links to extensions will look in the same way as links to members. -Support for resolution of links to extensions functions via `[Type.functionName]` was tracked in scope +Support for resolution of links to extensions functions via `[Type.extensionName]` was tracked in scope of [KT-13299](https://youtrack.jetbrains.com/issue/KT-13299) (most likely). Simple example for such an ambiguity is from kotlinx-coroutines `CoroutineScope.isActive` and @@ -84,7 +84,7 @@ fun testCoroutines() {} ### Multiple ways to refer to the same declaration -Links to member functions outside class scope are resolved only via `[Type.functionName]` where `Type` could be both +Links to member functions outside class scope are resolved only via `[Type.extensionName]` where `Type` could be both type where the function is declared, and all it’s inheritors: ```kotlin @@ -378,9 +378,9 @@ fun testModifyComparable() {} ## Proposed solution -**Resolve `[Type.functionName]` links with any `Type` which can be used as a receiver in code (resolved by compiler).** +**Resolve `[Type.extensionName]` links with any `Type` which can be used as a receiver in code (resolved by compiler).** -Links to extensions in a form of `[Type.functionName]` should be treated in the same way as with member functions, so +Links to extensions in a form of `[Type.extensionName]` should be treated in the same way as with member functions, so it should be possible to refer to extensions defined for supertypes. When functions have type parameters, with or without bounds, those functions which can be called on `Type` should be resolved. @@ -395,11 +395,11 @@ Such resolution logic brings several benefits such as: 2. Links in KDoc and function calls use the same semantics; 3. If a member is converted to an extension (and vice versa), all KDoc links will be still resolved. -As a general rule, KDoc links to extensions in a form of `[Type.functionName]` should be resolved in all cases +As a general rule, KDoc links to extensions in a form of `[Type.extensionName]` should be resolved in all cases when it's possible to write a [callable reference](https://kotlinlang.org/docs/reflection.html#callable-references) -in a form of `Type::functionName`. If `Type` has type parameters, e.g `interface Container>`, +in a form of `Type::extensionName`. If `Type` has type parameters, e.g `interface Container>`, then the link should be resolved if it's possibly to substitute them with some concrete types in callable reference, -e.g `Something>`, so that `functionName` is resolved. +e.g `Something>`, so that `extensionName` is resolved. Here are two examples to illustrate this behaviour: @@ -486,8 +486,8 @@ Here are two examples to illustrate this behaviour: All of the above could be described like this: -**KDoc link to an extension in a form of `[Type.functionName]` should be resolved -if and only if call `typeInstance.functionName(_)` is resolved by compiler where `typeInstance` is of type `Type<_>`:** +**KDoc link to an extension in a form of `[Type.extensionName]` should be resolved +if and only if call `typeInstance.extensionName(_)` is resolved by compiler where `typeInstance` is of type `Type<_>`:** > Note: `_` here are just placeholders (a.k.a. typed hole) @@ -515,7 +515,7 @@ fun testExplicit( There were a few alternatives considered but were rejected because of various reasons: * Resolve links where `Type` is the same type which is used as receiver in an extension. - If receiver is generic, resolve only `functionName` syntax. + If receiver is generic, resolve only `[extensionName]` syntax. Rejected because: * in the case of migration from member to extension or widening of a receiver type, old KDoc links will be broken. F.e: @@ -560,7 +560,7 @@ There were a few alternatives considered but were rejected because of various re */ fun testHeaders(builder: HeadersBuilder) {} ``` -* Resolve links only via `[functionName]` and do not resolve `[Type.functionName]` at all, so treat extension +* Resolve links only via `[extensionName]` and do not resolve `[Type.extensionName]` at all, so treat extension functions as just functions. Rejected because: * extensions in Kotlin are first-class entities, and from a call-site perspective they look like regular functions. @@ -574,7 +574,7 @@ There were a few alternatives considered but were rejected because of various re * The semantics of referring to members are the same as in Kotlin (including inheritors) * [Scala](https://docs.scala-lang.org/overviews/scaladoc/for-library-authors.html) * Extensions are defined similar to Kotlin - * Referring to extensions is possible only by top-level `functionName` without relying on receiver + * Referring to extensions is possible only by top-level `[[extensionName]]` without relying on receiver ```scala class Point(val x: Int) @@ -759,8 +759,8 @@ fun List.listExtension() {} ### Extension and member with the same name -If an extension has the same name as a member, it’s possible to refer to an extension only via `[functionName]`, as -`[Type.functionName]` will always prefer members during link resolution: +If an extension has the same name as a member, it’s possible to refer to an extension only via `[extensionName]`, as +`[Type.extensionName]` will always prefer members during link resolution: ```kotlin interface Something { @@ -776,7 +776,7 @@ fun Something.doWork(argument: String) {} fun testSomething() {} ``` -It's still possible to distinguish links to member and extensions in a form of `[Type.functionName]` using import +It's still possible to distinguish links to member and extensions in a form of `[Type.extensionName]` using import aliases: ```kotlin @@ -801,9 +801,9 @@ fun testSomething() {} It’s possible to refer to declarations both absolutely (using fully qualified names) and relatively (based on scope and imports where the link is defined). -Though, with links to extensions with `Type` like `[Type.functionName]` it’s possible to refer via fully qualified name +Though, with links to extensions with `Type` like `[Type.extensionName]` it’s possible to refer via fully qualified name only for `Type`. -While when using `[functionName]` fully qualified name is possible only for `functionName`. +While when using `[extensionName]` fully qualified name is possible only for `extensionName`. ```kotlin package com.example From c3c5a9c961bf4e7f1369952ba21f9f454182eab0 Mon Sep 17 00:00:00 2001 From: Oleg Yukhnevich Date: Mon, 12 Aug 2024 18:45:13 +0300 Subject: [PATCH 6/6] Add link to discussion --- proposals/kdoc/links-to-extensions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/kdoc/links-to-extensions.md b/proposals/kdoc/links-to-extensions.md index e6ce509ed..e0803c5d5 100644 --- a/proposals/kdoc/links-to-extensions.md +++ b/proposals/kdoc/links-to-extensions.md @@ -4,7 +4,7 @@ * **Author**: Oleg Yukhnevich * **Status**: Submitted * **Target issue**: [dokka/3555](https://github.com/Kotlin/dokka/issues/3555) -* **Discussion**: TBD +* **Discussion**: [KEEP-385](https://github.com/Kotlin/KEEP/issues/385) ## Summary