cg-code-graph 0.10.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. cg_code_graph-0.10.1.dist-info/METADATA +678 -0
  2. cg_code_graph-0.10.1.dist-info/RECORD +174 -0
  3. cg_code_graph-0.10.1.dist-info/WHEEL +5 -0
  4. cg_code_graph-0.10.1.dist-info/entry_points.txt +3 -0
  5. cg_code_graph-0.10.1.dist-info/licenses/LICENSE +21 -0
  6. cg_code_graph-0.10.1.dist-info/top_level.txt +1 -0
  7. codegraph/__init__.py +2 -0
  8. codegraph/aitools.py +129 -0
  9. codegraph/apps.py +76 -0
  10. codegraph/blindspots.py +428 -0
  11. codegraph/bridges.py +1701 -0
  12. codegraph/cli.py +725 -0
  13. codegraph/concepts.py +362 -0
  14. codegraph/config.py +559 -0
  15. codegraph/core/__init__.py +0 -0
  16. codegraph/core/cache.py +375 -0
  17. codegraph/core/detect.py +80 -0
  18. codegraph/core/extractors.py +187 -0
  19. codegraph/core/fsutil.py +61 -0
  20. codegraph/core/generated.py +575 -0
  21. codegraph/core/model.py +174 -0
  22. codegraph/core/paths.py +175 -0
  23. codegraph/core/plugin.py +160 -0
  24. codegraph/core/store.py +80 -0
  25. codegraph/core/syntax_errors.py +132 -0
  26. codegraph/coverage.py +928 -0
  27. codegraph/doctor.py +453 -0
  28. codegraph/external.py +613 -0
  29. codegraph/indexer.py +336 -0
  30. codegraph/link.py +434 -0
  31. codegraph/lint_async.py +524 -0
  32. codegraph/mcp_server.py +1303 -0
  33. codegraph/parity.py +473 -0
  34. codegraph/parity_structure.py +307 -0
  35. codegraph/payload.py +321 -0
  36. codegraph/plans.py +1285 -0
  37. codegraph/platform_scan.py +643 -0
  38. codegraph/platforms.py +1369 -0
  39. codegraph/plugins/__init__.py +0 -0
  40. codegraph/plugins/cfamily/__init__.py +0 -0
  41. codegraph/plugins/cfamily/plugin.py +930 -0
  42. codegraph/plugins/cfamily/syntax.py +881 -0
  43. codegraph/plugins/dart/__init__.py +0 -0
  44. codegraph/plugins/dart/bridges.py +345 -0
  45. codegraph/plugins/dart/extractor/bin/extract.dart +717 -0
  46. codegraph/plugins/dart/extractor/pubspec.lock +149 -0
  47. codegraph/plugins/dart/extractor/pubspec.yaml +7 -0
  48. codegraph/plugins/dart/http.py +904 -0
  49. codegraph/plugins/dart/models.py +308 -0
  50. codegraph/plugins/dart/plugin.py +625 -0
  51. codegraph/plugins/dart/program.py +907 -0
  52. codegraph/plugins/django/__init__.py +0 -0
  53. codegraph/plugins/django/extras.py +378 -0
  54. codegraph/plugins/django/models.py +508 -0
  55. codegraph/plugins/django/plugin.py +728 -0
  56. codegraph/plugins/django/schemas.py +339 -0
  57. codegraph/plugins/django/shapes.py +216 -0
  58. codegraph/plugins/django/urls.py +603 -0
  59. codegraph/plugins/express/__init__.py +0 -0
  60. codegraph/plugins/express/plugin.py +428 -0
  61. codegraph/plugins/flutter/__init__.py +0 -0
  62. codegraph/plugins/flutter/plugin.py +538 -0
  63. codegraph/plugins/kotlin/__init__.py +0 -0
  64. codegraph/plugins/kotlin/exact.py +457 -0
  65. codegraph/plugins/kotlin/plugin.py +1961 -0
  66. codegraph/plugins/kotlin/reparse.py +234 -0
  67. codegraph/plugins/laravel/__init__.py +0 -0
  68. codegraph/plugins/laravel/broadcast.py +351 -0
  69. codegraph/plugins/laravel/plugin.py +863 -0
  70. codegraph/plugins/laravel/tests.py +262 -0
  71. codegraph/plugins/laravel/values.py +728 -0
  72. codegraph/plugins/native/__init__.py +0 -0
  73. codegraph/plugins/native/gates.py +286 -0
  74. codegraph/plugins/native/runner.py +183 -0
  75. codegraph/plugins/native/scipread.py +194 -0
  76. codegraph/plugins/native/ts.py +54 -0
  77. codegraph/plugins/nest/__init__.py +0 -0
  78. codegraph/plugins/nest/plugin.py +654 -0
  79. codegraph/plugins/nextjs/__init__.py +0 -0
  80. codegraph/plugins/nextjs/plugin.py +336 -0
  81. codegraph/plugins/nuxt/__init__.py +0 -0
  82. codegraph/plugins/nuxt/plugin.py +308 -0
  83. codegraph/plugins/php/__init__.py +0 -0
  84. codegraph/plugins/php/extractor/composer.json +5 -0
  85. codegraph/plugins/php/extractor/composer.lock +76 -0
  86. codegraph/plugins/php/extractor/extract.php +743 -0
  87. codegraph/plugins/php/gating.py +573 -0
  88. codegraph/plugins/php/plugin.py +668 -0
  89. codegraph/plugins/php/strings.py +197 -0
  90. codegraph/plugins/python/__init__.py +0 -0
  91. codegraph/plugins/python/aitools.py +664 -0
  92. codegraph/plugins/python/external.py +245 -0
  93. codegraph/plugins/python/fields.py +107 -0
  94. codegraph/plugins/python/plugin.py +1733 -0
  95. codegraph/plugins/python/refs.py +485 -0
  96. codegraph/plugins/python/roots.py +412 -0
  97. codegraph/plugins/python/socketio.py +210 -0
  98. codegraph/plugins/python/subproc.py +864 -0
  99. codegraph/plugins/python/tests.py +1040 -0
  100. codegraph/plugins/python/values.py +179 -0
  101. codegraph/plugins/pyweb/__init__.py +0 -0
  102. codegraph/plugins/pyweb/plugin.py +1334 -0
  103. codegraph/plugins/pyweb/values.py +68 -0
  104. codegraph/plugins/rust/__init__.py +0 -0
  105. codegraph/plugins/rust/cargo.py +226 -0
  106. codegraph/plugins/rust/plugin.py +980 -0
  107. codegraph/plugins/rust/syntax.py +678 -0
  108. codegraph/plugins/scip/__init__.py +0 -0
  109. codegraph/plugins/scip/importer.py +129 -0
  110. codegraph/plugins/scip/scip.proto +962 -0
  111. codegraph/plugins/scip/scip_pb2.py +97 -0
  112. codegraph/plugins/stubs/__init__.py +0 -0
  113. codegraph/plugins/stubs/plugins.py +38 -0
  114. codegraph/plugins/swift/__init__.py +0 -0
  115. codegraph/plugins/swift/baseurl.py +109 -0
  116. codegraph/plugins/swift/exact.py +415 -0
  117. codegraph/plugins/swift/indexstore.py +209 -0
  118. codegraph/plugins/swift/packages.py +174 -0
  119. codegraph/plugins/swift/plugin.py +2890 -0
  120. codegraph/plugins/ts/__init__.py +0 -0
  121. codegraph/plugins/ts/baseurl.py +185 -0
  122. codegraph/plugins/ts/extractor/extract.mjs +2652 -0
  123. codegraph/plugins/ts/extractor/fw.mjs +685 -0
  124. codegraph/plugins/ts/extractor/package-lock.json +205 -0
  125. codegraph/plugins/ts/extractor/package.json +9 -0
  126. codegraph/plugins/ts/plugin.py +480 -0
  127. codegraph/plugins/tsweb/__init__.py +0 -0
  128. codegraph/plugins/tsweb/common.py +290 -0
  129. codegraph/plugins/tsweb/data.py +276 -0
  130. codegraph/presets/__init__.py +146 -0
  131. codegraph/presets/c_cpp.yaml +9 -0
  132. codegraph/presets/common.yaml +66 -0
  133. codegraph/presets/dart.yaml +9 -0
  134. codegraph/presets/django-ninja.yaml +15 -0
  135. codegraph/presets/django.yaml +25 -0
  136. codegraph/presets/djangorestframework.yaml +17 -0
  137. codegraph/presets/express.yaml +17 -0
  138. codegraph/presets/kotlin.yaml +11 -0
  139. codegraph/presets/laravel.yaml +40 -0
  140. codegraph/presets/nest.yaml +11 -0
  141. codegraph/presets/nextjs.yaml +15 -0
  142. codegraph/presets/nuxt.yaml +9 -0
  143. codegraph/presets/php.yaml +5 -0
  144. codegraph/presets/python.yaml +10 -0
  145. codegraph/presets/rust.yaml +5 -0
  146. codegraph/presets/swift.yaml +10 -0
  147. codegraph/presets/typescript.yaml +13 -0
  148. codegraph/process_runs.py +328 -0
  149. codegraph/protocols/__init__.py +299 -0
  150. codegraph/protocols/builtin.py +67 -0
  151. codegraph/protocols/matchers.py +144 -0
  152. codegraph/protocols/view.py +334 -0
  153. codegraph/query.py +2089 -0
  154. codegraph/realtime.py +260 -0
  155. codegraph/roundtrip.py +346 -0
  156. codegraph/routes.py +442 -0
  157. codegraph/starters.py +218 -0
  158. codegraph/tests_index.py +117 -0
  159. codegraph/viz/__init__.py +0 -0
  160. codegraph/viz/graph.py +369 -0
  161. codegraph/viz/server.py +198 -0
  162. codegraph/viz/static/app.css +148 -0
  163. codegraph/viz/static/app.js +1082 -0
  164. codegraph/viz/static/index.html +81 -0
  165. codegraph/viz/static/layered.js +237 -0
  166. codegraph/viz/static/vendor/VERSIONS.txt +4 -0
  167. codegraph/viz/static/vendor/cose-base.js +3214 -0
  168. codegraph/viz/static/vendor/cytoscape-fcose.js +1549 -0
  169. codegraph/viz/static/vendor/cytoscape.min.js +31 -0
  170. codegraph/viz/static/vendor/layout-base.js +5230 -0
  171. codegraph/viz/tools/package-lock.json +303 -0
  172. codegraph/viz/tools/package.json +7 -0
  173. codegraph/viz/tools/shoot.mjs +165 -0
  174. codegraph/xcode.py +251 -0
