Skip to content

feat(compose): flag composables and resolve @Preview into PREVIEWS edges - #1624

Open
rrodriguesNutrium wants to merge 2 commits into
CodeGraphContext:mainfrom
rrodriguesNutrium:stack/4-compose-semantics
Open

feat(compose): flag composables and resolve @Preview into PREVIEWS edges#1624
rrodriguesNutrium wants to merge 2 commits into
CodeGraphContext:mainfrom
rrodriguesNutrium:stack/4-compose-semantics

Conversation

@rrodriguesNutrium

@rrodriguesNutrium rrodriguesNutrium commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Adds two Compose-aware facts to the graph: is_composable on Function, and a PREVIEWS edge from each @Preview function to the composable it renders. Now also fixes #1602 at source, which this PR previously only worked around.

Both Compose facts exist to answer questions that are currently unanswerable. A Compose UI tree is a call graph of @Composable functions and nothing marks them as such; and a @Preview function looks exactly like dead code — nothing calls it, because the IDE and the Compose tooling do.

is_composable

Set from an anchored module-level match, ^@Composable\b. Anchored and word-bounded on purpose: a substring test would also match @NonComposable-style names, and \b keeps it from matching a longer annotation that merely starts with the same letters.

Declared in all three places the Kuzu backend requires — node-table declaration, SCHEMA_MAP allow-list, and simple_migrations. Missing any one of the three loses the property with no error: CREATE NODE TABLE on an existing database throws "already exists" and is swallowed, and SCHEMA_MAP is an allow-list whose misses are dropped silently at write time. The failure mode is that Kuzu quietly lacks the property while schemaless backends have it, so the two backends disagree and nothing reports it.

PREVIEWS

FROM Function TO Function. Source is the @Preview-annotated function, target is the composable it calls.

Annotation matching strips arguments down to the bare name and compares for equality against "Preview". That's the word-boundary-safe equivalent of the is_composable regex, and it's why PreviewParameter and PreviewScreenSizes are correctly excluded without needing a second annotation-name parser.

Resolution is scoped to calls in the preview function's own body, so an unrelated call (a logging helper, say) isn't mistaken for the previewed composable.

Second commit: fixing #1602 rather than working around it

KOTLIN_QUERIES["calls"] captures constructor_invocation nodes — and an annotation carrying arguments is a constructor_invocation in this grammar. So @Preview(showBackground = true) on fun GreetingPreview() was recorded as a call, from GreetingPreview, named Preview.

The echo also reached expressions inside the argument list: @PreviewParameter(provider = FooProvider::class) contributes a callable_reference capture. That's why a preview function appeared to call itself and every type named anywhere in its own annotations.

The first version of this PR filtered these annotation-echo rows inside build_previews_links and left the parser alone. That was the wrong layer — the phantoms are present in function_calls for Kotlin generally, not only under Compose. The second commit filters at source instead.

The discriminator is the parse tree, not the name. _is_within_annotation walks ancestors looking for an annotation node:

Shape Ancestor chain Kept?
@Preview(showBackground = true) constructor_invocation <- annotation rejected
FooProvider::class in an annotation arg callable_reference <- value_argument <- … <- annotation rejected
@file:JvmName("Utils") constructor_invocation <- file_annotation rejected
class A : B() constructor_invocation <- delegation_specifier kept
constructor() : this(1) constructor_delegation_call <- secondary_constructor kept

A filter keyed on the node type alone would have deleted the last two — every superclass-construction and constructor-delegation edge in the Kotlin graph. The regression guards for both are in the test class.

Two node types are accepted, not one. Declaration-level annotations nest under annotation, which covers functions, classes, objects, interfaces, typealiases, properties, and use-site targets like @field:ColumnInfo(...) and @get:JvmName(...). File-level annotations are the exception — @file:JvmName("Utils") produces file_annotation with no annotation anywhere in the chain, so checking only annotation would have left the phantom at the top of every file using a @file: target. Each position has a test.

build_previews_links keeps its own_decorator_names rejection as a backstop rather than dropping it now that the parser is clean. It costs one set lookup, and its test feeds hand-built rows rather than parser output, so it still pins a real invariant. The docstring now says the parser filters at source.

Golden refresh

