codebase-explorer 2.2.0__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.
- cbe/__init__.py +9 -0
- cbe/__main__.py +4 -0
- cbe/aggregation.py +183 -0
- cbe/api_completion.py +410 -0
- cbe/batching.py +283 -0
- cbe/catalog_query.py +376 -0
- cbe/checks.py +390 -0
- cbe/cli.py +434 -0
- cbe/concrete_facts.py +321 -0
- cbe/data/LICENSE.tiktoken +30 -0
- cbe/data/o200k_base.tiktoken +199998 -0
- cbe/declarations.py +537 -0
- cbe/docs.py +929 -0
- cbe/generate.py +316 -0
- cbe/grammars.py +741 -0
- cbe/graph.py +857 -0
- cbe/graph_backends/__init__.py +43 -0
- cbe/graph_backends/_calls.py +262 -0
- cbe/graph_backends/_paths.py +42 -0
- cbe/graph_backends/identity.py +93 -0
- cbe/graph_backends/imports.py +256 -0
- cbe/graph_backends/joern.py +1021 -0
- cbe/graph_backends/resolution.py +288 -0
- cbe/graph_backends/toolchain.py +396 -0
- cbe/grounding.py +57 -0
- cbe/host_run.py +1752 -0
- cbe/model_settings.py +80 -0
- cbe/projection.py +305 -0
- cbe/prompts.py +316 -0
- cbe/records.py +200 -0
- cbe/skeleton.py +474 -0
- cbe/source_context.py +709 -0
- cbe/symbols.py +136 -0
- cbe/token_budget.py +68 -0
- cbe/treename.py +398 -0
- cbe/usage.py +300 -0
- cbe_install/__init__.py +11 -0
- cbe_install/cli.py +434 -0
- cbe_install/resources/agents/cbe-writer.md +16 -0
- cbe_install/resources/codebase-explorer/SKILL.md +101 -0
- cbe_install/resources/codebase-explorer/references/claude-code.md +63 -0
- cbe_install/resources/codebase-explorer/references/codex.md +37 -0
- codebase_explorer-2.2.0.dist-info/METADATA +233 -0
- codebase_explorer-2.2.0.dist-info/RECORD +47 -0
- codebase_explorer-2.2.0.dist-info/WHEEL +4 -0
- codebase_explorer-2.2.0.dist-info/entry_points.txt +3 -0
- codebase_explorer-2.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
"""Find and install the call-graph analyzer toolchain: Joern and Java.
|
|
2
|
+
|
|
3
|
+
``cbe graph setup`` downloads a pinned Joern release and a Temurin JRE into
|
|
4
|
+
the user cache directory. Every download is checked against SHA-256 values
|
|
5
|
+
pinned below before anything is unpacked. Nothing here changes an installed
|
|
6
|
+
Joern or Java outside the cache directory.
|
|
7
|
+
|
|
8
|
+
Lookup order used by ``cbe graph build``:
|
|
9
|
+
|
|
10
|
+
* Joern: ``--joern-home``, ``CBE_JOERN_HOME``, the cache directory, ``joern-parse`` on ``PATH``.
|
|
11
|
+
* Java: ``JAVA_HOME``, ``java`` on ``PATH``, the cache directory.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import hashlib
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import platform
|
|
20
|
+
import re
|
|
21
|
+
import shutil
|
|
22
|
+
import subprocess
|
|
23
|
+
import sys
|
|
24
|
+
import tarfile
|
|
25
|
+
import tempfile
|
|
26
|
+
import time
|
|
27
|
+
import urllib.request
|
|
28
|
+
import zipfile
|
|
29
|
+
from dataclasses import dataclass
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
JOERN_VERSION = "4.0.654"
|
|
33
|
+
JAVA_VERSION = "21.0.13+9"
|
|
34
|
+
MIN_JAVA_MAJOR = 21
|
|
35
|
+
|
|
36
|
+
_JOERN_BASE = f"https://github.com/joernio/joern/releases/download/v{JOERN_VERSION}/"
|
|
37
|
+
_JAVA_BASE = "https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.13%2B9/"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True)
|
|
41
|
+
class Asset:
|
|
42
|
+
"""One pinned download. ``checksums`` maps a hash name to its hex digest."""
|
|
43
|
+
|
|
44
|
+
name: str
|
|
45
|
+
url: str
|
|
46
|
+
size: int
|
|
47
|
+
checksums: dict
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# Joern: the sha512 values are the upstream ``<asset>.sha512`` files; the sha256
|
|
51
|
+
# values are the digests GitHub computed for the release assets. Java: the
|
|
52
|
+
# sha256 values are the ``checksum`` that the Adoptium API reports. The
|
|
53
|
+
# linux-x86_64 Joern zip was also hashed after a real download.
|
|
54
|
+
PINNED: dict[str, dict[str, Asset]] = {
|
|
55
|
+
"linux-x86_64": {
|
|
56
|
+
"joern": Asset(
|
|
57
|
+
"joern-cli-linux-x86_64.zip", _JOERN_BASE + "joern-cli-linux-x86_64.zip", 1798080877,
|
|
58
|
+
{"sha256": "830539ea998708362e4c479afea895a7dbfa90ed70537b38c84e53183e6e1fcd",
|
|
59
|
+
"sha512": "706ec7cdc0c6c554b454fc7b83d3ba35ff47b40f9c9b74e041fac61e0b54902e"
|
|
60
|
+
"8d2a394b71b2a7cb7d1eb5ac9733f5442bd7ba1e6354a8e59f010e01e0377091"}),
|
|
61
|
+
"java": Asset(
|
|
62
|
+
"OpenJDK21U-jre_x64_linux_hotspot_21.0.13_9.tar.gz",
|
|
63
|
+
_JAVA_BASE + "OpenJDK21U-jre_x64_linux_hotspot_21.0.13_9.tar.gz", 52181297,
|
|
64
|
+
{"sha256": "a535a1df9c6284d9618872984e026ae0352d1f7e9e0605425c0e6114f47351dd"}),
|
|
65
|
+
},
|
|
66
|
+
"macos-arm64": {
|
|
67
|
+
"joern": Asset(
|
|
68
|
+
"joern-cli-macos-arm64.zip", _JOERN_BASE + "joern-cli-macos-arm64.zip", 1723620390,
|
|
69
|
+
{"sha256": "4b4634dd1f5ad71eda91a9c4ca8baa18716f4cb03009de24ce1b1bc827401c97",
|
|
70
|
+
"sha512": "779ca311bb0c6cef3130b57862b4f996ce3fabdc1f606a17b42cb1048dc3c1a1"
|
|
71
|
+
"d8f828d9ca70ba0f6f9bbf5adc79d8f4ac2c9069ad1505ddf42d563d999dc839"}),
|
|
72
|
+
"java": Asset(
|
|
73
|
+
"OpenJDK21U-jre_aarch64_mac_hotspot_21.0.13_9.tar.gz",
|
|
74
|
+
_JAVA_BASE + "OpenJDK21U-jre_aarch64_mac_hotspot_21.0.13_9.tar.gz", 47935740,
|
|
75
|
+
{"sha256": "225bab0a09491ef06835488cd942f5d2befa9a0410a2484f0c8bbcf7506ec09a"}),
|
|
76
|
+
},
|
|
77
|
+
"macos-x86_64": {
|
|
78
|
+
"joern": Asset(
|
|
79
|
+
"joern-cli-macos-x86_64.zip", _JOERN_BASE + "joern-cli-macos-x86_64.zip", 1729326215,
|
|
80
|
+
{"sha256": "de66af2becda50080f35d120d5f723350687608f1f25b6ce73a2016467f53be0",
|
|
81
|
+
"sha512": "7c66ddf9848283631833bc98e8829bd7bb6d48dd1dd1040dc2f1a4279458898d"
|
|
82
|
+
"858be909c41a09c37c438ed095951e53aa056a7ec7477d1e83c16becca4f9fec"}),
|
|
83
|
+
"java": Asset(
|
|
84
|
+
"OpenJDK21U-jre_x64_mac_hotspot_21.0.13_9.tar.gz",
|
|
85
|
+
_JAVA_BASE + "OpenJDK21U-jre_x64_mac_hotspot_21.0.13_9.tar.gz", 41902098,
|
|
86
|
+
{"sha256": "c909674b1c10c261b9a3fb7e9c0efebd303e33f88a4fe84be7135b5e2d711f89"}),
|
|
87
|
+
},
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
_MANUAL = (f"Install it by hand: download joern-cli-<platform>.zip for Joern {JOERN_VERSION} from "
|
|
91
|
+
f"https://github.com/joernio/joern/releases/tag/v{JOERN_VERSION}, unzip it, and pass the "
|
|
92
|
+
"joern-cli directory as --joern-home (or set CBE_JOERN_HOME). Joern needs Java "
|
|
93
|
+
f"{MIN_JAVA_MAJOR} or newer on PATH or in JAVA_HOME.")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class SetupError(ValueError):
|
|
97
|
+
"""A setup step failed; the message says what to do next."""
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def platform_key() -> str | None:
|
|
101
|
+
system, machine = platform.system(), platform.machine().lower()
|
|
102
|
+
arch = {"x86_64": "x86_64", "amd64": "x86_64", "arm64": "arm64", "aarch64": "arm64"}.get(machine)
|
|
103
|
+
name = {"Linux": "linux", "Darwin": "macos"}.get(system)
|
|
104
|
+
key = f"{name}-{arch}"
|
|
105
|
+
return key if key in PINNED else None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def cache_dir() -> Path:
|
|
109
|
+
"""``<user cache>/cbe/tools``: XDG on Linux, ``~/Library/Caches`` on macOS."""
|
|
110
|
+
if platform.system() == "Darwin":
|
|
111
|
+
return Path.home() / "Library" / "Caches" / "cbe" / "tools"
|
|
112
|
+
base = os.environ.get("XDG_CACHE_HOME")
|
|
113
|
+
return (Path(base).expanduser() if base else Path.home() / ".cache") / "cbe" / "tools"
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def download_size(key: str | None = None, table=PINNED) -> int | None:
|
|
117
|
+
assets = table.get(key or platform_key() or "")
|
|
118
|
+
return sum(asset.size for asset in assets.values()) if assets else None
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def setup_hint(key: str | None = None, table=PINNED) -> str:
|
|
122
|
+
size = download_size(key, table)
|
|
123
|
+
gigabytes = f"about {size / 1e9:.1f} GB" if size else "about 2 GB"
|
|
124
|
+
return f"run `cbe graph setup` (downloads {gigabytes})"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _joern_dir_ok(path: Path) -> bool:
|
|
128
|
+
return (path / "joern").is_file() and (path / "joern-parse").is_file()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class ToolchainError(ValueError):
|
|
132
|
+
"""A tool location the user named does not hold the tool."""
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _joern_in(base: Path) -> Path | None:
|
|
136
|
+
return next((c for c in (base, base / "joern-cli", base / "bin") if _joern_dir_ok(c)), None)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def find_joern(joern_home: Path | str | None = None, cache: Path | None = None) -> Path | None:
|
|
140
|
+
"""Locate the Joern CLI directory that holds ``joern`` and ``joern-parse``.
|
|
141
|
+
|
|
142
|
+
A location the user named (``joern_home`` or ``CBE_JOERN_HOME``) must be valid:
|
|
143
|
+
it raises :class:`ToolchainError` instead of falling through to the next place.
|
|
144
|
+
"""
|
|
145
|
+
for label, base in (("--joern-home", joern_home), ("CBE_JOERN_HOME", os.environ.get("CBE_JOERN_HOME"))):
|
|
146
|
+
if not base:
|
|
147
|
+
continue
|
|
148
|
+
base = Path(base).expanduser()
|
|
149
|
+
found = _joern_in(base)
|
|
150
|
+
if found is None:
|
|
151
|
+
raise ToolchainError(
|
|
152
|
+
f"{label} {base} does not hold joern-parse: looked for joern and joern-parse in {base}, "
|
|
153
|
+
f"{base / 'joern-cli'} and {base / 'bin'}. Fix the path, or {setup_hint()}")
|
|
154
|
+
return found
|
|
155
|
+
cached = (cache or cache_dir()) / f"joern-{JOERN_VERSION}" / "joern-cli"
|
|
156
|
+
if _joern_dir_ok(cached):
|
|
157
|
+
return cached
|
|
158
|
+
which = shutil.which("joern-parse")
|
|
159
|
+
if which and _joern_dir_ok(Path(which).resolve().parent):
|
|
160
|
+
return Path(which).resolve().parent
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@dataclass(frozen=True)
|
|
165
|
+
class JavaInstall:
|
|
166
|
+
home: Path
|
|
167
|
+
version: str
|
|
168
|
+
major: int
|
|
169
|
+
source: str
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _java_home_in(root: Path) -> Path | None:
|
|
173
|
+
for candidate in (root, root / "Contents" / "Home"):
|
|
174
|
+
if (candidate / "bin" / "java").is_file():
|
|
175
|
+
return candidate
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def probe_java(java_exe: Path) -> tuple[Path, str, int] | None:
|
|
180
|
+
"""Ask a ``java`` executable for its home and version; None if it cannot run."""
|
|
181
|
+
try:
|
|
182
|
+
done = subprocess.run([str(java_exe), "-XshowSettings:properties", "-version"],
|
|
183
|
+
capture_output=True, text=True, timeout=60, check=False,
|
|
184
|
+
stdin=subprocess.DEVNULL)
|
|
185
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
186
|
+
return None
|
|
187
|
+
text = (done.stderr or "") + (done.stdout or "")
|
|
188
|
+
home = re.search(r"^\s*java\.home = (.+)$", text, re.M)
|
|
189
|
+
version = re.search(r"^\s*java\.version = (\S+)$", text, re.M)
|
|
190
|
+
if done.returncode != 0 or not home or not version:
|
|
191
|
+
return None
|
|
192
|
+
major = re.match(r"(\d+)", version.group(1))
|
|
193
|
+
return Path(home.group(1).strip()), version.group(1), int(major.group(1)) if major else 0
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def java_candidates(cache: Path | None = None) -> list[tuple[str, Path]]:
|
|
197
|
+
"""Java executables in lookup order, as (source, path)."""
|
|
198
|
+
found: list[tuple[str, Path]] = []
|
|
199
|
+
if os.environ.get("JAVA_HOME"):
|
|
200
|
+
found.append(("JAVA_HOME", Path(os.environ["JAVA_HOME"]).expanduser() / "bin" / "java"))
|
|
201
|
+
if shutil.which("java"):
|
|
202
|
+
found.append(("PATH", Path(shutil.which("java"))))
|
|
203
|
+
home = _java_home_in((cache or cache_dir()) / f"jre-{JAVA_VERSION}")
|
|
204
|
+
if home:
|
|
205
|
+
found.append(("cache", home / "bin" / "java"))
|
|
206
|
+
return found
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def find_java(cache: Path | None = None, *, rejected: list | None = None) -> JavaInstall | None:
|
|
210
|
+
"""First Java of at least ``MIN_JAVA_MAJOR`` in lookup order; others go to ``rejected``."""
|
|
211
|
+
for source, exe in java_candidates(cache):
|
|
212
|
+
if not exe.is_file():
|
|
213
|
+
if rejected is not None:
|
|
214
|
+
rejected.append((source, str(exe), "not an executable file"))
|
|
215
|
+
continue
|
|
216
|
+
probed = probe_java(exe)
|
|
217
|
+
if probed is None:
|
|
218
|
+
if rejected is not None:
|
|
219
|
+
rejected.append((source, str(exe), "does not run"))
|
|
220
|
+
continue
|
|
221
|
+
home, version, major = probed
|
|
222
|
+
if major >= MIN_JAVA_MAJOR:
|
|
223
|
+
return JavaInstall(home, version, major, source)
|
|
224
|
+
if rejected is not None:
|
|
225
|
+
rejected.append((source, str(exe), f"Java {version} is older than {MIN_JAVA_MAJOR}"))
|
|
226
|
+
return None
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def java_environment(java: JavaInstall | None, base: dict | None = None) -> dict | None:
|
|
230
|
+
"""Environment that makes Joern's launcher use ``java``; None when nothing needs changing."""
|
|
231
|
+
if java is None:
|
|
232
|
+
return base
|
|
233
|
+
env = dict(os.environ if base is None else base)
|
|
234
|
+
env["JAVA_HOME"] = str(java.home)
|
|
235
|
+
env["PATH"] = str(java.home / "bin") + os.pathsep + env.get("PATH", "")
|
|
236
|
+
return env
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def check(cache: Path | None = None) -> dict:
|
|
240
|
+
"""What is installed and what is missing; downloads nothing."""
|
|
241
|
+
from .joern import joern_version # noqa: PLC0415
|
|
242
|
+
|
|
243
|
+
key = platform_key()
|
|
244
|
+
joern_error = None
|
|
245
|
+
try:
|
|
246
|
+
joern_dir = find_joern(cache=cache)
|
|
247
|
+
except ToolchainError as exc:
|
|
248
|
+
joern_dir, joern_error = None, str(exc)
|
|
249
|
+
rejected: list = []
|
|
250
|
+
java = find_java(cache, rejected=rejected)
|
|
251
|
+
joern = None
|
|
252
|
+
if joern_dir:
|
|
253
|
+
joern = {"path": str(joern_dir), "version": joern_version(joern_dir / "joern", 120, java=java)}
|
|
254
|
+
missing = []
|
|
255
|
+
if joern is None:
|
|
256
|
+
missing.append("joern")
|
|
257
|
+
if java is None:
|
|
258
|
+
missing.append("java")
|
|
259
|
+
report = {
|
|
260
|
+
"platform": key or f"{platform.system().lower()}-{platform.machine().lower()}",
|
|
261
|
+
"joern": joern,
|
|
262
|
+
"java": ({"path": str(java.home), "version": java.version, "source": java.source}
|
|
263
|
+
if java else None),
|
|
264
|
+
"java_rejected": [{"source": s, "path": p, "reason": r} for s, p, r in rejected],
|
|
265
|
+
"missing": missing,
|
|
266
|
+
"cache_dir": str(cache or cache_dir()),
|
|
267
|
+
"pinned_joern": JOERN_VERSION,
|
|
268
|
+
}
|
|
269
|
+
if joern_error:
|
|
270
|
+
report["joern_error"] = joern_error
|
|
271
|
+
if missing:
|
|
272
|
+
report["next"] = setup_hint(key)
|
|
273
|
+
return report
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _human(size: float) -> str:
|
|
277
|
+
return f"{size / 1e9:.2f} GB" if size >= 1e8 else f"{size / 1e6:.1f} MB"
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _download(asset: Asset, target: Path, say) -> None:
|
|
281
|
+
"""Stream ``asset`` to ``target`` and verify its pinned checksums before returning."""
|
|
282
|
+
digests = {name: hashlib.new(name) for name in asset.checksums}
|
|
283
|
+
started, shown = time.monotonic(), -1
|
|
284
|
+
try:
|
|
285
|
+
with urllib.request.urlopen(asset.url, timeout=60) as response, target.open("wb") as out:
|
|
286
|
+
total = int(response.headers.get("Content-Length") or asset.size)
|
|
287
|
+
done = 0
|
|
288
|
+
while chunk := response.read(1 << 20):
|
|
289
|
+
out.write(chunk)
|
|
290
|
+
for digest in digests.values():
|
|
291
|
+
digest.update(chunk)
|
|
292
|
+
done += len(chunk)
|
|
293
|
+
step = done * 10 // max(total, 1)
|
|
294
|
+
if step != shown and total >= 1e8:
|
|
295
|
+
shown = step
|
|
296
|
+
say(f" {step * 10}% ({_human(done)} of {_human(total)}, {time.monotonic() - started:.0f}s)")
|
|
297
|
+
except OSError as exc:
|
|
298
|
+
target.unlink(missing_ok=True)
|
|
299
|
+
raise SetupError(f"download failed for {asset.url}: {exc}. Check the network and run the command again. "
|
|
300
|
+
+ _MANUAL) from exc
|
|
301
|
+
for name, expected in asset.checksums.items():
|
|
302
|
+
actual = digests[name].hexdigest()
|
|
303
|
+
if actual != expected:
|
|
304
|
+
target.unlink(missing_ok=True)
|
|
305
|
+
raise SetupError(f"{name} mismatch for {asset.name}: expected {expected}, got {actual}. "
|
|
306
|
+
"The file was deleted and nothing was installed.")
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _extract(archive: Path, destination: Path) -> None:
|
|
310
|
+
destination.mkdir(parents=True)
|
|
311
|
+
root = destination.resolve()
|
|
312
|
+
if archive.name.endswith(".zip"):
|
|
313
|
+
with zipfile.ZipFile(archive) as zf:
|
|
314
|
+
for info in zf.infolist():
|
|
315
|
+
target = (root / info.filename).resolve()
|
|
316
|
+
if target != root and root not in target.parents:
|
|
317
|
+
raise SetupError(f"archive entry escapes the target directory: {info.filename}")
|
|
318
|
+
mode = info.external_attr >> 16
|
|
319
|
+
if (mode & 0o170000) == 0o120000:
|
|
320
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
321
|
+
target.unlink(missing_ok=True)
|
|
322
|
+
target.symlink_to(zf.read(info).decode())
|
|
323
|
+
continue
|
|
324
|
+
zf.extract(info, root)
|
|
325
|
+
if mode & 0o111 and not info.is_dir():
|
|
326
|
+
target.chmod(target.stat().st_mode | 0o111) # zipfile drops the executable bit
|
|
327
|
+
else:
|
|
328
|
+
with tarfile.open(archive) as tf:
|
|
329
|
+
tf.extractall(root, filter="data")
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _install_asset(kind: str, asset: Asset, final: Path, cache: Path, say, *, strip_top: bool) -> None:
|
|
333
|
+
say(f"Downloading {asset.name} ({_human(asset.size)}) from {asset.url}")
|
|
334
|
+
say(f" to {final}")
|
|
335
|
+
cache.mkdir(parents=True, exist_ok=True)
|
|
336
|
+
work = Path(tempfile.mkdtemp(prefix=f".{kind}-", dir=cache))
|
|
337
|
+
try:
|
|
338
|
+
archive = work / asset.name
|
|
339
|
+
_download(asset, archive, say)
|
|
340
|
+
say(f" checksums match: {', '.join(sorted(asset.checksums))}")
|
|
341
|
+
_extract(archive, work / "unpacked")
|
|
342
|
+
archive.unlink()
|
|
343
|
+
unpacked = work / "unpacked"
|
|
344
|
+
entries = [p for p in unpacked.iterdir()]
|
|
345
|
+
top = entries[0] if strip_top and len(entries) == 1 and entries[0].is_dir() else unpacked
|
|
346
|
+
if final.exists():
|
|
347
|
+
shutil.rmtree(final)
|
|
348
|
+
shutil.move(str(top), str(final))
|
|
349
|
+
except zipfile.BadZipFile as exc:
|
|
350
|
+
raise SetupError(f"{asset.name} is not a valid zip file: {exc}") from exc
|
|
351
|
+
except tarfile.TarError as exc:
|
|
352
|
+
raise SetupError(f"{asset.name} is not a valid tar file: {exc}") from exc
|
|
353
|
+
finally:
|
|
354
|
+
shutil.rmtree(work, ignore_errors=True)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def setup(*, force: bool = False, with_java: bool = False, cache: Path | None = None,
|
|
358
|
+
table: dict | None = None, key: str | None = None, say=None) -> dict:
|
|
359
|
+
"""Install pinned Joern and, when no Java 21+ is usable, a JRE into the cache directory."""
|
|
360
|
+
say = say or (lambda text: print(text, file=sys.stderr, flush=True))
|
|
361
|
+
table = PINNED if table is None else table
|
|
362
|
+
key = key or platform_key()
|
|
363
|
+
if key is None or key not in table:
|
|
364
|
+
raise SetupError(f"`cbe graph setup` has no download for {platform.system()} {platform.machine()}. "
|
|
365
|
+
f"Supported: {', '.join(sorted(table))}. " + _MANUAL)
|
|
366
|
+
cache = cache or cache_dir()
|
|
367
|
+
assets = table[key]
|
|
368
|
+
joern_final = cache / f"joern-{JOERN_VERSION}"
|
|
369
|
+
jre_final = cache / f"jre-{JAVA_VERSION}"
|
|
370
|
+
result: dict = {"platform": key, "cache_dir": str(cache), "steps": []}
|
|
371
|
+
|
|
372
|
+
if _joern_dir_ok(joern_final / "joern-cli") and not force:
|
|
373
|
+
say(f"Joern {JOERN_VERSION} is already installed in {joern_final}; use --force to reinstall.")
|
|
374
|
+
result["steps"].append({"joern": "already_installed"})
|
|
375
|
+
else:
|
|
376
|
+
_install_asset("joern", assets["joern"], joern_final, cache, say, strip_top=False)
|
|
377
|
+
if not _joern_dir_ok(joern_final / "joern-cli"):
|
|
378
|
+
raise SetupError(f"{assets['joern'].name} unpacked without joern-cli/joern and joern-parse")
|
|
379
|
+
result["steps"].append({"joern": "installed"})
|
|
380
|
+
|
|
381
|
+
have_java = None if with_java else find_java(cache)
|
|
382
|
+
if force and _java_home_in(jre_final):
|
|
383
|
+
have_java = None # --force also reinstalls a JRE this command put in the cache
|
|
384
|
+
if have_java is not None:
|
|
385
|
+
say(f"Java {have_java.version} from {have_java.source} is usable ({have_java.home}); "
|
|
386
|
+
"not downloading a JRE. Use --with-java to download one anyway.")
|
|
387
|
+
result["steps"].append({"java": f"skipped, using {have_java.source}"})
|
|
388
|
+
else:
|
|
389
|
+
_install_asset("jre", assets["java"], jre_final, cache, say, strip_top=True)
|
|
390
|
+
if _java_home_in(jre_final) is None:
|
|
391
|
+
raise SetupError(f"{assets['java'].name} unpacked without bin/java")
|
|
392
|
+
result["steps"].append({"java": "installed"})
|
|
393
|
+
final = check(cache)
|
|
394
|
+
result.update(joern=final["joern"], java=final["java"], missing=final["missing"])
|
|
395
|
+
return result
|
|
396
|
+
|
cbe/grounding.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Check whether identifier-like words in an explanation occur in its unit's source."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
IDENT = re.compile(r"[A-Za-z_$][A-Za-z0-9_$]*")
|
|
8
|
+
STOP = {"true", "false", "null", "none", "nil", "self", "this", "void", "int", "str", "string", "bool",
|
|
9
|
+
"boolean", "dict", "list", "tuple", "set", "object", "async", "await", "return", "yield", "if",
|
|
10
|
+
"else", "for", "while", "try", "except", "catch", "finally", "raise", "throw", "class", "def",
|
|
11
|
+
"function", "fn", "func", "import", "from", "export", "const", "let", "var", "static", "public",
|
|
12
|
+
"private", "new", "in", "is", "not", "and", "or", "id", "args", "kwargs", "i.e", "e.g"}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
#: An answer is rejected when at least this many of its explanations, and at least this share of
|
|
16
|
+
#: them, are suspect. Measured on seven runs: the one wrong answer had 59% suspect, the other 56
|
|
17
|
+
#: answers at most 14%.
|
|
18
|
+
BATCH_MIN = 3
|
|
19
|
+
BATCH_SHARE = 0.3
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def idents(text):
|
|
23
|
+
return {t for t in IDENT.findall(text)}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def codey(tok):
|
|
27
|
+
return "_" in tok.strip("_") or re.search(r"[a-z0-9][A-Z]", tok) is not None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def mentions(explain, repo_names):
|
|
31
|
+
ticks = set()
|
|
32
|
+
for span in re.findall(r"`([^`]+)`", explain):
|
|
33
|
+
ticks |= idents(span)
|
|
34
|
+
plain = re.sub(r"`[^`]+`", " ", explain)
|
|
35
|
+
code = {t for t in idents(plain) if codey(t)}
|
|
36
|
+
code |= {m for m in re.findall(r"([A-Za-z_$][A-Za-z0-9_$]*)\(", plain)}
|
|
37
|
+
names = {t for t in idents(plain) if t in repo_names and len(t) >= 3}
|
|
38
|
+
return ticks, code, names
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def repo_names(symbol_names) -> frozenset[str]:
|
|
42
|
+
"""The valid final dotted name segments in this run's symbol inventory."""
|
|
43
|
+
names = set()
|
|
44
|
+
for symbol_name in symbol_names:
|
|
45
|
+
name = (symbol_name or "").split(".")[-1]
|
|
46
|
+
if IDENT.fullmatch(name or ""):
|
|
47
|
+
names.add(name)
|
|
48
|
+
return frozenset(names)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def ungrounded(explain, source_text, repo_names) -> list[str]:
|
|
52
|
+
"""Return missing names only when no mentioned identifier occurs in the source."""
|
|
53
|
+
ok = {t.lower() for t in idents(source_text)}
|
|
54
|
+
ticks, code, names = mentions(explain, repo_names)
|
|
55
|
+
mentioned = {t for t in ticks | code | names if t.lower() not in STOP}
|
|
56
|
+
missing = sorted(t for t in mentioned if t.lower() not in ok)
|
|
57
|
+
return missing if mentioned and len(missing) == len(mentioned) else []
|