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.
Files changed (47) hide show
  1. cbe/__init__.py +9 -0
  2. cbe/__main__.py +4 -0
  3. cbe/aggregation.py +183 -0
  4. cbe/api_completion.py +410 -0
  5. cbe/batching.py +283 -0
  6. cbe/catalog_query.py +376 -0
  7. cbe/checks.py +390 -0
  8. cbe/cli.py +434 -0
  9. cbe/concrete_facts.py +321 -0
  10. cbe/data/LICENSE.tiktoken +30 -0
  11. cbe/data/o200k_base.tiktoken +199998 -0
  12. cbe/declarations.py +537 -0
  13. cbe/docs.py +929 -0
  14. cbe/generate.py +316 -0
  15. cbe/grammars.py +741 -0
  16. cbe/graph.py +857 -0
  17. cbe/graph_backends/__init__.py +43 -0
  18. cbe/graph_backends/_calls.py +262 -0
  19. cbe/graph_backends/_paths.py +42 -0
  20. cbe/graph_backends/identity.py +93 -0
  21. cbe/graph_backends/imports.py +256 -0
  22. cbe/graph_backends/joern.py +1021 -0
  23. cbe/graph_backends/resolution.py +288 -0
  24. cbe/graph_backends/toolchain.py +396 -0
  25. cbe/grounding.py +57 -0
  26. cbe/host_run.py +1752 -0
  27. cbe/model_settings.py +80 -0
  28. cbe/projection.py +305 -0
  29. cbe/prompts.py +316 -0
  30. cbe/records.py +200 -0
  31. cbe/skeleton.py +474 -0
  32. cbe/source_context.py +709 -0
  33. cbe/symbols.py +136 -0
  34. cbe/token_budget.py +68 -0
  35. cbe/treename.py +398 -0
  36. cbe/usage.py +300 -0
  37. cbe_install/__init__.py +11 -0
  38. cbe_install/cli.py +434 -0
  39. cbe_install/resources/agents/cbe-writer.md +16 -0
  40. cbe_install/resources/codebase-explorer/SKILL.md +101 -0
  41. cbe_install/resources/codebase-explorer/references/claude-code.md +63 -0
  42. cbe_install/resources/codebase-explorer/references/codex.md +37 -0
  43. codebase_explorer-2.2.0.dist-info/METADATA +233 -0
  44. codebase_explorer-2.2.0.dist-info/RECORD +47 -0
  45. codebase_explorer-2.2.0.dist-info/WHEEL +4 -0
  46. codebase_explorer-2.2.0.dist-info/entry_points.txt +3 -0
  47. codebase_explorer-2.2.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,288 @@
