Repository navigation
fix(kotlin): resolve calls through typed class properties (#1555) - #2682
Merged
Merged
Conversation
The cross-file Kotlin walk resolved a call on a typed method parameter by type, but a call on a class property - a primary-constructor val, a body property or `this.prop` - fell to the name guess. The cross registry held each own-file class from its extraction defs, whose properties carry no type (constructor vals none at all), and the walk's typed AST registration of the same class was added behind that entry under the same QN, so every lookup returned the field-less one. Constructor injection is the common Kotlin dependency shape, so `repository.inserir(...)` on an injected port bound to whichever same-named method the guess found, including an implementation of another port (#1555). The walk now registers the file's own declarations into an overlay chained to the cross registry and checked first, indexed once the top-level declarations are collected. The sole-definer fallback walks the whole chain so its candidate set is unchanged. Typing more receivers exposed resolution gaps that the untyped receivers had hidden; each would otherwise turn a typed call into a wrong name guess: - a member of the receiver class wins over a same-named extension of the same file (Kotlin resolution order; the overlay holds both); - `x.hashCode()` / `toString()` / `equals()` on a receiver typed by another file: the per-file pass emits the kotlin.Any placeholder, the cross pass the declared override, and two targets at one occurrence are ambiguous to the call join. The per-file placeholder is dropped when the cross pass resolves the same occurrence; - the same members on a stdlib or JDK type (`count.toString()` on an Int) resolve to the kotlin.Any builtin instead of a seeded stdlib member that has no graph node; - a property READ on a typed receiver whose QN the graph spells differently (a nested class's property) never becomes a CALL_REFERENCE to a same-named function: the reference join falls back by name to non-callable targets only, which binds the property's own node; - a typed value without an `invoke` operator (`private val handshake: Handshake?`) no longer shadows the same-named member function; - inside a lambda whose typed receiver becomes `this` (`repo.let { .. }`), and inside a member extension, the enclosing class's members stay callable bare (the implicit-receiver chain); a call naming a class stays a constructor call. Fixes #1555 Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
… up the Smart CI selection fix #2684) Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Kotlin member calls through a class property — a primary-constructor
val, a body property, orthis.prop— now bind to the property's declared type, on both the sequential and the parallel resolver. Fixes #1555.Why it was wrong
The cross-file Kotlin walk resolved a call on a typed method parameter by type, but a call on a class property fell to the name guess. The cross registry held each class of the current file from its extraction defs, and their properties carry no type (constructor vals none at all). The walk's typed AST registration of the same class was added behind that entry under the same QN, so every lookup returned the field-less one. Constructor injection is the common Kotlin dependency shape, so
repository.inserir(...)on an injected port was bound to whichever same-named method the guess found, including an implementation of another port.The walk now registers the file's own declarations into an overlay that is chained to the cross registry and checked first. The overlay is indexed once the top-level declarations are collected. The sole-definer fallback walks the whole chain, so its candidate set is unchanged.
Typing many more receivers exposed resolution gaps that untyped receivers had hidden. Each would otherwise have turned a newly typed call into a wrong name guess, so each is handled in this PR:
x.hashCode()on a receiver typed by another filecount.toString()onInt/String/ JDK typesprivate val handshake: Handshake?next tooverride fun handshake()invokeyields to the member functionrepo.let { … }or a member extensionEvidence
Measured on 14 open-source Kotlin repositories (base = current main), indexed with and without the change. The changed edges were hand-judged against the source in a stratified sample: 20 per strategy stratum, 366 edges.
Per edge type (parallel resolver, all 14 repositories):
Another 9,817 CALLS keep their target and are now resolved by the Kotlin resolver instead of a name guess.
CALLS per repository (estimates from the judged strata):
Judged strata (op × strategy; C = correct, W = wrong, U = undecidable from the source):
==on a project type that declares noequalsno longer gets an edge to the kotlin.Any.equals builtin (24 edges; an external target, no project edge involved).raise()in member extensions, apolloconnectToAndroidComponentin a component lambda).Tests
tests/test_kotlin_typed_calls.cruns one fixture through both resolvers (CBM_INDEX_SINGLE_THREAD=1and the parallel path). Every shape in the table above has an assertion with a same-named decoy elsewhere. Each assertion checks the target and, where relevant, that a resolver strategy (not a name guess) produced it. On main the test fails for each of the original #1555 shapes. With only the receiver-chain step reverted, exactly its two assertions fail.Local legs: macOS arm64,
kotlin_typed_calls,kotlin_lsp,complexity,usagesand the parallel suites; lint-ci (cppcheck 2.20, clang-format, NOLINT, memory-core).