Skip to content

feat(kotlin): index @Composable as component, record annotations - #1653

Open
aabolfazl wants to merge 1 commit into
colbymchenry:mainfrom
aabolfazl:feat/kotlin-compose-annotations
Open

feat(kotlin): index @Composable as component, record annotations#1653
aabolfazl wants to merge 1 commit into
colbymchenry:mainfrom
aabolfazl:feat/kotlin-compose-annotations

Conversation

@aabolfazl

Copy link
Copy Markdown

Annotation-driven Kotlin was unqueryable: codegraph_search kind=component returned zero, and @HiltViewModel / @Dao / @Entity left no trace on the graph. Framework annotations live in libraries outside the index, so the unresolved decorates ref was the only artifact and it never resolved.

What changed

@Composable functions and methods get the component kind, and annotation names persist onto the node and show in codegraph_node:

$ codegraph query "" --kind component
component   NiaApp        app/src/main/.../ui/NiaApp.kt:89
component   NiaButton     core/designsystem/.../component/Button.kt:50
...  151 components indexed          # 0 on main

$ codegraph node NewsResourceDao
**NewsResourceDao** (interface)
**Annotations:** `@Dao`               # nothing on main

Three extraction bugs fall out of the same collector — all silent before:

Shape main this PR
@Preview(showBackground = true) dropped (constructor_invocation never unwrapped) Preview
@Dao interface / @Serializable enum class dropped (path collected no decorators) Dao / Serializable
@[Suppress("x") JvmStatic] Suppress only both

Opt-in per language via LanguageExtractor.extendedAnnotations, which only Kotlin sets: each of the 13 kernel walkers owns its decorator logic, so widening the shared engine alone breaks parity elsewhere — and collecting past the first target is Kotlin-specific, since Swift's @Siblings(through: Pivot.self, from: \.$left) would harvest self and $left as names.

Kotlin routes to the kernel, so codegraph-kernel/src/kotlin.rs and the wasm walker both change; annotation collection moved ahead of node creation in both, because a node's kind and decorators are fixed when it is built.

Blast radius. A component node silently drops out of gates keyed on function/method. Widened alongside: flushFnRefCandidates/defined_fn_names (a real dropped-edge bug — register(::Header) lost its function_ref, pinned by a test), captureValueRefScope/value_scopes, enclosingScopeStartLine, matchFuzzy's callable kinds, the function-ref candidate filter, findDeadCode's defaults, and kmpKindsCompatible for an asymmetrically annotated expect/actual pair.

Tested

Kernel↔wasm sweep, fresh --depth 1 clones. Before is a worktree at main with its own dist/ and its own kernel built from that tree's kotlin.rs, so deltas are attributable:

Repo Files Diffs Deferred before → after Nodes before → after Components
okio 327 0 24 → 24 6,764 → 6,764 0
okhttp 617 0 51 → 51 16,396 → 16,396 0
kotlinx.coroutines 1,082 0 51 → 51 15,379 → 15,379 0
nowinandroid 350 0 11 → 11 5,603 → 5,603 151
compose-samples 380 0 16 → 16 8,836 → 8,836 545

0 diffs on 2,756 files; deferral and node counts unchanged (the +1/+2 vs the recorded 23/49/51 is repo drift — the before-tree reproduces 24/51/51 on these checkouts). The three non-Compose libraries correctly produce zero components while still gaining annotation coverage (okhttp 0 → 3,895 annotated symbols).

31 tests in __tests__/kotlin-annotations.test.ts, every guarantee asserted on both arms (CODEGRAPH_KERNEL=0 for wasm, tryKernelExtract for the kernel) — a bare extractFromSource only exercises the kernel for Kotlin. torture.kt extended with the new shapes. Full suite: 3,254 passed, 0 failed.

Not run: an agent A/B. An earlier eval found census questions ("how many @Composable…") don't make the agent reach for codegraph — 3/3 runs chose grep. The component kind and the codegraph_node line ride tools the agent already calls; neither has a measured retrieval win yet. Recorded in docs/design/kotlin-annotation-extraction.md.

Node ids hash the kind, so an existing Kotlin index needs a re-index before component nodes appear.

🤖 Generated with Claude Code

On a Compose/Hilt/Room codebase none of "which functions are @composable",
"which classes are @hiltviewmodel", or "find every @dao" could be answered:
`codegraph_search kind=component` returned zero for Kotlin, and annotations
left no trace on the graph. Framework annotations are declared in libraries
outside the index, so the unresolved `decorates` reference extraction emitted
was the only artifact and it never resolved.

Annotation simple names now persist onto the annotated node and surface in
`codegraph_node`, and a declarative annotation->NodeKind map classifies
`@Composable` functions and methods as `component` — the Kotlin analogue of
the function-level component nodes the React resolver already creates for
JSX-returning functions.

Kotlin routes to the native kernel, so the Rust walker and the wasm extractor
both change: the parity gate compares the two arms and fails on either one
alone. In both, annotation collection moved ahead of node creation, because a
node's kind and its decorators list are fixed when the node is built.

Three extraction bugs fall out of the same collector. Arg-bearing annotations
(`@Preview(showBackground = true)`) parse as `constructor_invocation`, which
neither arm unwrapped, so they emitted nothing at all. Annotations on
interfaces and enums were never collected either — that is where Room puts
`@Dao`. And Kotlin's bracket form (`@[Suppress("x") JvmStatic]`) yielded only
its first entry.

All of it is opt-in per language via `LanguageExtractor.extendedAnnotations`,
which only Kotlin sets. Each of the 13 kernel walkers owns its own decorator
logic, so widening the shared engine alone would break parity for every other
routed language; and collecting past the first target is Kotlin-specific —
Swift carries argument expressions inside the attribute node, so
`@Siblings(through: Pivot.self, from: \.$left)` would harvest `self` and
`$left` as annotation names.

Reclassifying to `component` has a blast radius: such a node silently drops
out of every gate keyed on function/method. The ones widened alongside it are
`flushFnRefCandidates`/`defined_fn_names` (a real dropped-edge bug, pinned by
a test), `captureValueRefScope`/`value_scopes`, `enclosingScopeStartLine`,
`matchFuzzy`'s callable kinds, the function-ref candidate filter,
`findDeadCode`'s defaults, and `kmpKindsCompatible` for an asymmetrically
annotated expect/actual pair.

Node ids hash the kind, so an existing Kotlin index needs a re-index before
the component nodes appear.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant