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,256 @@
|
|
|
1
|
+
"""File/module import backends: dependency-cruiser (JS/TS/TSX) and Grimp (Python).
|
|
2
|
+
|
|
3
|
+
Joern exposes import *declarations*; these two tools resolve them to concrete
|
|
4
|
+
files/modules and are the source of truth for the ``import`` graph kind where
|
|
5
|
+
they apply. Both are optional: a missing binary becomes a diagnostic, not a
|
|
6
|
+
crash, and a run that succeeds with no dependencies reports ``empty`` rather
|
|
7
|
+
than an unexplained absence.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import shutil
|
|
15
|
+
import subprocess
|
|
16
|
+
import sys
|
|
17
|
+
|
|
18
|
+
from ._paths import canonical_repo_path
|
|
19
|
+
from .identity import repository_files, file_id, external_id
|
|
20
|
+
from .resolution import ImportResolver
|
|
21
|
+
|
|
22
|
+
DEPCRUISE_LANGUAGES = frozenset({"javascript", "typescript", "tsx"})
|
|
23
|
+
_JS_EXTENSIONS = (".js", ".jsx", ".mjs", ".cjs", ".ts", ".mts", ".cts", ".tsx")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def find_depcruise(depcruise_bin=None):
|
|
27
|
+
if depcruise_bin is not None:
|
|
28
|
+
candidate = os.path.expanduser(str(depcruise_bin))
|
|
29
|
+
if os.path.isfile(candidate):
|
|
30
|
+
return candidate
|
|
31
|
+
env = os.environ.get("CBE_DEPCRUISE_BIN")
|
|
32
|
+
if env and os.path.isfile(env):
|
|
33
|
+
return env
|
|
34
|
+
which = shutil.which("depcruise")
|
|
35
|
+
return which or None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def build_with_depcruise(repo, run_dir, depcruise_bin, timeout: int, languages: list[str]) -> dict:
|
|
39
|
+
os.makedirs(str(run_dir), exist_ok=True)
|
|
40
|
+
try:
|
|
41
|
+
completed = subprocess.run(
|
|
42
|
+
[str(depcruise_bin), "--no-config", "--output-type", "json", "."],
|
|
43
|
+
cwd=str(repo), check=False, capture_output=True, text=True, timeout=timeout,
|
|
44
|
+
stdin=subprocess.DEVNULL,
|
|
45
|
+
)
|
|
46
|
+
except subprocess.TimeoutExpired:
|
|
47
|
+
return _result([], [], [{"path": None, "code": "backend_timeout",
|
|
48
|
+
"message": f"dependency-cruiser timed out after {timeout}s"}],
|
|
49
|
+
languages, status="failed")
|
|
50
|
+
except OSError as exc:
|
|
51
|
+
return _result([], [], [{"path": None, "code": "backend_error",
|
|
52
|
+
"message": f"cannot run dependency-cruiser: {exc}"}],
|
|
53
|
+
languages, status="failed")
|
|
54
|
+
if completed.returncode != 0:
|
|
55
|
+
return _result([], [], [{"path": None, "code": "backend_error",
|
|
56
|
+
"message": f"dependency-cruiser failed (exit {completed.returncode})"}],
|
|
57
|
+
languages, status="failed")
|
|
58
|
+
try:
|
|
59
|
+
payload = json.loads(completed.stdout or "{}")
|
|
60
|
+
except json.JSONDecodeError as exc:
|
|
61
|
+
return _result([], [], [{"path": None, "code": "backend_error",
|
|
62
|
+
"message": f"dependency-cruiser output unreadable: {exc}"}],
|
|
63
|
+
languages, status="failed")
|
|
64
|
+
|
|
65
|
+
nodes: list[dict] = []
|
|
66
|
+
edges: list[dict] = []
|
|
67
|
+
seen: set[str] = set()
|
|
68
|
+
counter = 0
|
|
69
|
+
files = repository_files(repo)
|
|
70
|
+
if (not payload.get("modules") and any(lang in {"typescript", "tsx"} for lang in languages)
|
|
71
|
+
and any(path.endswith((".ts", ".tsx", ".mts", ".cts")) for path in files)):
|
|
72
|
+
return _result([], [], [{"path": None, "code": "depcruise_no_modules",
|
|
73
|
+
"message": "dependency-cruiser returned zero modules for TypeScript input; this usually means "
|
|
74
|
+
"the 'typescript' package is missing or unsupported (require >=2 <6)"}],
|
|
75
|
+
languages, status="failed")
|
|
76
|
+
|
|
77
|
+
def add_file(rel: str) -> str | None:
|
|
78
|
+
node_id = file_id(rel, files)
|
|
79
|
+
if node_id is None:
|
|
80
|
+
return None
|
|
81
|
+
if node_id not in seen:
|
|
82
|
+
seen.add(node_id)
|
|
83
|
+
nodes.append({"id": node_id, "kind": "file", "path": rel,
|
|
84
|
+
"start_line": None, "end_line": None, "name": rel,
|
|
85
|
+
"backend": "dependency-cruiser"})
|
|
86
|
+
return node_id
|
|
87
|
+
|
|
88
|
+
for module in payload.get("modules", []):
|
|
89
|
+
source = canonical_repo_path(module.get("source", ""))
|
|
90
|
+
if not source or not source.endswith(_JS_EXTENSIONS):
|
|
91
|
+
continue
|
|
92
|
+
source_id = add_file(source)
|
|
93
|
+
if source_id is None:
|
|
94
|
+
continue
|
|
95
|
+
for dep in module.get("dependencies", []):
|
|
96
|
+
resolved = dep.get("resolved")
|
|
97
|
+
expression = dep.get("module", "")
|
|
98
|
+
counter += 1
|
|
99
|
+
if dep.get("couldNotResolve"):
|
|
100
|
+
target_id = None
|
|
101
|
+
status = "unresolved"
|
|
102
|
+
elif resolved and not dep.get("coreModule"):
|
|
103
|
+
target_rel = canonical_repo_path(resolved)
|
|
104
|
+
target_id = add_file(target_rel) if target_rel else None
|
|
105
|
+
status = "resolved" if target_id else "unresolved"
|
|
106
|
+
elif dep.get("coreModule"):
|
|
107
|
+
target_rel = canonical_repo_path(str(expression))
|
|
108
|
+
target_id = external_id(target_rel or expression)
|
|
109
|
+
if target_id not in seen:
|
|
110
|
+
seen.add(target_id)
|
|
111
|
+
nodes.append({"id": target_id, "kind": "external", "path": None,
|
|
112
|
+
"start_line": None, "end_line": None, "name": expression,
|
|
113
|
+
"external": True, "backend": "dependency-cruiser"})
|
|
114
|
+
status = "external"
|
|
115
|
+
else:
|
|
116
|
+
target_id = None
|
|
117
|
+
status = "unresolved"
|
|
118
|
+
edges.append({
|
|
119
|
+
"id": f"dc/import/{counter}",
|
|
120
|
+
"source": source_id,
|
|
121
|
+
"target": target_id,
|
|
122
|
+
"kind": "import",
|
|
123
|
+
"status": status,
|
|
124
|
+
"path": source,
|
|
125
|
+
"expression": expression,
|
|
126
|
+
"analyzer_id": None,
|
|
127
|
+
"backend": "dependency-cruiser",
|
|
128
|
+
})
|
|
129
|
+
return _result(nodes, edges, [], languages, status="resolved" if edges else "empty")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def build_with_grimp(repo, languages: list[str], suffix_map=None) -> dict:
|
|
133
|
+
if "python" not in languages:
|
|
134
|
+
return {"nodes": [], "edges": [], "diagnostics": [], "capabilities": []}
|
|
135
|
+
try:
|
|
136
|
+
import grimp # noqa: PLC0415
|
|
137
|
+
except ImportError:
|
|
138
|
+
return {"nodes": [], "edges": [], "diagnostics": [
|
|
139
|
+
{"path": None, "code": "backend_unavailable",
|
|
140
|
+
"message": "grimp is not installed; Python import graph skipped (install the 'graphs' extra)"}],
|
|
141
|
+
"capabilities": []}
|
|
142
|
+
resolver = ImportResolver(repo, suffix_map=suffix_map)
|
|
143
|
+
package_roots = _python_package_roots(repo)
|
|
144
|
+
if not package_roots:
|
|
145
|
+
return _result([], [], [{"path": None, "code": "no_python_packages",
|
|
146
|
+
"message": "no importable top-level Python package was found"}],
|
|
147
|
+
None, status="empty", language="python")
|
|
148
|
+
diagnostics: list[dict] = []
|
|
149
|
+
nodes: list[dict] = []
|
|
150
|
+
edges: list[dict] = []
|
|
151
|
+
seen: set[str] = set()
|
|
152
|
+
built = 0
|
|
153
|
+
hard_failures = 0
|
|
154
|
+
not_packages = 0
|
|
155
|
+
inserted = []
|
|
156
|
+
for _, root in reversed(package_roots):
|
|
157
|
+
value = str(root)
|
|
158
|
+
sys.path.insert(0, value)
|
|
159
|
+
inserted.append(value)
|
|
160
|
+
try:
|
|
161
|
+
for package, _ in package_roots:
|
|
162
|
+
try:
|
|
163
|
+
graph = grimp.build_graph(package)
|
|
164
|
+
except Exception as exc: # grimp has several failure modes for arbitrary trees
|
|
165
|
+
kind = type(exc).__name__
|
|
166
|
+
if kind.startswith("NotATopLevel"):
|
|
167
|
+
not_packages += 1
|
|
168
|
+
diagnostics.append({"path": None, "code": "not_a_package",
|
|
169
|
+
"message": f"grimp treats {package!r} as a top-level module, not a package"})
|
|
170
|
+
else:
|
|
171
|
+
hard_failures += 1
|
|
172
|
+
diagnostics.append({"path": None, "code": "backend_error",
|
|
173
|
+
"message": f"grimp could not build {package}: {kind}"})
|
|
174
|
+
continue
|
|
175
|
+
built += 1
|
|
176
|
+
for module in graph.modules:
|
|
177
|
+
source_path = resolver.python_file(module)
|
|
178
|
+
if source_path is None:
|
|
179
|
+
diagnostics.append({"path": None, "code": "module_file_missing",
|
|
180
|
+
"message": f"grimp module {module!r} has no repository file"})
|
|
181
|
+
continue
|
|
182
|
+
module_id = _add_module(nodes, seen, module, source_path, resolver.files)
|
|
183
|
+
for imported in graph.find_modules_directly_imported_by(module):
|
|
184
|
+
target_path = resolver.python_file(imported)
|
|
185
|
+
status = "resolved" if target_path else ("unresolved" if resolver.python_local(imported) else "external")
|
|
186
|
+
target_id = _add_module(nodes, seen, imported, target_path, resolver.files) if status != "unresolved" else None
|
|
187
|
+
edges.append({
|
|
188
|
+
"id": f"grimp/import/{module}->{imported}",
|
|
189
|
+
"source": module_id,
|
|
190
|
+
"target": target_id,
|
|
191
|
+
"kind": "import",
|
|
192
|
+
"status": status,
|
|
193
|
+
"path": source_path,
|
|
194
|
+
"expression": imported,
|
|
195
|
+
"analyzer_id": None,
|
|
196
|
+
"backend": "grimp",
|
|
197
|
+
})
|
|
198
|
+
finally:
|
|
199
|
+
for value in inserted:
|
|
200
|
+
try:
|
|
201
|
+
sys.path.remove(value)
|
|
202
|
+
except ValueError:
|
|
203
|
+
pass
|
|
204
|
+
if edges:
|
|
205
|
+
status = "resolved"
|
|
206
|
+
elif built:
|
|
207
|
+
status = "empty"
|
|
208
|
+
elif hard_failures:
|
|
209
|
+
status = "failed"
|
|
210
|
+
elif not_packages:
|
|
211
|
+
status = "unsupported"
|
|
212
|
+
else:
|
|
213
|
+
status = "empty"
|
|
214
|
+
return _result(nodes, edges, diagnostics, None, status=status, language="python")
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _add_module(nodes: list[dict], seen: set[str], module: str, path: str | None, files) -> str:
|
|
218
|
+
module_id = file_id(path, files) if path else external_id(module)
|
|
219
|
+
if module_id not in seen:
|
|
220
|
+
seen.add(module_id)
|
|
221
|
+
nodes.append({"id": module_id, "kind": "file" if path else "external",
|
|
222
|
+
"path": path,
|
|
223
|
+
"start_line": None, "end_line": None, "name": module,
|
|
224
|
+
"backend": "grimp", **({"external": True} if path is None else {})})
|
|
225
|
+
return module_id
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _result(nodes, edges, diagnostics, languages, *, status, language=None) -> dict:
|
|
229
|
+
if language is not None:
|
|
230
|
+
capabilities = [{"language": language, "graph": "import", "backend": "grimp",
|
|
231
|
+
"status": status,
|
|
232
|
+
"limitations": "module-level imports only"}]
|
|
233
|
+
else:
|
|
234
|
+
capabilities = [{"language": lang, "graph": "import", "backend": "dependency-cruiser",
|
|
235
|
+
"status": status, "limitations": None} for lang in (languages or [])]
|
|
236
|
+
return {"nodes": nodes, "edges": edges, "diagnostics": diagnostics, "capabilities": capabilities}
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _python_package_roots(repo) -> list[tuple[str, object]]:
|
|
240
|
+
"""Find top-level import names and the source roots that expose them."""
|
|
241
|
+
import pathlib
|
|
242
|
+
|
|
243
|
+
repo = pathlib.Path(repo)
|
|
244
|
+
files = repository_files(repo)
|
|
245
|
+
found: dict[tuple[str, str], None] = {}
|
|
246
|
+
for path in files:
|
|
247
|
+
pure = pathlib.PurePosixPath(path)
|
|
248
|
+
if path.endswith("/__init__.py"):
|
|
249
|
+
top = pure.parent
|
|
250
|
+
while str(top.parent) != "." and str(top.parent / "__init__.py") in files:
|
|
251
|
+
top = top.parent
|
|
252
|
+
root = repo if str(top.parent) == "." else repo / top.parent
|
|
253
|
+
found[(top.name, str(root))] = None
|
|
254
|
+
elif "/" not in path and pure.suffix == ".py" and pure.name != "setup.py":
|
|
255
|
+
found[(pure.stem, str(repo))] = None
|
|
256
|
+
return [(name, pathlib.Path(root)) for name, root in sorted(found)]
|