1
+ """Import declaration resolution and file identity, shared by graph backends.
2
+
3
+ Resolution is static and repository bounded: it never imports project code or
4
+ searches the host environment. Absolute Python dependencies outside the source
5
+ roots are external, whether installed on this machine or not.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import ast
10
+ import re
11
+ from pathlib import Path, PurePosixPath
12
+
13
+ from ._paths import canonical_repo_path
14
+ from .identity import repository_files, file_id, external_id
15
+ from cbe.grammars import suffix_languages
16
+
17
+
18
+ # System header namespaces documented by POSIX Issue 8, <sys/...>,
19
+ # <netinet/in.h>, <arpa/inet.h>, and Linux man-pages, headers(7).
20
+ # These namespaces never establish a repository include search root.
21
+ SYSTEM_INCLUDE_PREFIXES = frozenset({"sys", "netinet", "arpa", "net", "linux", "asm", "asm-generic"})
22
+
23
+
24
+ def go_modules(repo: Path, files) -> dict[str, str]:
25
+ """Read root and nested Go module identities without searching the host."""
26
+ modules = {}
27
+ for path in sorted(files):
28
+ if PurePosixPath(path).name != "go.mod":
29
+ continue
30
+ try:
31
+ text = (repo / path).read_text(encoding="utf-8")
32
+ except OSError:
33
+ continue
34
+ match = re.search(r"(?m)^\s*module\s+(\S+)\s*$", text)
35
+ if match:
36
+ parent = PurePosixPath(path).parent
37
+ modules[match[1]] = "" if str(parent) == "." else str(parent)
38
+ return modules
39
+
40
+
41
+ class ImportResolver:
42
+ def __init__(self, repo: Path, files=None, suffix_map=None):
43
+ self.repo = repo
44
+ self.suffix_map = suffix_languages() if suffix_map is None else suffix_map
45
+ self.files = repository_files(repo) if files is None else files
46
+ self.suffix_files = {}
47
+ for path in sorted(self.files):
48
+ parts = path.split("/")
49
+ for start in range(len(parts)):
50
+ self.suffix_files.setdefault("/".join(parts[start:]), []).append(path)
51
+ self.diagnostics = []
52
+ self.include_basis = {}
53
+ self.go_modules = go_modules(repo, self.files)
54
+ self.python_roots = [""]
55
+ # Locate import roots from package ancestry rather than assuming that
56
+ # top-level packages live at the repository root (src/ is common).
57
+ for path in self.files:
58
+ if not path.endswith("/__init__.py"):
59
+ continue
60
+ package = PurePosixPath(path).parent
61
+ top = package
62
+ while str(top.parent) != "." and str(top.parent / "__init__.py") in self.files:
63
+ top = top.parent
64
+ root = "" if str(top.parent) == "." else str(top.parent) + "/"
65
+ if root not in self.python_roots:
66
+ self.python_roots.append(root)
67
+
68
+ def python_file(self, module: str) -> str | None:
69
+ stem = module.replace(".", "/")
70
+ for root in self.python_roots:
71
+ for candidate in (root + stem + "/__init__.py", root + stem + ".py"):
72
+ if candidate in self.files:
73
+ return candidate
74
+ return None
75
+
76
+ def python_local(self, module: str) -> bool:
77
+ top = module.split(".")[0]
78
+ return any(p.startswith(root + top + "/") or p == root + top + ".py"
79
+ for root in self.python_roots for p in self.files)
80
+
81
+ def _python(self, path: str, text: str) -> list[tuple[str, str | None]]:
82
+ try:
83
+ statement = ast.parse(text).body[0]
84
+ except (SyntaxError, IndexError):
85
+ return [("unresolved", None)]
86
+ if isinstance(statement, ast.Import):
87
+ modules = [alias.name for alias in statement.names]
88
+ relative = False
89
+ elif isinstance(statement, ast.ImportFrom):
90
+ base = statement.module or ""
91
+ relative = bool(statement.level)
92
+ if relative:
93
+ parent = list(PurePosixPath(path).parent.parts)
94
+ if parent and parent[0] == "src" and "src/" in self.python_roots:
95
+ parent = parent[1:]
96
+ if statement.level > len(parent):
97
+ return [("unresolved", None)]
98
+ base = ".".join(parent[:len(parent) - statement.level + 1] + ([base] if base else []))
99
+ modules = []
100
+ for alias in statement.names:
101
+ child = base + "." + alias.name if base else alias.name
102
+ # An imported name can be a submodule or a symbol of the base.
103
+ child_stem = child.replace(".", "/")
104
+ namespace_child = any(p.startswith(root + child_stem + "/")
105
+ for root in self.python_roots for p in self.files)
106
+ modules.append(child if self.python_file(child) or namespace_child else base)
107
+ else:
108
+ return [("unresolved", None)]
109
+ results = []
110
+ for module in modules:
111
+ target = self.python_file(module)
112
+ # Scripts outside packages can import adjacent modules when run
113
+ # directly. Keep this lookup repository bounded too.
114
+ parent = PurePosixPath(path).parent
115
+ if not relative and str(parent / "__init__.py") not in self.files:
116
+ stem = str(parent / module.replace(".", "/"))
117
+ target = next((p for p in (stem + ".py", stem + "/__init__.py") if p in self.files), target)
118
+ if target:
119
+ results.append(("resolved", target))
120
+ elif relative or self.python_local(module):
121
+ results.append(("unresolved", None))
122
+ else:
123
+ results.append(("external", module))
124
+ return list(dict.fromkeys(results))
125
+
126
+ def resolve(self, path: str, expression: str) -> list[tuple[str, str | None]]:
127
+ text = (expression or "").strip()
128
+ suffix = PurePosixPath(path).suffix.lower()
129
+ language = self.suffix_map.get(suffix)
130
+ suffix = {"c": ".c", "cpp": ".c", "ruby": ".rb", "php": ".php"}.get(language, suffix)
131
+ if suffix == ".py":
132
+ return self._python(path, text)
133
+ if suffix in {".c", ".h", ".cc", ".cpp", ".hpp", ".cxx", ".hh"}:
134
+ match = re.fullmatch(r'#\s*include\s*([<"])([^>"]+)[>"]\s*', text)
135
+ if not match:
136
+ return [("unresolved", None)]
137
+ delimiter, token = match.groups()
138
+ local = canonical_repo_path(str(PurePosixPath(path).parent / token))
139
+ if delimiter == '"' and local in self.files:
140
+ return [("resolved", local)]
141
+ matches = self.suffix_files.get(token, [])
142
+ if delimiter == '"' and not matches:
143
+ matches = self.suffix_files.get(PurePosixPath(token).name, [])
144
+ if delimiter == "<":
145
+ # A basename alone supplies no evidence of an include search
146
+ # root. In particular, repository compatibility/test headers
147
+ # must not shadow standard or third-party library headers.
148
+ if token.split("/", 1)[0] in SYSTEM_INCLUDE_PREFIXES:
149
+ return [("external", token)]
150
+ if len(matches) > 1:
151
+ self.diagnostics.append(dict(path=path, code="ambiguous_include",
152
+ expression=text, candidates=matches,
153
+ message=f"multiple repository candidates for <{token}>: {', '.join(matches)}"))
154
+ return [("unresolved", None)]
155
+ if "/" not in token:
156
+ return [("external", token)]
157
+ if len(matches) == 1:
158
+ prefix = str(PurePosixPath(token).parent)
159
+ self.include_basis[(path, text)] = {
160
+ "kind": "repository_directory_prefix", "prefix": prefix,
161
+ "include_root": matches[0][:-len(token)].rstrip("/") or ".",
162
+ }
163
+ return [("resolved", matches[0])]
164
+ return [("external", token)]
165
+ if len(matches) == 1:
166
+ return [("resolved", matches[0])]
167
+ return [("unresolved", None)]
168
+ token = import_token(text, suffix, self.suffix_map)
169
+ if not token:
170
+ return [("unresolved", None)]
171
+ if suffix in {".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs", ".mts", ".cts"}:
172
+ if not token.startswith("."):
173
+ return [("external", token)]
174
+ stem = canonical_repo_path(str(PurePosixPath(path).parent / token))
175
+ if stem:
176
+ extensions = (".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".mts", ".cts")
177
+ candidates = [stem] + [stem + ext for ext in extensions] + [stem + "/index" + ext for ext in extensions]
178
+ # TypeScript source commonly imports the emitted JS filename.
179
+ replacements = {".js": (".ts", ".tsx"), ".mjs": (".mts",), ".cjs": (".cts",)}
180
+ for emitted, sources in replacements.items():
181
+ if stem.endswith(emitted):
182
+ candidates.extend(stem[:-len(emitted)] + ext for ext in sources)
183
+ for candidate in candidates:
184
+ if candidate in self.files:
185
+ return [("resolved", candidate)]
186
+ if suffix == ".rb":
187
+ local = text.startswith("require_relative")
188
+ stem = canonical_repo_path(str(PurePosixPath(path).parent / token)) if local else token
189
+ for candidate in (stem, (stem or "") + ".rb"):
190
+ if candidate in self.files:
191
+ return [("resolved", candidate)]
192
+ return [("unresolved", None)] if local else [("external", token)]
193
+ if suffix in {".java", ".kt", ".kts", ".cs", ".php", ".swift"}:
194
+ # Class imports can include a member; try the owning file too.
195
+ parts = token.replace("\\", ".").rstrip(".*").split(".")
196
+ extensions = {".kts": (".kt", ".kts")}.get(suffix, (suffix,))
197
+ for end in range(len(parts), 0, -1):
198
+ stem = "/".join(parts[:end])
199
+ matches = sorted(p for p in self.files if any(
200
+ p == stem + ext or p.endswith("/" + stem + ext) for ext in extensions))
201
+ if matches:
202
+ return [("resolved", matches[0])] if len(matches) == 1 else [("unresolved", None)]
203
+ return [("external", token)]
204
+ if suffix == ".go":
205
+ # A package import maps to every non-test Go source in its
206
+ # directory. Exploding the declaration preserves real file
207
+ # identities instead of inventing one representative file.
208
+ for module, root in sorted(self.go_modules.items(), key=lambda row: len(row[0]), reverse=True):
209
+ if token != module and not token.startswith(module + "/"):
210
+ continue
211
+ relative = token[len(module):].lstrip("/")
212
+ directory = PurePosixPath(root) / relative if relative else PurePosixPath(root)
213
+ prefix = "" if str(directory) == "." else str(directory).rstrip("/") + "/"
214
+ targets = sorted(path for path in self.files if path.startswith(prefix)
215
+ and "/" not in path[len(prefix):] and path.endswith(".go")
216
+ and not path.endswith("_test.go"))
217
+ return [("resolved", path) for path in targets] or [("unresolved", None)]
218
+ return [("external", token)]
219
+ if suffix == ".rs":
220
+ parts = token.split("::")
221
+ if parts[0] not in {"crate", "self", "super"}:
222
+ return [("external", token)]
223
+ base = "src" if parts[0] == "crate" else str(PurePosixPath(path).parent)
224
+ if parts[0] == "super":
225
+ base = str(PurePosixPath(base).parent)
226
+ for end in range(len(parts), 1, -1):
227
+ stem = canonical_repo_path(str(PurePosixPath(base).joinpath(*parts[1:end])))
228
+ for candidate in ((stem or "") + ".rs", (stem or "") + "/mod.rs"):
229
+ if candidate in self.files:
230
+ return [("resolved", candidate)]
231
+ return [("unresolved", None)]
232
+ return [("unresolved", None)]
233
+
234
+
235
+ def import_token(text: str, suffix: str, suffix_map=None) -> str | None:
236
+ """Select syntax by source language; Python must never hit JS's `from`."""
237
+ if suffix == ".py":
238
+ try:
239
+ statement = ast.parse(text).body[0]
240
+ except (SyntaxError, IndexError):
241
+ return None
242
+ if isinstance(statement, ast.ImportFrom):
243
+ return "." * statement.level + (statement.module or "")
244
+ if isinstance(statement, ast.Import) and len(statement.names) == 1:
245
+ return statement.names[0].name
246
+ return None
247
+ language = (suffix_languages() if suffix_map is None else suffix_map).get(suffix)
248
+ suffix = {"ruby": ".rb", "php": ".php"}.get(language, suffix)
249
+ patterns = {
250
+ ".go": r'^import\s+(?:(?:\w+|\.)\s+)?["`]?([^"`\s]+)["`]?$',
251
+ ".java": r"^import\s+(?:static\s+)?([\w.*]+)\s*;?$",
252
+ ".kt": r"^import\s+([\w.*]+)(?:\s+as\s+\w+)?$",
253
+ ".kts": r"^import\s+([\w.*]+)(?:\s+as\s+\w+)?$",
254
+ ".cs": r"^(?:global\s+)?using\s+(?:static\s+)?(?:\w+\s*=\s*)?([\w.]+)\s*;?$",
255
+ ".rb": r'''^(?:require_relative|require)\s*\(?\s*["']([^"']+)["']''',
256
+ ".rs": r"^use\s+([\w:]+)(?:\s+as\s+\w+)?\s*;?$",
257
+ ".php": r"^use\s+(?:function\s+|const\s+)?([\w\\]+)(?:\s+as\s+\w+)?\s*;?$",
258
+ ".swift": r"^import\s+(?:(?:class|struct|func|enum|protocol|var|let|typealias)\s+)?([\w.]+)$",
259
+ }
260
+ pattern = patterns.get(suffix, r'''(?:require\s*\(\s*|from\s*|import\s*)["']([^"']+)["']''')
261
+ match = re.search(pattern, text)
262
+ if match:
263
+ return next((g for g in match.groups() if g), None)
264
+ return text if text and not re.search(r"[\s()#]", text) else None
265
+
266
+
267
+ def resolve_declarations(repo: Path, nodes: list[dict], edges: list[dict], *, files=None, diagnostics=None, suffix_map=None) -> tuple[list[dict], list[dict]]:
268
+ resolver = ImportResolver(repo, files, suffix_map)
269
+ known = {n["id"] for n in nodes}
270
+ result = []
271
+ for edge in edges:
272
+ if edge.get("kind") != "import" or edge.get("backend") != "joern":
273
+ result.append(edge)
274
+ continue
275
+ for index, (status, target) in enumerate(resolver.resolve(edge.get("path") or "", edge.get("expression") or "")):
276
+ target_id = (file_id(target, resolver.files) if status == "resolved" else external_id(target)) if target else None
277
+ if target_id and target_id not in known:
278
+ known.add(target_id)
279
+ nodes.append({"id": target_id, "kind": "file" if status == "resolved" else "external",
280
+ "path": target if status == "resolved" else None,
281
+ "name": target, "start_line": None, "end_line": None,
282
+ "backend": "joern", **({"external": True} if status == "external" else {})})
283
+ basis = resolver.include_basis.get((edge.get("path") or "", (edge.get("expression") or "").strip()))
284
+ result.append({**edge, "id": edge["id"] + f"/{index}", "status": status, "target": target_id,
285
+ **({"resolution_basis": basis} if basis else {})})
286
+ if diagnostics is not None:
287
+ diagnostics.extend(resolver.diagnostics)
288
+ return nodes, result