@@ -0,0 +1,457 @@
1
+ """Kotlin exact layer: compiler-resolved references from a scip-java SCIP index (SemanticDB compiler plugin).
2
+
3
+ Where the index comes from, in order:
4
+ 1. a `--scip` file whose documents include Kotlin sources (consumed here, not by the generic SCIP importer);
5
+ 2. $CODEGRAPH_KOTLIN_SCIP_FILE;
6
+ 3. running scip-java on the Gradle / Maven build when $CODEGRAPH_KOTLIN_SCIP=1 (opt-in: indexing runs the build,
7
+ which executes the project's build scripts, and writes build/ output into the project). The run is cached
8
+ like the Rust / C indexers (codegraph/plugins/native/runner.py).
9
+ Without an index the heuristic layer stays; `stats["scip"]` says why (no JDK, no scip-java, not opted in, the run
10
+ failed), and `cg coverage` reports it.
11
+
12
+ Mapping: SCIP definitions are matched to the tree-sitter declarations by (file, line of the name, name), so node ids
13
+ do not change. Every reference to a project method / function becomes a CALLS edge (a constructor reference
14
+ `Foo#<init>().` an INSTANTIATES edge) from the innermost declaration around it (a Ktor route handler lambda or
15
+ Compose page, else the function / method, else the class, else the file), confidence exact. The heuristic call edges
16
+ of the files the index covers are replaced; files it does not cover keep them. The overlap of both layers is kept
17
+ as precision / recall of the heuristic layer against the exact one (`stats["exact_vs_heuristic"]`).
18
+
19
+ Mixed Kotlin / Java modules: the Java documents of the same index (scip-java indexes both) are imported too, since
20
+ the generic SCIP importer skips an index this layer consumed. Java classes and methods become `java` nodes (ids from
21
+ the generic importer's symbol mapping, codegraph/plugins/scip/importer.py), Kotlin -> Java and Java -> Kotlin / Java
22
+ calls and constructor calls become exact edges, the Java caller being the innermost Java method / class around the
23
+ reference (SCIP enclosing ranges). `stats["java"]` counts them.
24
+ """
25
+ from __future__ import annotations
26
+
27
+ import os
28
+ import re
29
+ import shutil
30
+ import subprocess
31
+ from collections import defaultdict
32
+ from pathlib import Path
33
+
34
+ from ...core.model import EXACT, HEURISTIC
35
+ from ..native import runner, scipread
36
+
37
+ BUILD_FILES = ("build.gradle.kts", "build.gradle", "settings.gradle.kts", "settings.gradle", "pom.xml")
38
+ CALL_KINDS = ("CALLS", "INSTANTIATES")
39
+ # compiler-generated members with no declaration of their own: data class `copy` / `componentN`, enum `values` /
40
+ # `valueOf` / `entries` (`<init>` and `<anonymous object at ...>` start with "<")
41
+ SYNTHETIC = re.compile(r"copy|component\d+|values|valueOf|entries")
42
+ FACADE = re.compile(r"(?<=/)\w+Kt#(?=[^#]+\.$)") # the JVM facade class of a Kotlin file's top-level functions
43
+
44
+
45
+ def _has_kotlin_docs(path: str) -> bool:
46
+ try:
47
+ idx = scipread.load(path)
48
+ except Exception:
49
+ return False
50
+ return any(p.endswith((".kt", ".kts")) for p in idx.docs)
51
+
52
+
53
+ def find_java() -> str | None:
54
+ jh = os.environ.get("JAVA_HOME")
55
+ if jh and (Path(jh) / "bin" / "java").exists():
56
+ return str(Path(jh) / "bin" / "java")
57
+ return shutil.which("java")
58
+
59
+
60
+ def find_index(project, files: list[str]) -> tuple[Path | None, dict]:
61
+ """(SCIP path or None, info). info["status"] explains a missing index."""
62
+ for s in project.options.get("scip") or []:
63
+ if _has_kotlin_docs(s):
64
+ consumed = project.options.setdefault("scip_consumed", [])
65
+ consumed.append(s)
66
+ return Path(s), {"source": "--scip", "path": str(s)}
67
+ pre = os.environ.get("CODEGRAPH_KOTLIN_SCIP_FILE")
68
+ if pre:
69
+ if Path(pre).exists():
70
+ return Path(pre), {"source": "CODEGRAPH_KOTLIN_SCIP_FILE", "path": pre}
71
+ return None, {"status": f"CODEGRAPH_KOTLIN_SCIP_FILE={pre} does not exist"}
72
+ root = Path(project.root)
73
+ build = next((b for b in BUILD_FILES if (root / b).exists()), None)
74
+ tools = scip_java_candidates()
75
+ java = find_java()
76
+ if not build:
77
+ return None, {"status": "no Gradle / Maven build file at the project root"}
78
+ if not java:
79
+ return None, {"status": "no JDK (java not on PATH, JAVA_HOME unset)"}
80
+ if not tools:
81
+ return None, {"status": "scip-java not installed (docs/kotlin.md#exact-mode)"}
82
+ if os.environ.get("CODEGRAPH_KOTLIN_SCIP") != "1":
83
+ return None, {"status": "scip-java found but not run: indexing runs the Gradle / Maven build (the project's "
84
+ "build scripts); set CODEGRAPH_KOTLIN_SCIP=1 or pass --scip", "indexer": tools[0]}
85
+ env = dict(os.environ) # the JDK found for the run, without changing this process's environment
86
+ jbin = str(Path(java).parent)
87
+ if jbin not in env.get("PATH", "").split(os.pathsep):
88
+ env["PATH"] = jbin + os.pathsep + env.get("PATH", "")
89
+ env.setdefault("JAVA_HOME", str(Path(java).resolve().parent.parent))
90
+ kv = kotlin_version(root)
91
+ andr = android_modules(root)
92
+ if len(tools) > 1: # several scip-java releases: the one whose compiler plugin fits the build first
93
+ tools.sort(key=lambda t: not _supports(_generation(t), kv))
94
+ srcs = list(files) + [b for b in BUILD_FILES if (root / b).exists()]
95
+ timeout = int(os.environ.get("CODEGRAPH_INDEXER_TIMEOUT", "3600"))
96
+ attempts = []
97
+ for i, tool in enumerate(tools):
98
+ key = runner.fingerprint(root, srcs, f"scip-java|{tool}|{runner.tool_version(tool, ('version',))}")
99
+ path, info = runner.run_cached("kotlin", key, [tool, "index"], root, "--output", timeout, out_flag_style="eq",
100
+ env=env, notes=r"incompatible|^e: |^error: |Could not resolve|"
101
+ r"SDK location not found|NoSuchMethodError|AbstractMethodError|"
102
+ r"prefer settings repositories")
103
+ info.update(source="scip-java", build_file=build, indexer=tool)
104
+ if andr:
105
+ info["android_modules"] = andr
106
+ if kv:
107
+ info["kotlin_version"] = ".".join(map(str, kv))
108
+ if path is not None:
109
+ if attempts:
110
+ info["attempts"] = attempts
111
+ return path, info
112
+ tail = " ".join((info.get("notes") or []) + (info.get("stderr_tail") or []))
113
+ mismatch = _plugin_mismatch(tail)
114
+ attempts.append({"indexer": tool, "error": info.get("error", "?"), "plugin_mismatch": mismatch})
115
+ if not (mismatch and i + 1 < len(tools)):
116
+ break
117
+ notes = sorted(info.get("notes") or [], key=_note_rank) # the cause first
118
+ tail = " ".join(notes + (info.get("stderr_tail") or []))
119
+ last = (notes[0] if notes else
120
+ next((x.strip() for x in reversed(info.get("stderr_tail") or []) if x.strip()), ""))
121
+ last = re.sub(r"file://\S*/", "", last)
122
+ hint = ""
123
+ if _plugin_mismatch(tail):
124
+ hint = "; the scip-java Kotlin compiler plugin does not load into this Kotlin version"
125
+ if kv and kv >= (2, 2, 20):
126
+ hint += (f" ({'.'.join(map(str, kv))}: no released scip-java supports it yet; 0.12 covers Kotlin <= 2.1, "
127
+ "0.13 covers 2.2.0 - 2.2.10)")
128
+ elif kv and kv >= (2, 2):
129
+ hint += f" ({'.'.join(map(str, kv))} needs scip-java 0.13)"
130
+ elif kv:
131
+ hint += f" ({'.'.join(map(str, kv))} needs scip-java 0.12)"
132
+ hint += " (docs/kotlin.md#exact-mode)"
133
+ elif "incompatible version of Kotlin" in tail:
134
+ hint = "; dependencies need a newer Kotlin compiler than the build uses (docs/kotlin.md#exact-mode)"
135
+ elif "prefer settings repositories" in tail:
136
+ hint = ("; the build's settings forbid project repositories (dependencyResolutionManagement "
137
+ "FAIL_ON_PROJECT_REPOS) and scip-java's Gradle plugin adds one (docs/kotlin.md#exact-mode)")
138
+ if andr:
139
+ hint += (f"; Android modules ({', '.join(andr[:5])}) need the Android SDK (ANDROID_HOME) and scip-java's "
140
+ "Gradle plugin compiles no Android variant")
141
+ info["status"] = ("scip-java run failed (" + info.get("error", "?") + (f": {last[:160]}" if last else "")
142
+ + ")" + hint)
143
+ if len(attempts) > 1:
144
+ info["attempts"] = attempts
145
+ return None, info
146
+
147
+
148
+ def scip_java_candidates() -> list[str]:
149
+ """scip-java launchers: $CODEGRAPH_SCIP_JAVA (one path, or several joined by os.pathsep), else `scip-java` on
150
+ PATH / in the tool directories plus versioned installs beside it (`scip-java-0.13.1/scip-java`, `scip-java-0.13.1`),
151
+ since each release's Kotlin compiler plugin loads into a narrow range of Kotlin versions."""
152
+ v = os.environ.get("CODEGRAPH_SCIP_JAVA")
153
+ if v:
154
+ return [p for p in v.split(os.pathsep) if p and (Path(p).exists() or shutil.which(p))]
155
+ out = []
156
+ home = Path.home()
157
+ dirs = [home / "tools", home / ".local" / "bin", home / ".local" / "share" / "coursier" / "bin"]
158
+ main = runner.find_tool("CODEGRAPH_SCIP_JAVA", ["scip-java"], dirs)
159
+ if main:
160
+ out.append(main)
161
+ for d in dirs:
162
+ if not d.is_dir():
163
+ continue
164
+ for p in sorted(d.glob("scip-java-*/scip-java")) + sorted(d.glob("scip-java-*")):
165
+ if p.is_file() and os.access(p, os.X_OK):
166
+ out.append(str(p))
167
+ seen, uniq = set(), []
168
+ for p in out:
169
+ r = os.path.realpath(p)
170
+ if r not in seen:
171
+ seen.add(r)
172
+ uniq.append(p)
173
+ return uniq
174
+
175
+
176
+ _GEN: dict = {}
177
+
178
+
179
+ def _generation(tool: str) -> int:
180
+ """13 for scip-java >= 0.13 (the Kotlin rewrite: `aggregate` command, SCIP shards), else 12."""
181
+ if tool not in _GEN:
182
+ try:
183
+ r = subprocess.run([tool, "--help"], capture_output=True, text=True, timeout=120)
184
+ _GEN[tool] = 13 if "aggregate" in (r.stdout + r.stderr) else 12
185
+ except Exception:
186
+ _GEN[tool] = 12
187
+ return _GEN[tool]
188
+
189
+
190
+ def _supports(gen: int, kv: tuple | None) -> bool:
191
+ """Kotlin versions whose compiler the release's Kotlin plugin loads into (checked on Gradle builds): 0.12.x
192
+ bundles semanticdb-kotlinc for Kotlin <= 2.1; 0.13.x (built against 2.2.0) works on 2.2.0 - 2.2.10, and fails
193
+ on 2.1 (AbstractMethodError), 2.2.20+ (NoSuchMethodError) and 2.3+ (incompatible registrar)."""
194
+ if kv is None:
195
+ return gen == 12
196
+ if gen >= 13:
197
+ return (2, 2) <= kv < (2, 2, 20)
198
+ return kv < (2, 2)
199
+
200
+
201
+ def _note_rank(line: str) -> int:
202
+ if re.search(r"is incompatible with the current|NoSuchMethodError|AbstractMethodError|SDK location not found|"
203
+ r"prefer settings repositories", line):
204
+ return 0
205
+ return 1 if "incompatible" in line and "Deprecated Gradle" not in line else 2
206
+
207
+
208
+ def _plugin_mismatch(text: str) -> bool:
209
+ return bool(re.search(r"semanticdb_kotlinc|kotlinc\.AnalyzerRegistrar|NoSuchMethodError: '[^']*kotlin|"
210
+ r"AbstractMethodError: [^\n]*kotlin", text))
211
+
212
+
213
+ _KV = [re.compile(r'kotlin\(\s*"[\w-]+"\s*\)\s*version\s*"(\d+\.\d+(?:\.\d+)?)'),
214
+ re.compile(r'id\s*\(?\s*["\']org\.jetbrains\.kotlin\.[\w.-]+["\']\s*\)?\s*version\s*["\'](\d+\.\d+(?:\.\d+)?)'),
215
+ re.compile(r'kotlin-gradle-plugin:(\d+\.\d+(?:\.\d+)?)'),
216
+ re.compile(r'<kotlin\.version>(\d+\.\d+(?:\.\d+)?)'),
217
+ re.compile(r'^\s*kotlin(?:[-_]?[vV]ersion)?\s*=\s*["\']?(\d+\.\d+(?:\.\d+)?)', re.M)]
218
+ KV_FILES = ("build.gradle.kts", "build.gradle", "settings.gradle.kts", "settings.gradle", "gradle/libs.versions.toml",
219
+ "gradle.properties", "pom.xml", "buildSrc/build.gradle.kts")
220
+
221
+
222
+ ANDROID_PLUGIN = re.compile(r"com\.android\.(?:application|library|dynamic-feature|test)|"
223
+ r"plugins\.android\.(?:application|library|dynamic\.feature|test)\b")
224
+
225
+
226
+ def android_modules(root: Path) -> list[str]:
227
+ """Gradle modules (up to two levels deep) that apply the Android Gradle plugin."""
228
+ out = []
229
+ for pat in ("*/build.gradle.kts", "*/build.gradle", "*/*/build.gradle.kts", "*/*/build.gradle"):
230
+ for f in sorted(root.glob(pat)):
231
+ rel = f.parent.relative_to(root).as_posix()
232
+ if rel.startswith(("build", ".", "buildSrc")) or rel in out:
233
+ continue
234
+ try:
235
+ if ANDROID_PLUGIN.search(f.read_text(errors="replace")):
236
+ out.append(rel)
237
+ except OSError:
238
+ pass
239
+ return out
240
+
241
+
242
+ def skipped_modules(modules: list[str] | None, doc_paths) -> list[dict]:
243
+ """Android modules none of whose sources the index contains."""
244
+ paths = list(doc_paths)
245
+ return [{"module": m, "reason": "Android module: scip-java's Gradle plugin compiles no Android variant "
246
+ "(docs/kotlin.md#exact-mode)"}
247
+ for m in modules or () if not any(p.startswith(m + "/") for p in paths)]
248
+
249
+
250
+ def kotlin_version(root: Path) -> tuple | None:
251
+ """The Kotlin (Gradle plugin) version the build declares, from the root build files / version catalog."""
252
+ for f in KV_FILES:
253
+ try:
254
+ t = (root / f).read_text(errors="replace")
255
+ except OSError:
256
+ continue
257
+ for rx in _KV:
258
+ m = rx.search(t)
259
+ if m:
260
+ return tuple(int(x) for x in m.group(1).split("."))
261
+ return None
262
+
263
+
264
+ class ExactLayer:
265
+ def __init__(self, plugin):
266
+ self.p = plugin
267
+ self.b = plugin.b
268
+
269
+ def apply(self, path: Path, st: dict) -> bool:
270
+ idx = scipread.load(path)
271
+ self.doc_paths = list(idx.docs)
272
+ kdocs = {p: d for p, d in idx.docs.items() if p.endswith((".kt", ".kts"))}
273
+ st["scip_documents"] = len(kdocs)
274
+ if not kdocs:
275
+ return False
276
+ decl_pos = {}
277
+ by_file = defaultdict(list)
278
+ for d in self._all_decls(): # overloads share a node id but keep their own Decl
279
+ decl_pos[(d.file, d.name_line or d.line, d.name)] = d.id
280
+ by_file[d.file].append(d)
281
+ # symbol -> node id
282
+ sym = {}
283
+ matched = unmatched = 0
284
+ # Kotlin properties: the term `C#x.` and its synthetic `getX()` / `setX()` share the definition position
285
+ term_pos = {(rel, o.line, o.col) for s, defs in idx.defs.items() if s.endswith(".") and not s.endswith(").")
286
+ for rel, o in defs}
287
+ for s, defs in idx.defs.items():
288
+ if s.endswith((")", "]")): # parameters `m().(x)`, type parameters `C#[T]`
289
+ continue
290
+ for rel, o in defs:
291
+ if rel not in kdocs:
292
+ continue
293
+ dd = scipread.descriptors(s)
294
+ if not dd or not dd[1]:
295
+ continue
296
+ name, suf = dd[1][-1]
297
+ nid = decl_pos.get((rel, o.line + 1, name))
298
+ if nid is None and suf in ("(", "#"):
299
+ # a declaration whose name sits on another line than the tree-sitter node expects
300
+ for d in by_file.get(rel, ()):
301
+ if d.name == name and d.line <= o.line + 1 <= d.end and (suf == "#") == (d.kind == "class"):
302
+ nid = d.id
303
+ break
304
+ if nid is None:
305
+ if (suf in ("(", "#") and not name.startswith("<") and not SYNTHETIC.fullmatch(name)
306
+ and (rel, o.line, o.col) not in term_pos):
307
+ unmatched += 1
308
+ continue
309
+ sym[s] = nid
310
+ matched += 1
311
+ st["scip_defs_matched"] = matched
312
+ st["scip_defs_unmatched"] = unmatched
313
+ w = scipread.health_warning(Path(path).name, idx.occurrences, idx.positioned, None if not matched + unmatched
314
+ else matched + unmatched, matched, "the Kotlin declarations")
315
+ if w:
316
+ st["scip_warning"] = w
317
+ jdocs = {p: d for p, d in idx.docs.items() if p.endswith(".java")}
318
+ jowners = self._java_nodes(jdocs, sym, st) if jdocs else {}
319
+ covered = set(kdocs)
320
+ heur = self._remove_heuristic(covered)
321
+ exact = set()
322
+ owners = self._owner_index(covered)
323
+ owners.update(jowners)
324
+ for rel, doc in list(kdocs.items()) + list(jdocs.items()):
325
+ java = rel in jdocs
326
+ # a property access `x.pets` is reported as the term `C#pets.` plus the synthetic accessor `C#getPets().`,
327
+ # which collides with a declared `fun getPets()`: accessors at a property reference are not calls
328
+ prop_at = {(o.line, o.col) for o in doc.occs if not (o.roles & scipread.DEFINITION)
329
+ and o.symbol.endswith(".") and not o.symbol.endswith(").")}
330
+ for o in doc.occs:
331
+ if o.roles & scipread.DEFINITION:
332
+ continue
333
+ if o.symbol.endswith((")", "]")) or (o.line, o.col) in prop_at:
334
+ continue
335
+ dd = scipread.descriptors(o.symbol)
336
+ if not dd or not dd[1]:
337
+ continue
338
+ name, suf = dd[1][-1]
339
+ if suf != "(":
340
+ continue
341
+ if name == "<init>":
342
+ cls_sym = o.symbol[:o.symbol.rfind("`<init>`")] if "`<init>`" in o.symbol else None
343
+ dst = sym.get(cls_sym) if cls_sym else None
344
+ kind = "INSTANTIATES"
345
+ else:
346
+ dst = sym.get(o.symbol)
347
+ if dst is None and java: # a top-level Kotlin function seen from Java: `pkg/FileKt#f().`
348
+ dst = sym.get(FACADE.sub("", o.symbol))
349
+ kind = "CALLS"
350
+ if dst is None:
351
+ if java:
352
+ st["java"]["references_external"] += 1
353
+ else:
354
+ st["scip_refs_external"] = st.get("scip_refs_external", 0) + 1
355
+ continue
356
+ src = self._owner(owners, rel, o.line + 1)
357
+ if src is None or src == dst:
358
+ continue
359
+ self.b.add_edge(src, dst, kind, rel, o.line + 1, EXACT, source="scip")
360
+ if kind == "INSTANTIATES":
361
+ self.b.add_edge(src, dst, "USES_TYPE", rel, o.line + 1, EXACT, how="constructor call", source="scip")
362
+ if java:
363
+ st["java"]["references"] += 1
364
+ continue
365
+ exact.add((src, dst, kind))
366
+ st["scip_references"] = st.get("scip_references", 0) + 1
367
+ agree = len(heur & exact)
368
+ st["exact_vs_heuristic"] = {
369
+ "heuristic_edges": len(heur), "exact_edges": len(exact), "agree": agree,
370
+ "precision": round(agree / len(heur), 3) if heur else None,
371
+ "recall": round(agree / len(exact), 3) if exact else None,
372
+ # candidate edges (receiver type unknown, one per same-name method, #83) are compared on their own
373
+ "candidate_edges": len(self.candidates), "candidate_agree": len(self.candidates & exact)}
374
+ seen = set(getattr(self.p, "file_report", {}).get("seen") or covered)
375
+ st["scip_files"] = len({f for f in covered & seen if f.endswith(".kt")})
376
+ return True
377
+
378
+ def _java_nodes(self, jdocs: dict, sym: dict, st: dict) -> dict:
379
+ """Nodes for the Java classes / methods the index defines (added to `sym`); returns their owner ranges."""
380
+ from ..scip.importer import to_node
381
+ js = st["java"] = {"documents": len(jdocs), "classes": 0, "methods": 0, "references": 0,
382
+ "references_external": 0}
383
+ owners = defaultdict(list)
384
+ for rel, doc in jdocs.items():
385
+ fid = self.b.add_node("file", f"java:{rel}", name=rel, file=rel, line=1, lang="java")
386
+ for o in doc.occs:
387
+ if not (o.roles & scipread.DEFINITION) or o.symbol.endswith((")", "]")):
388
+ continue
389
+ n = to_node(o.symbol, "java")
390
+ if not n or n[0] not in ("class", "method") or n[2] == "<init>":
391
+ continue
392
+ kind, key, name = n
393
+ sl, el = (o.enclosing[0] + 1, o.enclosing[1] + 1) if o.enclosing else (o.line + 1, o.line + 1)
394
+ pkg = key.rsplit(".", 1)[0] if kind == "class" and "." in key else None
395
+ nid = self.b.add_node(kind, key, name=name, fqn=key, file=rel, line=o.line + 1, end_line=el,
396
+ lang="java", module=pkg, attrs={"scip_symbol": o.symbol, "source": "scip"})
397
+ sym[o.symbol] = nid
398
+ js["classes" if kind == "class" else "methods"] += 1
399
+ parent = f"class:{key.split('::')[0]}" if kind == "method" else None
400
+ outer = to_node(o.symbol[:o.symbol.rstrip('#').rfind('#') + 1], "java") if kind == "class" else None
401
+ if parent is None and outer and outer[0] == "class" and outer[1] != key:
402
+ parent = f"class:{outer[1]}"
403
+ self.b.add_edge(parent or fid, nid, "CONTAINS", rel, o.line + 1, EXACT)
404
+ owners[rel].append((sl, el, 1 if kind == "class" else 0, nid))
405
+ owners.setdefault(rel, [])
406
+ return owners
407
+
408
+ def _remove_heuristic(self, covered: set) -> set:
409
+ """Drop the heuristic call edges made in covered files; returns their (src, dst, kind) set, without the
410
+ candidate edges (self.candidates)."""
411
+ out, cand = set(), set()
412
+ for key, e in list(self.b.edges.items()):
413
+ if e.confidence != HEURISTIC or e.file not in covered:
414
+ continue
415
+ if e.attrs.get("property"):
416
+ continue # property reads / writes (#89): scip-java reports them as accessors, not mapped yet
417
+
418
+ if e.kind in CALL_KINDS or (e.kind == "USES_TYPE" and e.attrs.get("how") == "constructor call"):
419
+ n = self.b.nodes.get(e.src)
420
+ if n is not None and n.lang == "kotlin":
421
+ if e.kind in CALL_KINDS:
422
+ (cand if e.attrs.get("binding") == "candidate" else out).add((e.src, e.dst, e.kind))
423
+ del self.b.edges[key]
424
+ self.candidates = cand
425
+ return out
426
+
427
+ def _all_decls(self) -> list:
428
+ seen, out = set(), []
429
+ for d in list(self.p.decls.values()) + [d for ds in self.p.by_name.values() for d in ds]:
430
+ if id(d) not in seen:
431
+ seen.add(id(d))
432
+ out.append(d)
433
+ return out
434
+
435
+ def _owner_index(self, covered: set) -> dict:
436
+ """Per file, the line ranges of functions / methods / classes (each overload separately) and pages."""
437
+ idx = defaultdict(list)
438
+ for d in self._all_decls():
439
+ if d.file in covered and d.line and d.id in self.b.nodes:
440
+ idx[d.file].append((d.line, d.end or d.line, 0 if d.kind != "class" else 1, d.id))
441
+ decl_ids = {d.id for d in self._all_decls()}
442
+ for nid, n in self.b.nodes.items(): # pages and route-handler lambdas own the calls inside them
443
+ if (n.lang == "kotlin" and n.kind in ("page", "function") and nid not in decl_ids and n.file in covered
444
+ and n.line):
445
+ idx[n.file].append((n.line, n.end_line or n.line, 0, nid))
446
+ return idx
447
+
448
+ def _owner(self, owners: dict, rel: str, line: int) -> str:
449
+ best = None
450
+ for sl, el, cls, nid in owners.get(rel, ()):
451
+ if sl <= line <= el:
452
+ k = (cls, el - sl)
453
+ if best is None or k < best[0]:
454
+ best = (k, nid)
455
+ if best:
456
+ return best[1]
457
+ return f"file:java:{rel}" if rel.endswith(".java") else f"file:kotlin:{rel}"