Skip to content

fix(kotlin): a bare call reaches only a member in reach - #2172

Merged
colbymchenry merged 1 commit into
mainfrom
fix/kotlin-bare-member-scope
Sep 30, 2026
Merged

colbymchenry merged 1 commit into
mainfrom
fix/kotlin-bare-member-scope

Conversation

@colbymchenry

Copy link
Copy Markdown
Owner

A receiver-less Kotlin call exact-matched any project class's same-named method:

  • koin's module { … } in one test went to another test class's private module 268 times;
  • error("…") went to a Logger's error;
  • get() inside a definition lambda went to Koin::get rather than the Scope the lambda runs on.

A bare call now reaches a method only when its class is in reach:

  • Around the call. A class or object, an anonymous object : Table() { … }, or an extension function's receiver. These are read from the nodes and from the source's braces, so a class the parser lost still counts. What they inherit also counts, including the Android framework ancestry, so an extension on ComponentCallbacks is in reach of an AppCompatActivity.
  • A DSL lambda's receiver. Any type the project's function types take as a receiver (Module.() -> Unit, Scope.(ParametersHolder) -> T, JdbcTransaction.(TestDB) -> Unit) and what it inherits. Also any outside type the project writes extensions on (fun MacrobenchmarkScope.waitForContent(), fun StringBuilder.padInt(…)).
  • An imported object member. For example import okio.TestUtil.deepCopy, or a companion member import.
  • Left unjudged. Calls inside with(x) { / x.apply { / x.run { blocks and in .kts scripts.
  • Kotlin's preconditions. require(n >= 0) { … } / check(!closed) belong to the standard library, not a member require(byteCount: Long).
  • Java methods. These are judged the same way from Kotlin. Constructors are exempt.

A/B (wasm, main → fix), edges −/+:

repo − +
koin 388 330
Exposed 754 848
nowinandroid 21 1
moshi 63 14
okio 661 418
retrofit 26 0

The added edges are the right targets:

  • module { → org.koin.dsl::module (267);
  • addLogger(…) → Transaction::addLogger;
  • fileSystem.read(path) { readUtf8() } → BufferedSource::readUtf8;
  • write { writeUtf8() } → BufferedSink::writeUtf8.

The removed edges are stdlib/Gradle/Compose calls, lambda-parameter invocations, and wrong-class members. kotlin-top-level-visibility's fixture now calls its assertEquals extensions inside a withTables { } receiver lambda, as real Exposed code does. Before, the fixture called them where no receiver was in scope, which isn't valid Kotlin.

The full suite passes except ui-server-api "engine's own busiest symbol", which reads this checkout's own local index and fails on main too.

🤖 Generated with Claude Code

A receiver-less Kotlin call exact-matched any class's same-named method:
koin's module { } in one test went to another test class's private
module 268 times, error("…") to a Logger's error, get() inside a
definition lambda to Koin's get rather than the Scope it runs on.

A bare call now reaches a method of a class, anonymous object or extension
receiver around it (read from nodes and from the source's braces, so a
class the parser lost still counts) and what those inherit (with Android
framework ancestry), of a type the project's function types take as a
lambda receiver (plus outside types it writes extensions on), or of an
object the file imports. with/apply/run blocks and .kts scripts are left
unjudged; Kotlin's require/check/assert preconditions are the stdlib's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@colbymchenry
colbymchenry merged commit 2e2b98c into main Sep 30, 2026
@colbymchenry
colbymchenry deleted the fix/kotlin-bare-member-scope branch September 30, 2026 15:20
colbymchenry added a commit that referenced this pull request Sep 30, 2026
#2172 made the members of every type a project function type takes as a
receiver reachable from any bare call, since it could not tell which
lambda a call is in. With several such types in reach, proximity chose:
koin's `single { C51(get(), get()) }` and `Scope.new(...)`'s `get()` went to
`Koin.get` (koin also has `Koin.() -> ...` lambdas), 2,190 times.

- A lambda's receiver is read from the function it is passed to: the type
  before `.(` in its last parameter's function type, directly or through a
  typealias (`definition: Definition<T>`, `typealias Definition<T> =
  Scope.(ParametersHolder) -> T`), when every project function of that name
  agrees. The brace frames put it in the lambda's hierarchy.
- Of the members a bare call can reach, those its lexical hierarchy reaches -
  its class, an extension's receiver, the lambda it is in - come before those
  only the project-wide receiver set reaches.

koin: `get()` -> Scope (2,184), `viewModel` in `scope { }` -> ScopeDSL,
`modules(...)` in `startKoin { }` -> KoinApplication. Exposed: `varchar(...)`,
`integer`, `reference`, `datetime` in a table object -> Table's (were a
test table's same-named property). kotlinx.coroutines: `launch { }` ->
CoroutineScope.launch (was a test DSL's). Edge counts ~unchanged (targets
move): koin, Exposed, okhttp, coroutines, okio; nowinandroid, moshi
unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant