Skip to content

fix: correct JVM dependency graphs, Kotlin import aliases, and container entry points - #736

Closed
apandeya wants to merge 3 commits into
peteromallet:mainfrom
apandeya:fix/kotlin-graph-and-alias
Closed

fix: correct JVM dependency graphs, Kotlin import aliases, and container entry points#736
apandeya wants to merge 3 commits into
peteromallet:mainfrom
apandeya:fix/kotlin-graph-and-alias

Conversation

@apandeya

Copy link
Copy Markdown

Problem

Scanning a real Spring Boot Kotlin codebase (446 files, hexagonal architecture) produced 627 findings, of which ~567 were false positives. Three root causes, all in shared tree-sitter/JVM machinery:

1. Zero-edge dependency graphs (critical)

ts_build_dep_graph compared resolver output (absolute paths) against the raw file list, which find_source_files may return as relative paths. The resolved not in file_set check never matched, so no import edge was ever recorded and every file reported importer_count = 0. The orphaned detector then flagged 280 heavily-used files ("zero importers, not an entry point") — including a domain model with 20 importers — and test_coverage flagged the same 280 files as untested modules.

2. Kotlin import aliases invisible to unused-import analysis

The Kotlin grammar nests aliases as import_header > import_alias > [as, type_identifier], but _extract_alias never descended into import_alias and didn't accept type_identifier leaves. Every import com.foo.Bar as Baz was checked under the name Bar, which never appears in the file — flagging every aliased import as unused. (Aliased imports are a recommended Kotlin convention for disambiguation.)

3. DI-wired and same-package files look orphaned

Even with a correct graph, Spring beans (@Service, @RestController, ...) are container-instantiated and have zero static importers by design, and same-package references in JVM languages need no import statement — so DTOs, mappers, and helpers referenced only by same-package files still looked orphaned.

Fix

  • imports/graph.py: map normalized absolute paths back to the file-list key form (handling both cwd-relative and scan-root-relative entries) before recording edges; add optional same-package reference detection via new spec fields.
  • treesitter/types.py / specs/compiled.py: new optional package_query + top_level_name_query spec fields, wired up for Kotlin. Same-package references are registered only as importers of the declaring file (never as outgoing imports), so cycle detection stays import-based. The name query deliberately doesn't require a body, so data classes, interfaces and type aliases are covered.
  • analysis/unused_imports.py: descend into import_alias nodes and accept type_identifier/simple_identifier leaves in _extract_alias.
  • engine/detectors/orphaned.py: mirroring the existing Next.js convention awareness, JVM files (.kt/.kts/.java/.scala) carrying Spring stereotypes, Spring Data repository interfaces, or a main entry point are treated as entry points.
  • languages/kotlin/__init__.py: exclude Gradle build scripts (build.gradle.kts, settings.gradle.kts) from orphan findings.

Verification

Against the codebase that surfaced this (Spring Boot + Kotlin, hexagonal architecture):

Detector Before After Notes
orphaned 280 5 remaining 5 verified genuinely unreferenced (dead code)
unused (imports) 7 0 all 7 were aliased imports in active use
test_coverage 280 140 the 140 dropped were graph noise; the rest are real test gaps with correct importer counts
objective score 77.4 90.4 same tool version, only this patch differs

Tests: new regression tests for relative-path file lists, Kotlin alias extraction, same-package references (real grammar), and JVM entry-point markers. Full suite: 5818 passed; the only failures (test_transitive_modules, test_cli::TestStatePath, test_bash_unused_imports) reproduce on clean main in this environment and are unrelated.

Two defects produced empty or incomplete dependency graphs for
tree-sitter-backed JVM languages (Kotlin in particular):

1. ts_build_dep_graph compared resolver output (absolute paths) against
   the raw file list, which finders may return as relative paths. No edge
   ever matched, so every file reported zero importers and the orphaned
   and test-coverage detectors mass-flagged hundreds of heavily-used
   files as dead code.

2. Same-package references need no import statement in JVM languages, so
   even a correct import graph under-counts importers: DTOs, mappers and
   helpers referenced by same-package files looked orphaned. A new
   optional spec pair (package_query + top_level_name_query) registers
   same-package references as importers of the declaring file only, so
   cycle detection stays import-based. Wired up for Kotlin, including
   body-less declarations (data classes, interfaces, type aliases).

Also excludes Gradle build scripts (build.gradle.kts / settings.gradle.kts)
from Kotlin orphan findings via entry_patterns.

Verified against a real Spring Boot Kotlin codebase: orphaned findings
dropped from 280 (virtually all false positives) to 5 genuinely
unreferenced files, and test-coverage findings now reflect real importer
counts.
Kotlin's tree-sitter grammar nests aliased imports as
import_header > import_alias > [as, type_identifier], but
_extract_alias never descended into import_alias and did not accept
type_identifier leaves. Aliased imports were therefore checked under
their original name, which never appears in the file, and reported as
unused — a false positive for every aliased import, a pattern Kotlin
style guides actively encourage for disambiguation.
Files wired by dependency-injection frameworks have zero static
importers by design. Mirroring the existing Next.js convention
awareness, JVM files (.kt/.kts/.java/.scala) are no longer flagged as
orphaned when they carry Spring-style stereotypes (@service,
@RestController, @configuration, listeners, scheduled jobs, ...),
implement Spring Data repository interfaces (proxied at runtime), or
declare a main entry point (fun main / public static void main).
@apandeya apandeya closed this Sep 11, 2026
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