3737JVM_BUILD_FILES = ("pom.xml" , "build.gradle" , "build.gradle.kts" )
3838# `package a.b.c;` (Java) / `package a.b.c` (Kotlin, no semicolon).
3939JVM_PACKAGE_RE = re .compile (r"^\s*package\s+([A-Za-z_][\w.]*)\s*;?\s*$" , re .M )
40+ # `import a.b.C;` / `import static a.b.C.m;` / `import a.b.*;` (captured with a
41+ # trailing dot, stripped in code) / Kotlin `import a.b.C as D`.
42+ JVM_IMPORT_RE = re .compile (r"^\s*import\s+(?:static\s+)?([A-Za-z_][\w.]*)" , re .M )
4043JVM_EXTS = (".java" , ".kt" )
4144
4245
@@ -54,6 +57,7 @@ def collect(repo, tree=None):
5457 _py_edges (repo , tree , dirs , counter )
5558 _js_edges (repo , tree , dirs , counter )
5659 _rust_edges (repo , tree , counter )
60+ _jvm_edges (repo , tree , counter )
5761
5862 edges = [{"from" : f or "(root)" , "to" : t or "(root)" , "lang" : lang , "count" : n }
5963 for (f , t , lang ), n in counter .items ()]
@@ -263,6 +267,49 @@ def _jvm_modules(tree):
263267 if any (b in files for b in JVM_BUILD_FILES ))
264268
265269
270+ def _jvm_edges (repo , tree , counter ):
271+ """Java/Kotlin edges: imports resolved against the package-declaration
272+ index by longest package prefix. Externals (java.*, kotlin.*, third-party)
273+ are never in the index, so they drop out naturally."""
274+ index = _jvm_package_index (repo , tree )
275+ if not index :
276+ return
277+ for rel , files in tree .items ():
278+ for f in files :
279+ if not f .endswith (JVM_EXTS ):
280+ continue
281+ lang = "Kotlin" if f .endswith (".kt" ) else "Java"
282+ for path in JVM_IMPORT_RE .findall (_read (repo , rel , f )):
283+ dirs = _jvm_resolve (path .rstrip ("." ), index )
284+ for target in _jvm_prefer_main (dirs ):
285+ if target != rel :
286+ counter [(rel , target , lang )] += 1
287+
288+
289+ def _jvm_resolve (path , index ):
290+ """Longest package prefix of an import path present in the index.
291+
292+ Full path first (wildcard imports name the package itself), then
293+ successively dropping trailing segments (class name, nested classes)."""
294+ parts = path .split ("." )
295+ for i in range (len (parts ), 0 , - 1 ):
296+ dirs = index .get ("." .join (parts [:i ]))
297+ if dirs :
298+ return dirs
299+ return ()
300+
301+
302+ def _jvm_prefer_main (dirs ):
303+ """A package usually exists in both main and test source roots; edges
304+ into test dirs from an import would be fabricated, so prefer non-test
305+ dirs when the package is declared in several places."""
306+ if len (dirs ) <= 1 :
307+ return sorted (dirs )
308+ main = [d for d in dirs
309+ if not {"test" , "tests" } & set (d .split ("/" ))]
310+ return sorted (main ) or sorted (dirs )
311+
312+
266313def _jvm_package_index (repo , tree ):
267314 """{package name -> set of dirs declaring it}, from `package` declarations.
268315
0 commit comments