Skip to content

Adjust the KEEP according to Dokka / Analysis API design discussions #443

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

BlaBlaHuman
Copy link

No description provided.

@BlaBlaHuman
Copy link
Author

Hi, please pay close attention to all the examples and details in the tag-sections chapter. I tried to gather all the information on them I could from our discussions and from the original KEEP, however, I might have misunderstood something.

And I'm still not quite sure about the details of this multi-resolution. In the KEEP I mentioned that we should return all the symbols the compiler could resolve this reference to (i.e. all symbols from some scope), not just symbols of the same kind. It seems like a valid and requested feature, and there won't be too many symbols with the same name in the same scope anyways. It's probably more important than just handling overloads by returning multiple symbols of the same kind.

/**
 * @see Foo - now it's able to point to the function
 */
interface Foo 

fun Foo(): Foo = object : Foo {}

@nikitabobko
Copy link
Member

BlaBlaHuman wants to merge 1 commit into kdoc/Streamline-KDoc-ambiguity-references from kdoc/omak/Streamline-KDoc-ambiguity-references

The proposal is already merged to the main branch https://github.com/Kotlin/KEEP/blob/main/proposals/KEEP-0389-kdoc-streamline-KDoc-ambiguity-references.md, so please update the proposal in the main branch, avoid merging non-main branches into each other

@BlaBlaHuman BlaBlaHuman changed the base branch from kdoc/Streamline-KDoc-ambiguity-references to main July 15, 2025 10:21
@BlaBlaHuman BlaBlaHuman force-pushed the kdoc/omak/Streamline-KDoc-ambiguity-references branch from d6a1d6f to 9a3f9b8 Compare July 15, 2025 10:25
@@ -1,24 +1,29 @@
# Streamline KDoc ambiguity links

* **Type**: KDoc proposal
* **Type**: Proposal
* **Author**: Vadim Mishenev
* **Status**: Submitted
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you also please update the status according to https://github.com/Kotlin/KEEP?tab=readme-ov-file#keep-document-lifecycle

Copy link

@marcopennekamp marcopennekamp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the comprehensive update! 🙌

I left a lot of comments in the review, but I think my main points of contention are:

  1. We need clear definitions for the key concepts, such as the "KDoc context", the exact steps of the resolution algorithm, and generally just more precise formulations for a lot of things.
  2. I think the resolution algorithm could be described more clearly by mentioning the exact layers (self-links, scopes, global references, packages), essentially rearranging some sections a bit to arrive at a single, well-described algorithm.
  3. We need to discuss property resolution and whether (1) properties should be resolved as first segments during multi-segment resolution and (2) legally accessing a property (e.g. [name.length]) in KDoc references is desired in the future.

And this definitely needs another round of copyediting, but let's get the specifics right first. 👍

Comment on lines 23 to +24
- Fully qualified ones, for example `[com.example.classA]`, starting with a full package name
- Relative ones (also known as short names), for example `[memberProperty]`
- Relative ones (also known as short names), for example `[memberProperty]` / `[classA.myObject.method]`.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should be idiomatic here and say ClassA instead of classA?

which both K1 and K2 handle using the same mechanisms and rules to deal with them.

### Self-links
Javadoc and KDoc allow having links to the elements from the context declaration, it is a quite popular practice.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Javadoc and KDoc allow having links to the elements from the context declaration, it is a quite popular practice.
Javadoc and KDoc allow elements to link to themselves with priority. It is a popular practice.

"allow having links to the elements from the context declaration" sounds too imprecise/confusing to me. Also I'd add that the point of a "self link" is the inherent priority it brings, as otherwise it would just be a "link".

* For the property, a fully qualified link can be used.
*/
fun f(abc: String) = 0
* [foo], [x], [T] - self-links, point to symbols from the context declaration,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like the term "context" is a bit too out of left field. How about "documented", i.e. "the documented declaration"? Or "the declaration with the KDoc comment"?

If you want to stick with "context", I'd define it somewhere.

and proceeds to global scopes (package scope, explicit import scopes, star import scopes, scopes of other packages).
The result of such an approach is a single most local symbol of the highest priority kind possible.

This strategy will be then referred to as the **declaration-kind-first approach**.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we could shorten "declaration-kind-first approach" to just "kind-first approach". It should be equally valid and is easier to read.

Comment on lines +280 to +282
K1 implementation
failing to determine what declarations the current context of KDoc contains leads to unresolved references.
However, inherited members are still seen and resolved.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some words missing? Hard to read anyway.

which name is a segment prefix of this link (search for `A.B.C.D`, then for `A.B.C` and so on).
Let's imagine that some package with `A.B` name is found.
The only thing left to do is to resolve `C.D.foo` inside this package scope.
Now it's quite similar to the logic we had for relative and short names.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In fact, it should be the same algorithm and we can formalize that.

