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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant