Skip to content

feat(dead-code): exclude framework entry points and overrides on JVM codebases - #1622

Merged
Shashankss1205 merged 3 commits into
CodeGraphContext:mainfrom
rrodriguesNutrium:stack/2-android-dead-code
Aug 13, 2026
Merged

feat(dead-code): exclude framework entry points and overrides on JVM codebases#1622
Shashankss1205 merged 3 commits into
CodeGraphContext:mainfrom
rrodriguesNutrium:stack/2-android-dead-code

Conversation

@rrodriguesNutrium

Copy link
Copy Markdown
Contributor

find_dead_code is close to unusable on an Android codebase: almost everything it reports is framework-invoked and therefore alive. This PR teaches the query the two ways JVM code gets called without any project function calling it.

The problem

Nothing in an Android app calls onCreate. The framework does. Same for onBindViewHolder, doWork, onReceive, and every override of a superclass or interface member — the call site is a supertype reference, which the graph does not resolve to the override.

On a real multi-module Android repo the report was dominated by these, which is the failure mode where a dead-code tool stops being read at all.

The change

Two AND NOT clauses, plus one exported constant.

Framework entry points, scoped to lang IN ['kotlin', 'java'] so every other language keeps the existing global behaviour:

AND NOT (
      func.lang IN ['kotlin', 'java']
      AND toLower(func.name) IN ['oncreate', 'onstart', ...]
    )

29 lifecycle names, lowercased on both sides. Scoping by language matters — onCreate is not special in Python, and a global list would silently change results for existing users.

Overrides, using the modifiers array this stack's first PR adds:

AND NOT (
      func.modifiers IS NOT NULL
      AND 'override' IN func.modifiers
    )

The IS NOT NULL guard is required, not defensive noise. In Cypher 'override' IN null evaluates to null, and a null predicate drops the row — so without the guard every function whose modifiers is unset (i.e. every function in every other language) would vanish from the report. I verified this behaviour directly rather than assuming it.

ANDROID_DECORATOR_PRESET — 22 annotation names to pass as exclude_decorated_with when analysing an Android project: @Composable, @Preview, @Test, @Inject, @Provides, @Dao, @TypeConverter and so on. Each means "something other than project code calls this". It's exported rather than hardcoded into the query because the choice is the caller's — a project that wants its unused @Test functions reported can simply not pass it. Matching is by substring, so the bare names match annotations carrying arguments.

Verification

pytest tests/unit -q12 failed, 1191 passed, 19 skipped.
Same 12 failures on unmodified origin/main (12 failed, 1176 passed, 19 skipped) — pre-existing and unrelated.

The new tests assert both directions: a lifecycle method and an override are excluded, and a genuinely uncalled ordinary function is still reported. A one-directional test here would pass trivially against a query that returns nothing at all.

Scope

code_finder.py +39, one new test file, two doc lines. No schema change.


Second of five. Requires the modifiers column from #1620 — the override clause is inert without it. Review only the last commit; the earlier one is that PR, carried along because GitHub can't host a stacked base across a fork boundary.

rrodriguesNutrium and others added 2 commits August 13, 2026 21:27
Follows CodeGraphContext#1596, which added `decorators`. The same `modifiers` node also
carries visibility (public/private/internal/protected) and the class
kind (data/sealed/value/annotation), plus abstract/open/override and
suspend/inline -- none of which reached the graph.

Adds two properties on Function and Class, and completes the two columns
CodeGraphContext#1596 deferred on Interface and Object:

  Function/Class      visibility STRING, modifiers STRING[]
  Interface/Object    visibility STRING, modifiers STRING[], decorators STRING[]

Each in all three required places -- node-table declaration, SCHEMA_MAP
allow-list, and simple_migrations so pre-existing databases get them via
ALTER TABLE. Without the last, CREATE NODE TABLE throws "already exists"
on an existing database and is swallowed, so the columns never arrive.

Two grammar details worth knowing:

`enum` is not a modifier. `enum class C` produces no `modifiers` node at
all -- the keyword is a direct child of class_declaration, exactly like
`interface`. It is derived with the pattern _parse_classes already uses
for interface detection, so `modifiers` is the single place to ask what
kind of class this is.

visibility defaults to the string "public" rather than null, matching
Kotlin's own default, so consumers need no null handling.