So in the relative case, we have "find first segment" and then "apply relative access algorithm to it".

And here we have "find classifier by longest existing package" and then "apply relative access algorithm to it".

Just needs a better name than "relative access algorithm".

Comment on lines +883 to +886
* Before performing a scope transformation for relative names, the resolver has to make sure that there are no
non-function declarations matching some prefix of the given name in a more local scope.
If such a declaration is found in some scope, then there is no need to process all further scopes,
as the compiler would resolve this prefix to the declaration in this scope.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the wording is quite roundabout here. For example, "prefix" can suggest a lot of incorrect things, like a prefix of the string name, or more than the first segment (a multi-segment prefix of a multi-segment name).

I would specifically concentrate in the definitions in this whole section on "resolving the first segment" and whenever that resolves to a non-function, we have deterministically picked the scope that we want to apply relative resolution to.

And then we don't need any special wording for "if a conflicting declaration is found in a local scope, the resolver should not proceed to further scopes/global retrieval", because the outlined algorithm clearly stops where the first segment is resolved to a non-function.

Comment on lines +899 to +900
The result of the resolution is the set of all symbols found in self-links (for short names)
plus symbols from the first non-empty scope sorted by their priorities.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't self-links just be the highest priority "source" in the algorithm and thus if we find a self-link, we can skip scope-based resolution?