The Kotlin golden was already stale on this branch before the fix commitAndroidAnnotations.kt gained the compose functions, shifting line numbers for UserViewModel, UserEntity, PlainHelper and findAll. tests/integration/test_parser_goldens.py -k kotlin was red on the first commit and is green now. (This is the refresh @Shashankss1205 flagged on #1623 as owed by this PR.)

Unlike every previous golden refresh in this series, this one removes edges, so I regenerated twice — once without the fix, once with — to isolate what the fix itself changes:

                without fix -> with fix
CALLS                    28 ->   24   (-4)
BINDS                     2 ->    2
COMPANION_OF              2 ->    2
CONTAINS                249 ->  249
HAS_PARAMETER            26 ->   26
IMPORTS                   2 ->    2
INHERITS                 11 ->   11
PREVIEWS                  2 ->    2
nodes                   244 ->  244

Only CALLS moves. At parser level the fix drops 6 phantom rows across the fixture, of which 4 had resolved into graph edges:

AndroidAnnotations.kt   'Preview' from GreetingPreview
                        'Entity'  from UserEntity
                        'Query'   from findAll
Annotations.kt          'Fancy'   from Foo, from baz (x2)

Three of the six are in Annotations.kt — a plain, non-Android Kotlin fixture using @Fancy(...) — which confirms the bug was never Compose- or Android-specific.

Verification

Measured on this branch, on macOS:

tests/unit          3 failed, 1246 passed, 19 skipped
tests/integration              45 passed

Baseline on the first commit of this PR was 3 failed, 1238 passed — the same 3 failures, +8 new tests. Those 3 are #1608 (tempfile returns /var/… while the parser resolves to /private/var/…); they reproduce on a clean checkout of main and are unrelated to this PR. Integration was red before the fix commit (stale golden) and is green after.

The fixture declares @Composable/@Preview as local stub annotations, so no Compose compiler or Android SDK is needed on the test path. Tests cover the negative cases directly: @PreviewParameter must not be treated as @Preview, and the self-edge from the annotation echo must not appear.

Commits

db56984  feat(compose): flag composables and resolve @Preview into PREVIEWS edges
bbd67db  fix(parsers/kotlin): stop recording annotations as function calls (#1602)

Last of the five-PR Android/Kotlin series; its three prerequisites (#1620, #1622, #1623) have merged, and #1621 (gradle) was independent and has also merged.

Closes #1602

@vercel

vercel Bot commented Aug 13, 2026

Copy link
Copy Markdown

@rrodriguesNutrium is attempting to deploy a commit to the shashankss1205's projects Team on Vercel.

A member of the Team first needs to authorize it.

@rrodriguesNutrium

Copy link
Copy Markdown
Contributor Author

Android/Kotlin series — merge order

main
 └── #1620  kotlin: visibility, modifiers, decorators on Interface/Object   <- base
      └── #1622  dead-code: framework entry points + overrides
           └── #1623  hilt: @Binds / @Provides -> BINDS edges
                └── #1624  compose: is_composable + PREVIEWS edges

main
 └── #1621  gradle: canonical module identity        (independent, any order)

Each PR targets main because a stacked PR base can't live across a fork boundary, so every one carries its prerequisites as earlier commits. Review only the last commit on #1622, #1623 and #1624 — the commit list is per-slice and unsquashed, so per-commit diffs are clean.

Happy to split, reorder, or squash any of these differently if it suits review better.

Adds `is_composable` to Function nodes and a PREVIEWS relationship from
each @Preview-annotated function to the composable it renders.

- kotlin.py sets is_composable from a module-level ^@composable\b match
- database_embedded_kuzu.py declares the column, the SCHEMA_MAP entry and
  the migration (all three are required; missing any one silently drops
  the property on Kuzu while schemaless backends keep it)
- build_previews_links rejects annotation-echo phantom calls: the parser
  records `@Preview` argument expressions as calls, so a preview function
  appears to call itself and every composable named in its annotation

Stacked on the Hilt slice.
@rrodriguesNutrium
rrodriguesNutrium force-pushed the stack/4-compose-semantics branch from ea2e3ba to db56984 Compare August 14, 2026 08:22
@rrodriguesNutrium

Copy link
Copy Markdown
Contributor Author

Rebased onto current main — conflict resolved.

The conflict came from #1620/#1622/#1623 landing as squash merges: identical content under new SHAs, so the stacked commits read as duplicate changes. Dropped them and replanted just the compose commit on top of main.

Now 1 commit, +772/-2. pytest tests/unit -q12 failed, 1229 passed, 19 skipped; current main gives 12 failed, 1213 passed, 19 skipped — same 12 pre-existing failures, no regressions.

rrodriguesNutrium added a commit to rrodriguesNutrium/CodeGraphContext that referenced this pull request Aug 14, 2026
…deGraphContext#1602)

An annotation with arguments is `annotation -> @ + constructor_invocation`
in the tree-sitter Kotlin grammar, and KOTLIN_QUERIES["calls"] captures
`constructor_invocation`, so every such annotation was recorded as a call
made by the annotated declaration. `@Preview(showBackground = true)` on
`fun GreetingPreview()` became a call from GreetingPreview to "Preview".

The echo also reached expressions *inside* the argument list:
`@PreviewParameter(provider = FooProvider::class)` contributed a
`callable_reference` capture, which is why a preview function appeared to
call every type named anywhere in its own annotations.

Filtered at source in _parse_calls via _is_within_annotation, an ancestor
walk for an annotation node. The parse tree is the discriminator, not the
name: `B()` in `class A : B()` is also a `constructor_invocation` but its
chain runs through `delegation_specifier`, so it survives, as does a
`constructor_delegation_call` under `secondary_constructor`. Keying on the
node type alone would have deleted both.

The walk accepts two node types rather than one. Declaration-level
annotations nest under `annotation` -- functions, classes, objects,
interfaces, typealiases, properties, and use-site targets like
`@field:ColumnInfo(...)` and `@get:JvmName(...)`. File-level annotations
are the exception: `@file:JvmName("Utils")` produces `file_annotation`
with no `annotation` in the chain, so checking only `annotation` would
have left the phantom at the top of every file using a `@file:` target.
Each position has a test.

This is the bug CodeGraphContext#1624 previously worked around rather than fixed.
build_previews_links keeps its own_decorator_names rejection as a
backstop (it is pinned by a test feeding hand-built rows, so it stays
meaningful now that parser output no longer carries the echo), with the
docstring updated to say the parser filters at source.

Golden refresh: the Kotlin golden was already stale on this branch --
AndroidAnnotations.kt gained the compose functions, shifting line numbers
for UserViewModel, UserEntity, PlainHelper and findAll -- so the
integration golden test was red before this commit and is green after.
Regenerating twice, with and without this fix, isolates its own effect to
CALLS 28 -> 24 (-4). Node counts and every other edge type -- PREVIEWS,
BINDS, INHERITS, CONTAINS, HAS_PARAMETER, IMPORTS, COMPANION_OF -- are
identical across the two regenerations.

At parser level the fix removes 6 phantom rows across the fixture; 4 had
resolved into graph edges. Three are in Annotations.kt (`@Fancy(...)`, a
plain non-Android fixture), confirming the issue was never Compose- or
Android-specific.

tests/unit: 3 failed, 1246 passed (was 3 failed, 1238 passed before this
commit -- the same 3 pre-existing macOS /var-vs-/private/var failures of
CodeGraphContext#1608, which reproduce on a clean checkout).
tests/integration: 45 passed.

Closes CodeGraphContext#1602

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…deGraphContext#1602)

An annotation with arguments is `annotation -> @ + constructor_invocation`
in the tree-sitter Kotlin grammar, and KOTLIN_QUERIES["calls"] captures
`constructor_invocation`, so every such annotation was recorded as a call
made by the annotated declaration. `@Preview(showBackground = true)` on
`fun GreetingPreview()` became a call from GreetingPreview to "Preview".

The echo also reached expressions *inside* the argument list:
`@PreviewParameter(provider = FooProvider::class)` contributed a
`callable_reference` capture, which is why a preview function appeared to
call every type named anywhere in its own annotations.

Filtered at source in _parse_calls via _is_within_annotation, an ancestor
walk for an annotation node. The parse tree is the discriminator, not the
name: `B()` in `class A : B()` is also a `constructor_invocation` but its
chain runs through `delegation_specifier`, so it survives, as does a
`constructor_delegation_call` under `secondary_constructor`. Keying on the
node type alone would have deleted both.

The walk accepts two node types rather than one. Declaration-level
annotations nest under `annotation` -- functions, classes, objects,
interfaces, typealiases, properties, and use-site targets like
`@field:ColumnInfo(...)` and `@get:JvmName(...)`. File-level annotations
are the exception: `@file:JvmName("Utils")` produces `file_annotation`
with no `annotation` in the chain, so checking only `annotation` would
have left the phantom at the top of every file using a `@file:` target.
Each position has a test.

This is the bug CodeGraphContext#1624 previously worked around rather than fixed.
build_previews_links keeps its own_decorator_names rejection as a
backstop (it is pinned by a test feeding hand-built rows, so it stays
meaningful now that parser output no longer carries the echo), with the
docstring updated to say the parser filters at source.

Golden refresh: the Kotlin golden was already stale on this branch --
AndroidAnnotations.kt gained the compose functions, shifting line numbers
for UserViewModel, UserEntity, PlainHelper and findAll -- so the
integration golden test was red before this commit and is green after.
Regenerating twice, with and without this fix, isolates its own effect to
CALLS 28 -> 24 (-4). Node counts and every other edge type -- PREVIEWS,
BINDS, INHERITS, CONTAINS, HAS_PARAMETER, IMPORTS, COMPANION_OF -- are
identical across the two regenerations.

At parser level the fix removes 6 phantom rows across the fixture; 4 had
resolved into graph edges. Three are in Annotations.kt (`@Fancy(...)`, a
plain non-Android fixture), confirming the issue was never Compose- or
Android-specific.

tests/unit: 3 failed, 1246 passed (was 3 failed, 1238 passed before this
commit -- the same 3 pre-existing macOS /var-vs-/private/var failures of
CodeGraphContext#1608, which reproduce on a clean checkout).
tests/integration: 45 passed.

Closes CodeGraphContext#1602

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@rrodriguesNutrium
rrodriguesNutrium force-pushed the stack/4-compose-semantics branch from bbd67db to 8d8d4dc Compare August 15, 2026 14:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Backlog tasks

Development

Successfully merging this pull request may close these issues.

bug(parsers/kotlin): annotations with arguments are recorded as function calls

1 participant