Skip to content

fix(kotlin): emit references edges for primary-constructor class_parameters#2063

Open
danielpucci wants to merge 1 commit into
Graphify-Labs:v8from
danielpucci:fix/kotlin-primary-constructor-refs
Open

fix(kotlin): emit references edges for primary-constructor class_parameters#2063
danielpucci wants to merge 1 commit into
Graphify-Labs:v8from
danielpucci:fix/kotlin-primary-constructor-refs

Conversation

@danielpucci

Copy link
Copy Markdown

Title

Kotlin extractor drops constructor-injected fields (primary_constructor/class_parameters never walked)

Summary

extract_kotlin never emits references edges for dependencies declared in a
class's primary constructor (class Foo(private val x: Bar)) — the
idiomatic Kotlin/Hilt/Dagger DI pattern. Only two Kotlin-specific code paths
exist in graphify/extractors/engine.py:

  1. delegation_specifiersinherits/implements edges for supertypes.
  2. property_declarationreferences edges for body-level val/var.

Neither walks primary_constructor → class_parameters → class_parameter, so
every constructor-injected field is invisible to the graph. Scala already has
the equivalent handling for its structurally identical class_parameters node
(same file, ~30 lines below the Kotlin block) — Kotlin never got the same
treatment.

Reproduction

Using the project's own fixture, tests/fixtures/sample.kt:

class HttpClient(private val config: Config) {
    ...
}
from graphify.extract import extract_kotlin
r = extract_kotlin(Path("tests/fixtures/sample.kt"))
refs = [(e["source"], e["target"]) for e in r["edges"] if e["relation"] == "references"]
assert ("HttpClient", "Config") in refs  # currently FAILS

No edge from HttpClient to Config is ever emitted, despite Config being
a required, explicit, statically-typed constructor dependency. Existing tests
(test_kotlin_parameter_return_generic_and_field_contexts,
test_kotlin_user_types_still_emit_references) already assert references
edges for a body-level val on DataProcessor, so the gap was masked —
nobody had asserted the primary-constructor case.

Why this matters (real-world impact)

Found while running /graphify on a ~600-file, multi-module Kotlin/Android
app that uses constructor-injection (Hilt) everywhere — the standard pattern
for every ViewModel and Repository. Before the fix:

  • A MealDraftStore class injected into 5 production ViewModels via
    private val mealDraftStore: MealDraftStore showed zero incoming edges
    from any of them in the graph — only from test classes that instantiate it
    directly (MealDraftStore() in a test's .setUp(), captured via the
    calls path, which is handled).
  • A FirestoreSyncService injected into 3 repositories was equally invisible.
  • Applying the fix and re-running extraction (AST-only, no LLM cost) on the
    same corpus: +292 edges (5,452 → 5,744), and the top god-nodes' degree
    jumped accordingly (GlucoseSettingsRepository 58→68, HistoryViewModel
    41→53, Food 63→72) — all constructor-injection edges that were silently
    dropped before.

For any Kotlin codebase using DI via primary-constructor (which is close to
all idiomatic modern Kotlin), this means the dependency graph systematically
undercounts how classes actually depend on each other, skewing god-node
rankings, community detection, and "who uses X" queries.

Fix

Mirror the existing Scala class_parameters handling
(graphify/extractors/engine.py, Scala block a few lines below the Kotlin
delegation_specifiers block) for Kotlin's primary_constructor → class_parameters → class_parameter nodes. Patch attached (kotlin-primary- constructor-fix.diff, applies cleanly to v8 HEAD as of abff1b1):

  • graphify/extractors/engine.py: +33 lines, new Kotlin-specific block
    right after the existing delegation_specifiers block.
  • tests/test_languages.py: +14 lines, one new regression test
    (test_kotlin_primary_constructor_val_emits_references) using the
    existing sample.kt fixture — no fixture changes needed, the reproducing
    case (HttpClient(private val config: Config)) was already there.

Verified:

  • pytest tests/test_languages.py — 318 passed, 13 skipped (unrelated
    optional deps), 0 failures, including the 13 Kotlin tests (12 existing
    • 1 new).
  • pytest tests/test_extract.py tests/test_multilang.py tests/test_cross_language_call_resolution.py — 174 passed, 12 skipped,
    0 failures.
  • Manually confirmed on the real-world corpus described above that the fix
    produces exactly the expected new edges (constructor-injected ViewModels →
    their injected repositories/stores) and nothing spurious.

Scope note

This fixes constructor-parameter type references only. It does not
address a related but separate gap: when (result) { is Result.Success -> ... } pattern-matching inside function bodies also isn't walked for type
references anywhere in the extractor (any language) — that's a broader,
more expensive change (walking arbitrary expression bodies) and out of scope
here.

…meters

The Kotlin extractor only walked delegation_specifiers (supertypes) and
property_declaration (body-level val/var) - it never walked
primary_constructor -> class_parameters -> class_parameter, so every
constructor-injected field (`class Foo(private val x: Bar)`, the
idiomatic Kotlin/Hilt/Dagger DI pattern) was invisible to the graph.

Mirrors the existing Scala class_parameters handling a few lines below.
Adds a regression test using the existing sample.kt fixture, which
already contained the reproducing case (HttpClient(private val config:
Config)) but had no assertion covering it.
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