It's important to preserve the priority of symbols in the resulting collection for purposes
specified in [Handling resolved symbols on the use-site](#handling-resolved-symbols-on-the-use-site).

## Handling resolved symbols on the use-site

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure we need this section here. It's enough to say that the described KDoc resolution algorithm can result in multiple candidate symbols, and that it's up to a tool implementation to pick the appropriate one(s).

I'm actually not sure how much we want to cover IDE features in a KEEP.

@nikitabobko What do you think?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I'm too much out-of-context for this proposal.

In general, it's ok to mention IDE and tooling integration in the language proposal. Sometimes the way the feature is toolable affects the design

## Visibility
KDoc ignores visibility, i.e. all declarations are public for KDoc references.
KDoc ignores visibility, i.e., all declarations are public for KDoc references.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO, this should be part of the main resolution schema description, rather than part of an algorithm. (I think it's mentioned in passing somewhere above, but perhaps it could be stated more definitely.)

Copy link
Contributor

@vmishenev vmishenev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the additions and proofreading 👍

I have not properly checked the 'Implementation details' section, as I am not familiar with the implementation, although the described logic seems correct.

I would also like to see the final version of KEEP after editing.

@@ -1,24 +1,29 @@
# Streamline KDoc ambiguity links

* **Type**: KDoc proposal
* **Type**: Proposal
* **Author**: Vadim Mishenev
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you could put your name as well.


## Problem
An ambiguity in KDoc name resolution is a case when there are multiple symbols available for the same KDoc reference.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Symbol" is a term used in the Kotlin Analysis API (AA) that may be confusing for users unfamiliar with AA.
According to this documentation, could we replace it with something like declarations?

Also, we have internally agreed to use link instead of reference, as it is more intuitive for users.

## Global declarations breaking the local resolution

The main concern is the order of priorities in the **declaration-kind-first** approach.
With the current approach, global context can easily break the local resolution.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is implemented only in K1, isn't it?

- Scope of origin

There are four main kinds of declarations (from higher priority to lower):
- Classifier
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: classifier could also be a type parameter (e.g. in the reflection - https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.reflect/-k-classifier/),
so I used just a class.

The tag `@constructor` has context where parameters of a primary constructor are available.
#### @constructor section

The tag `@constructor` can be applied to classes and secondary constructors
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m not sure if it should be applied to a secondary constructor. At least, the existing documentation only mentions primary constructors.

Comment on lines +591 to +593
The compiler also doesn't have a strict order of declaration kinds,
as it has more information about a required symbol from the use-site
(not just name as KDoc does).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. The compiler generally already knows what kinds of declaration it is looking for based on the syntactic context, inferred types e.t.c., whereas KDoc does not.

However, the priorities are switched: locality of scopes has a higher priority than declaration kinds.

Do you mean the opposite — that declaration kinds have a higher priority? or not?
Otherwise, I do not understand what the examples below are showing

In your example,

```kotlin
val foo = 9

fun main() {
    class foo()

    foo // variable access
}

Apparently, the kind of foo is prioritized over the scope.

Maybe, the following is more representative:

fun foo() = 0
class B {
class foo()
  class A {
      val foo = { 0 }
  
      fun usage() {
          foo()
      }
  }
}

where the expression foo() can refer to a class, a function or a property depends on the scope order, regardless of the declaration kind.


So the general priority order can be described as following:
- Self-links (symbols from the context declaration)
- Classifier (from local to global)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Compared to K1, the priorities have changed, but there is no clear reasoning behind why the KEEP prioritizes a class over packages.

see the reasoning behind this in the [Restrictions of multi-segment names resolution](#restrictions-of-multi-segment-names-resolution).

Imagine that we have a name `A.B.foo`.
We assume that this link is relative, so in each scope we have to find a class-like (i.e., a declaration container) `A`,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

class-like or classifier?


Unfortunately, there is no disambiguation syntax
that would allow explicitly specifying the kind of the declaration the current link should point to.
To overcome this issue at the current moment,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although this is not only to overcome that issue, but also for handling overloads

Copy link
Contributor

@whyoleg whyoleg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! Great work!
I've left mostly some questions regarding some of the examples, as the other parts already have a lot of good feedback!

class A(a: Int) {

/**
* [A] - to the current constructor, [a] - unresolved
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would expect [a] to be resolved here to the parameter a, in the same way as if it was a parameter of a function.


/**
* [A] - to the current constructor, [a] - unresolved
* @constructor [A] - to the current constructor, [a] - to the current parameter
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@constructor tag does only makes sense when documenting class. Yes, we don't currently have a specification about which tag could be used on which declaration, but we can at least not have such example here


A small summary example:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This example made me think, that probably we need to somehow improve examples/wording for @param and @property, based on the fact, that the first link after the tag, should always refer to a parameter or to property respectively: https://kotlinlang.org/docs/kotlin-doc.html#param-name (+ may be about ability to omit square braces for this reference)

In this example real documentation written in this KDoc will be:

  • for constructor - [abc] - to the constructor
  • for parameter - - to the parameter
  • for property - - to the property

So @param and @property sections does introduce new context for the description part of the tag, like @constructor, yes, but the first link is special and it should not even resolve anything else apart from parameters or properties.
JIC, currently IDEA does correctly resolves (and allows to follow the link) something like @param ClassA description for param - while in reality it should not.

Comment on lines +757 to +759
We assume that this link is relative, so in each scope we have to find a class-like (i.e., a declaration container) `A`,
then if `A` is found, we should start looking for class-like `B` in its class scope and so on.
Then the rest of the logic is quite simple: retrieve symbols from this scope by the short name (`foo`).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Btw, I've hit that problem a few times, and I wonder why don't we allow it?

I haven't even found an existing YT/Dokka issue for supporting [name.length], so you can create one. I believe it's out of scope here, as this will be a whole new feature for KDoc, IMO.

It's similar to Kotlin/dokka#3756 in some way, but even in #3756, we do have a chain of types that ends with a function or property.
But, if we allow name.length, what stops us from allowing something like String.length.toString or any other chain, where we have variables in the middle? Or maybe we also need to allow functions and use their return value, like List.map.size? :)

I mean, [name.length] might be convenient, but it might introduce unnecessary complexity in understanding those links or inconsistency if we provide this only for parameters/variables.

val A = 5

fun usage() {
A.B.C() // UNRESOLVED
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you help me to understand, why we don't resolve this link, but resolve the link above, with fun A()? (and the same for example with receiver below)

Comment on lines +761 to +769
The resolver should iterate through scopes from local to global
and look for a chain of nested classifiers starting in the current scope.
If this chain is found,
the resolver just picks this classifier member scope and looks for symbols with the given short name.
However, if, at some point of this symbols-by-segment search,
the resolver comes across some non-function symbol from which
it's impossible to continue the chain, the resolver should stop and consider this link as unresolved.
Examples of such cases are classes with no nested classifiers matching the next segment and properties.
Again, see [following chapter](#restrictions-of-multi-segment-names-resolution) for more info.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can I ask a bit of an unrelated question?
Why did the question about properties (e.g., [name.length]) even appear in the discussion? I mean, this isn't a supported KDoc syntax, and it is not mentioned in the document (as far as I see).
I mean, maybe I'm missing something obvious, or compiler-related knowledge, but for me, this feels like a whole new feature for KDoc, similar to overloads, and so should be discussed totally separately.

The problem of ambiguous KDoc links can be solved by tooling (Dokka and IDE).
Dokka can show all possible candidates *via a popup with an interactive list* in the same way as IDE does it for ambiguous resolving in Javadoc etc.
As we can see, the compiler only resolves this name when there are
no other non-function more local declarations that are resolvable by some prefix of the given name.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similar to questions above: why non-function and not just class-like?


```kotlin
/**
* @see Foo() function. - leads to the interface,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: the correct kdoc link syntax for @see should be Foo without parameters. At least currently, Foo() can be resolved incorrectly: Kotlin/dokka#3629
It's better to replace the @see Foo() with [Foo] in this example.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants