Skip to content

Commit a2750e3

Browse files
authored
feat: Java/Kotlin import edges via longest package-prefix resolution (issue-2, PR 2/3) (#31)
1 parent 336bff9 commit a2750e3

2 files changed

Lines changed: 108 additions & 0 deletions

File tree

src/repokg/deps.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@
3737
JVM_BUILD_FILES = ("pom.xml", "build.gradle", "build.gradle.kts")
3838
# `package a.b.c;` (Java) / `package a.b.c` (Kotlin, no semicolon).
3939
JVM_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)
4043
JVM_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+
266313
def _jvm_package_index(repo, tree):
267314
"""{package name -> set of dirs declaring it}, from `package` declarations.
268315

tests/test_deps_jvm.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,64 @@ def test_default_package_files_ignored(self):
6161

6262
if __name__ == "__main__":
6363
unittest.main()
64+
65+
66+
class TestJVMEdges(unittest.TestCase):
67+
def setUp(self):
68+
self.tmp = tempfile.TemporaryDirectory()
69+
self.repo = self.tmp.name
70+
write(self.repo, "core/src/main/java/com/acme/core/Engine.java",
71+
"package com.acme.core;\npublic class Engine { public static void run() {} }\n")
72+
write(self.repo, "core/src/main/java/com/acme/core/util/Str.java",
73+
"package com.acme.core.util;\npublic class Str {}\n")
74+
75+
def tearDown(self):
76+
self.tmp.cleanup()
77+
78+
def edges(self):
79+
from repokg.deps import collect
80+
return {(e["from"], e["to"], e["lang"]): e["count"]
81+
for e in collect(self.repo) if e["lang"] in ("Java", "Kotlin")}
82+
83+
def test_java_import_static_wildcard_and_externals(self):
84+
write(self.repo, "api/src/main/java/com/acme/api/Handler.java",
85+
"package com.acme.api;\n"
86+
"import java.util.List;\n" # external -> dropped
87+
"import com.acme.core.Engine;\n" # class import
88+
"import static com.acme.core.Engine.run;\n" # static member
89+
"import com.acme.core.util.*;\n" # wildcard = package itself
90+
"public class Handler {}\n")
91+
self.assertEqual(self.edges(), {
92+
("api/src/main/java/com/acme/api",
93+
"core/src/main/java/com/acme/core", "Java"): 2,
94+
("api/src/main/java/com/acme/api",
95+
"core/src/main/java/com/acme/core/util", "Java"): 1,
96+
})
97+
98+
def test_kotlin_import_with_alias(self):
99+
write(self.repo, "api/src/main/kotlin/Handlers.kt",
100+
"package com.acme.api\n"
101+
"import com.acme.core.Engine as E\n"
102+
"import kotlin.collections.listOf\n"
103+
"fun handle() = E.run()\n")
104+
self.assertEqual(self.edges(), {
105+
("api/src/main/kotlin",
106+
"core/src/main/java/com/acme/core", "Kotlin"): 1,
107+
})
108+
109+
def test_test_source_root_not_targeted_when_main_exists(self):
110+
# same package declared in a test root must not attract edges
111+
write(self.repo, "core/src/test/java/com/acme/core/EngineTest.java",
112+
"package com.acme.core;\nclass EngineTest {}\n")
113+
write(self.repo, "api/src/main/java/com/acme/api/H.java",
114+
"package com.acme.api;\nimport com.acme.core.Engine;\nclass H {}\n")
115+
edges = self.edges()
116+
self.assertIn(("api/src/main/java/com/acme/api",
117+
"core/src/main/java/com/acme/core", "Java"), edges)
118+
self.assertNotIn(("api/src/main/java/com/acme/api",
119+
"core/src/test/java/com/acme/core", "Java"), edges)
120+
121+
def test_same_package_import_no_self_edge(self):
122+
write(self.repo, "core/src/main/java/com/acme/core/Other.java",
123+
"package com.acme.core;\nimport com.acme.core.Engine;\nclass Other {}\n")
124+
self.assertEqual(self.edges(), {})

0 commit comments

Comments
 (0)