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.
- cg_code_graph-0.10.1.dist-info/METADATA +678 -0
- cg_code_graph-0.10.1.dist-info/RECORD +174 -0
- cg_code_graph-0.10.1.dist-info/WHEEL +5 -0
- cg_code_graph-0.10.1.dist-info/entry_points.txt +3 -0
- cg_code_graph-0.10.1.dist-info/licenses/LICENSE +21 -0
- cg_code_graph-0.10.1.dist-info/top_level.txt +1 -0
- codegraph/__init__.py +2 -0
- codegraph/aitools.py +129 -0
- codegraph/apps.py +76 -0
- codegraph/blindspots.py +428 -0
- codegraph/bridges.py +1701 -0
- codegraph/cli.py +725 -0
- codegraph/concepts.py +362 -0
- codegraph/config.py +559 -0
- codegraph/core/__init__.py +0 -0
- codegraph/core/cache.py +375 -0
- codegraph/core/detect.py +80 -0
- codegraph/core/extractors.py +187 -0
- codegraph/core/fsutil.py +61 -0
- codegraph/core/generated.py +575 -0
- codegraph/core/model.py +174 -0
- codegraph/core/paths.py +175 -0
- codegraph/core/plugin.py +160 -0
- codegraph/core/store.py +80 -0
- codegraph/core/syntax_errors.py +132 -0
- codegraph/coverage.py +928 -0
- codegraph/doctor.py +453 -0
- codegraph/external.py +613 -0
- codegraph/indexer.py +336 -0
- codegraph/link.py +434 -0
- codegraph/lint_async.py +524 -0
- codegraph/mcp_server.py +1303 -0
- codegraph/parity.py +473 -0
- codegraph/parity_structure.py +307 -0
- codegraph/payload.py +321 -0
- codegraph/plans.py +1285 -0
- codegraph/platform_scan.py +643 -0
- codegraph/platforms.py +1369 -0
- codegraph/plugins/__init__.py +0 -0
- codegraph/plugins/cfamily/__init__.py +0 -0
- codegraph/plugins/cfamily/plugin.py +930 -0
- codegraph/plugins/cfamily/syntax.py +881 -0
- codegraph/plugins/dart/__init__.py +0 -0
- codegraph/plugins/dart/bridges.py +345 -0
- codegraph/plugins/dart/extractor/bin/extract.dart +717 -0
- codegraph/plugins/dart/extractor/pubspec.lock +149 -0
- codegraph/plugins/dart/extractor/pubspec.yaml +7 -0
- codegraph/plugins/dart/http.py +904 -0
- codegraph/plugins/dart/models.py +308 -0
- codegraph/plugins/dart/plugin.py +625 -0
- codegraph/plugins/dart/program.py +907 -0
- codegraph/plugins/django/__init__.py +0 -0
- codegraph/plugins/django/extras.py +378 -0
- codegraph/plugins/django/models.py +508 -0
- codegraph/plugins/django/plugin.py +728 -0
- codegraph/plugins/django/schemas.py +339 -0
- codegraph/plugins/django/shapes.py +216 -0
- codegraph/plugins/django/urls.py +603 -0
- codegraph/plugins/express/__init__.py +0 -0
- codegraph/plugins/express/plugin.py +428 -0
- codegraph/plugins/flutter/__init__.py +0 -0
- codegraph/plugins/flutter/plugin.py +538 -0
- codegraph/plugins/kotlin/__init__.py +0 -0
- codegraph/plugins/kotlin/exact.py +457 -0
- codegraph/plugins/kotlin/plugin.py +1961 -0
- codegraph/plugins/kotlin/reparse.py +234 -0
- codegraph/plugins/laravel/__init__.py +0 -0
- codegraph/plugins/laravel/broadcast.py +351 -0
- codegraph/plugins/laravel/plugin.py +863 -0
- codegraph/plugins/laravel/tests.py +262 -0
- codegraph/plugins/laravel/values.py +728 -0
- codegraph/plugins/native/__init__.py +0 -0
- codegraph/plugins/native/gates.py +286 -0
- codegraph/plugins/native/runner.py +183 -0
- codegraph/plugins/native/scipread.py +194 -0
- codegraph/plugins/native/ts.py +54 -0
- codegraph/plugins/nest/__init__.py +0 -0
- codegraph/plugins/nest/plugin.py +654 -0
- codegraph/plugins/nextjs/__init__.py +0 -0
- codegraph/plugins/nextjs/plugin.py +336 -0
- codegraph/plugins/nuxt/__init__.py +0 -0
- codegraph/plugins/nuxt/plugin.py +308 -0
- codegraph/plugins/php/__init__.py +0 -0
- codegraph/plugins/php/extractor/composer.json +5 -0
- codegraph/plugins/php/extractor/composer.lock +76 -0
- codegraph/plugins/php/extractor/extract.php +743 -0
- codegraph/plugins/php/gating.py +573 -0
- codegraph/plugins/php/plugin.py +668 -0
- codegraph/plugins/php/strings.py +197 -0
- codegraph/plugins/python/__init__.py +0 -0
- codegraph/plugins/python/aitools.py +664 -0
- codegraph/plugins/python/external.py +245 -0
- codegraph/plugins/python/fields.py +107 -0
- codegraph/plugins/python/plugin.py +1733 -0
- codegraph/plugins/python/refs.py +485 -0
- codegraph/plugins/python/roots.py +412 -0
- codegraph/plugins/python/socketio.py +210 -0
- codegraph/plugins/python/subproc.py +864 -0
- codegraph/plugins/python/tests.py +1040 -0
- codegraph/plugins/python/values.py +179 -0
- codegraph/plugins/pyweb/__init__.py +0 -0
- codegraph/plugins/pyweb/plugin.py +1334 -0
- codegraph/plugins/pyweb/values.py +68 -0
- codegraph/plugins/rust/__init__.py +0 -0
- codegraph/plugins/rust/cargo.py +226 -0
- codegraph/plugins/rust/plugin.py +980 -0
- codegraph/plugins/rust/syntax.py +678 -0
- codegraph/plugins/scip/__init__.py +0 -0
- codegraph/plugins/scip/importer.py +129 -0
- codegraph/plugins/scip/scip.proto +962 -0
- codegraph/plugins/scip/scip_pb2.py +97 -0
- codegraph/plugins/stubs/__init__.py +0 -0
- codegraph/plugins/stubs/plugins.py +38 -0
- codegraph/plugins/swift/__init__.py +0 -0
- codegraph/plugins/swift/baseurl.py +109 -0
- codegraph/plugins/swift/exact.py +415 -0
- codegraph/plugins/swift/indexstore.py +209 -0
- codegraph/plugins/swift/packages.py +174 -0
- codegraph/plugins/swift/plugin.py +2890 -0
- codegraph/plugins/ts/__init__.py +0 -0
- codegraph/plugins/ts/baseurl.py +185 -0
- codegraph/plugins/ts/extractor/extract.mjs +2652 -0
- codegraph/plugins/ts/extractor/fw.mjs +685 -0
- codegraph/plugins/ts/extractor/package-lock.json +205 -0
- codegraph/plugins/ts/extractor/package.json +9 -0
- codegraph/plugins/ts/plugin.py +480 -0
- codegraph/plugins/tsweb/__init__.py +0 -0
- codegraph/plugins/tsweb/common.py +290 -0
- codegraph/plugins/tsweb/data.py +276 -0
- codegraph/presets/__init__.py +146 -0
- codegraph/presets/c_cpp.yaml +9 -0
- codegraph/presets/common.yaml +66 -0
- codegraph/presets/dart.yaml +9 -0
- codegraph/presets/django-ninja.yaml +15 -0
- codegraph/presets/django.yaml +25 -0
- codegraph/presets/djangorestframework.yaml +17 -0
- codegraph/presets/express.yaml +17 -0
- codegraph/presets/kotlin.yaml +11 -0
- codegraph/presets/laravel.yaml +40 -0
- codegraph/presets/nest.yaml +11 -0
- codegraph/presets/nextjs.yaml +15 -0
- codegraph/presets/nuxt.yaml +9 -0
- codegraph/presets/php.yaml +5 -0
- codegraph/presets/python.yaml +10 -0
- codegraph/presets/rust.yaml +5 -0
- codegraph/presets/swift.yaml +10 -0
- codegraph/presets/typescript.yaml +13 -0
- codegraph/process_runs.py +328 -0
- codegraph/protocols/__init__.py +299 -0
- codegraph/protocols/builtin.py +67 -0
- codegraph/protocols/matchers.py +144 -0
- codegraph/protocols/view.py +334 -0
- codegraph/query.py +2089 -0
- codegraph/realtime.py +260 -0
- codegraph/roundtrip.py +346 -0
- codegraph/routes.py +442 -0
- codegraph/starters.py +218 -0
- codegraph/tests_index.py +117 -0
- codegraph/viz/__init__.py +0 -0
- codegraph/viz/graph.py +369 -0
- codegraph/viz/server.py +198 -0
- codegraph/viz/static/app.css +148 -0
- codegraph/viz/static/app.js +1082 -0
- codegraph/viz/static/index.html +81 -0
- codegraph/viz/static/layered.js +237 -0
- codegraph/viz/static/vendor/VERSIONS.txt +4 -0
- codegraph/viz/static/vendor/cose-base.js +3214 -0
- codegraph/viz/static/vendor/cytoscape-fcose.js +1549 -0
- codegraph/viz/static/vendor/cytoscape.min.js +31 -0
- codegraph/viz/static/vendor/layout-base.js +5230 -0
- codegraph/viz/tools/package-lock.json +303 -0
- codegraph/viz/tools/package.json +7 -0
- codegraph/viz/tools/shoot.mjs +165 -0
- 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}"
|