Since CodeGraphContext#1596 gated `decorators` behind `category == "classes"` only
because Interface/Object had no column, that gate is removed here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Almost nothing in an Android app is called by another Kotlin function:
the framework invokes lifecycle methods, the manifest declares
components, Hilt supplies dependencies, the Compose runtime calls
composables, annotation processors generate Room implementations, test
runners call @test, and `override fun` is reached through its supertype.
So the tool reports thousands of false positives.

Three changes:

- `override` functions are treated as live, using the `modifiers`
  property added in the parent commit. Guarded with IS NOT NULL, because
  'override' IN NULL is NULL rather than false in Cypher -- unguarded it
  would silently drop every function lacking the property from results.
- Android/JVM lifecycle names (onCreate, onBind, doWork, ...) are treated
  as entry points, scoped to func.lang IN ['kotlin','java'] so other
  languages keep the original global list untouched.
- ANDROID_DECORATOR_PRESET: a documented tuple of annotations meaning
  "something other than project code calls this" -- Compose, test
  runners, Hilt, Room, and @TypeConverter.

Measured on a real 2,861-file Android codebase: 7,141 findings without
the preset, 1,055 with it -- an 85% reduction, dominated by @test methods
and Hilt providers.

Two honest limits, both documented:

The override exemption is inert for Java, because only kotlin.py emits
`modifiers`; the lifecycle-name half does work for Java. And four preset
entries (Dao, HiltViewModel, AndroidEntryPoint, Serializable) annotate
classes rather than functions, so they cannot match a query that does
MATCH (func:Function) -- kept, with a comment, because the data exists
and it is the query's scope that makes them inert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@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.

Conflict in tests/unit/core/test_database_kuzu_kotlin_metadata.py: main is
a strict superset (this branch's tests all landed via CodeGraphContext#1620, plus main has
CodeGraphContext#1617's three probe tests), so main's version is taken whole.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@Shashankss1205 Shashankss1205 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. This is the one I most wanted from your staging plan, and it delivers — verified end to end on a small Android-shaped fixture:

no filter           -> ['GreetingPreview', 'alsoDead', 'testSomething', 'trulyDead']
ANDROID preset      -> ['alsoDead', 'trulyDead']

alsoDead and trulyDead are the only genuinely dead functions in that fixture, so this is a clean result. Two details I checked specifically:

  • onCreate / onResume are excluded without needing the preset, via the lifecycle-name list and the override modifier check — so the default behaviour improves for anyone who never reads the docs. That's the right default.
  • Greeting correctly survives as not dead because GreetingPreview calls it, rather than being swept up by the @Composable exclusion. The preset excludes the annotated function itself, not its callees.

Scoping the Android names to func.lang IN ['kotlin', 'java'] is the right call — it keeps every other language on the original global list, so this can't regress Python or Go dead-code results.

The override exclusion depending on func.modifiers from #1620 is why the stack ordering matters; good that you sequenced it that way rather than duplicating the extraction.

I resolved the conflict with main and pushed. It was straightforward: main is now a strict superset of this branch's copy of test_database_kuzu_kotlin_metadata.py (your tests landed via #1620, and main additionally has #1617's probe tests), so I took main's whole.

test_dead_code_android.py    5 passed
tests/unit/               1224 passed
tests/integration/          45 passed

Between this, #1596 and #1609, cgc analyze dead-code is finally usable on Android — which was the original point of #1595.

@Shashankss1205
Shashankss1205 merged commit 0883b8c into CodeGraphContext:main Aug 13, 2026
14 of 15 checks passed
@github-project-automation github-project-automation Bot moved this from Backlog tasks to Done in CGC Progress Board Aug 13, 2026
@Shashankss1205 Shashankss1205 added gssoc:approved GSSoC validation: counts toward scoring level:advanced GSSoC difficulty: 55 pts contributor / 30 mentor mentor:Shashankss1205 GSSoC mentor attribution: credits reviewing mentor quality:exceptional GSSoC quality: x1.5 contributor / +10 mentor type:feature GSSoC type bonus: feature labels Aug 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gssoc:approved GSSoC validation: counts toward scoring level:advanced GSSoC difficulty: 55 pts contributor / 30 mentor mentor:Shashankss1205 GSSoC mentor attribution: credits reviewing mentor quality:exceptional GSSoC quality: x1.5 contributor / +10 mentor type:feature GSSoC type bonus: feature

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants