Skip to content

Kotlin parser extracts no annotations; Android frameworks (Hilt, Compose, Room) are invisible to the graph #1595

Description

@rrodriguesNutrium

The asymmetry

java.py carries a full framework-semantics layer — Spring stereotypes to graph labels, HTTP mapping annotations, DI annotations, JPA @Entity/@Table to DbTable nodes (#887, #843).

kotlin.py is one of the largest language extractors in the repo (2194 lines), with genuinely sophisticated type inference — smart-cast tracking, scope-function receivers, collection element-type inference, typealias resolution. But it extracts no annotation at all:

$ grep -ic "annotation" src/codegraphcontext/tools/languages/kotlin.py
1          # and that one hit is the substring inside "_strip_type_modifiers"

So Java gets Spring awareness, and Kotlin — whose largest ecosystem is Android — gets none. Every @Composable, @HiltViewModel, @Inject, @Preview, @Entity, @Dao and @Test is invisible to the graph.

Why it's cheap

decorators STRING[] is already declared for Function and Class (database_embedded_kuzu.py:177-178), already in the property allow-list (:823-824), and three consumers already read it:

Consumer Location
Dead-code analysis (exclude_decorated_with) tools/code_finder.py:807-820
Inheritance resolution indexing/resolution/inheritance.py:370-374
SCIP pipeline indexing/scip_pipeline.py:136

Ten extractors populate the field; eleven do not — including java.py, which already parses annotations for labels and http_method but never writes them to decorators. Kotlin is not unique in lacking it. The reason to start there is that it is one of the biggest extractors in the repo and its dominant ecosystem is annotation-driven throughout.

The write path is generic (row = dict(item)_sanitize_props), so populating decorators for Kotlin needs no schema change and no writer change.

Concrete impact today

cgc analyze dead-code is effectively unusable on an Android codebase. Almost nothing in an Android app is called by another Kotlin function — lifecycle methods are invoked by the framework, components are declared in the manifest, dependencies come from Hilt, composables are called by the Compose runtime, @Preview only by tooling, Room @Dao implementations are generated at build time, and @Test methods by the runner. Without annotations there is no way to express those exclusions, so the tool reports thousands of false positives.

Proposed staging

Sized so the no-schema-change slices can land first:

PR Content Schema change?
1a Populate decorators for Kotlin functions and classes No
1b visibility + modifiers; Interface/Object decorator columns Yes
2 Android dead-code entry points + exclusion preset No
3 Hilt @Binds / @Provides edges (relates to #846, #842) Maybe (rel pairs)
4 is_composable + PREVIEWS edges Yes
5 Gradle module path identity (:feature:symptomsgradle.py:47 collapses it to the leaf name, so two impl modules collide on the Kuzu PRIMARY KEY) No
6 Remove the shadowed add_file_to_graph (see below) No
7 Android resources + manifest graph Yes, large

PR 1a gates 1b, 2, 3 and 4 — they all need annotations to exist first.

I have 1a ready as a small, self-contained starting point — ~31 lines in kotlin.py, no schema change, and it immediately feeds the three existing consumers. I'll link the PR below.

Which of these slices would you like, and in what order? Happy to stop at 1a if you'd rather discuss the direction first.

Three unrelated pre-existing bugs found while investigating

Reporting rather than fixing — say the word if you'd like separate issues.

  1. Kotlin object declarations with a single-line body are never indexed. object A { } is captured and object A {\n fun x() = 1\n} is captured, but object A { fun x() = 1 } yields zero captures — the grammar misparses it into an infix_expression with an object_literal and a lambda_literal, so KOTLIN_QUERIES["classes"]'s (object_declaration (type_identifier) @name) pattern (kotlin.py:17) never matches. The sibling class_declaration pattern handles bodies fine.

  2. graph_builder.py defines add_file_to_graph twice — line 203 and line 768. The second wins, so lines 203-~750 are dead code, and its item_mappings list has drifted from the live one in indexing/persistence/writer.py:290-313 (the live one handles objects, mixins, extensions, modules, enum_members; the dead one handles none of them). Reading the dead copy gives a wrong picture of which node types reach the graph. Possibly belongs with bug(indexer): parser audit roundup — context attribution, wrong Module names, missing declarations and duplicate call records across 12 languages #1538.

  3. The writer stores every empty list as [""]. writer.py:405b[k] = [str(x) for x in v] if v else [""] (and :409) — so a function with decorators: [] persists as [""], not []. Affects every language and every list-valued property, args included. It does not break find_dead_code ("" CONTAINS 'Preview' is false, so un-annotated functions are correctly retained), but [""] isn't what any caller means by "none".


Environment note: on macOS, three tests in tests/unit/parsers/test_kotlin_parser.py::TestKotlinFunctionCallResolution fail on a clean checkout because tempfile returns /var/... while the parser resolves /private/var/.... Unrelated to the above; mentioning it so it isn't mistaken for a regression in the PR.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status
    Done
    Status
    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions