k-cli-for-devs 1.0.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.
- k_cli/__init__.py +77 -0
- k_cli/agents/__init__.py +0 -0
- k_cli/agents/adversarial_swarm.py +338 -0
- k_cli/agents/agent_core.py +255 -0
- k_cli/agents/background_daemon.py +141 -0
- k_cli/agents/orchestrator.py +376 -0
- k_cli/agents/persona.py +649 -0
- k_cli/agents/scaffold_engine.py +121 -0
- k_cli/agents/strands_agent.py +832 -0
- k_cli/agents/subagents.py +1496 -0
- k_cli/cli.py +3297 -0
- k_cli/core/__init__.py +0 -0
- k_cli/core/airgap.py +95 -0
- k_cli/core/credentials.py +548 -0
- k_cli/core/intent_sensor.py +177 -0
- k_cli/core/llm_driver.py +1028 -0
- k_cli/core/model_manager.py +1109 -0
- k_cli/core/models_hub.py +913 -0
- k_cli/core/prompting.py +41 -0
- k_cli/core/sdk.py +322 -0
- k_cli/core/session.py +826 -0
- k_cli/core/smart_router.py +230 -0
- k_cli/core/storage_manager.py +176 -0
- k_cli/core/viewport_engine.py +117 -0
- k_cli/demo/demo_runner.py +579 -0
- k_cli/git/__init__.py +0 -0
- k_cli/git/ai_bisect.py +208 -0
- k_cli/git/conflict_resolver.py +1039 -0
- k_cli/git/git_guard.py +417 -0
- k_cli/git/patcher.py +1175 -0
- k_cli/git/repo_map.py +1780 -0
- k_cli/git/smart_git.py +928 -0
- k_cli/git/verifier.py +969 -0
- k_cli/github/__init__.py +0 -0
- k_cli/github/dedup_engine.py +787 -0
- k_cli/github/github_client.py +1702 -0
- k_cli/github/github_engine.py +641 -0
- k_cli/github/local_hub.py +209 -0
- k_cli/github/pr_watcher.py +129 -0
- k_cli/github/trending.py +205 -0
- k_cli/tools/__init__.py +0 -0
- k_cli/tools/audit.py +79 -0
- k_cli/tools/chaos_immunity.py +377 -0
- k_cli/tools/codebase_qa.py +106 -0
- k_cli/tools/command_runner.py +256 -0
- k_cli/tools/diagram_generator.py +547 -0
- k_cli/tools/doc_retriever.py +1332 -0
- k_cli/tools/feature.py +105 -0
- k_cli/tools/ghost_daemon.py +122 -0
- k_cli/tools/incident_triage.py +1365 -0
- k_cli/tools/mcp_client.py +1846 -0
- k_cli/tools/repo_gardener.py +142 -0
- k_cli/tools/rules.py +109 -0
- k_cli/tools/security.py +52 -0
- k_cli/tools/security_healer.py +999 -0
- k_cli/tools/synapse_graph.py +155 -0
- k_cli/tui/__init__.py +0 -0
- k_cli/tui/diff_viewer.py +223 -0
- k_cli/tui/tui.py +1145 -0
- k_cli/tui/tui_animations.py +648 -0
- k_cli/tui/tui_app.py +2788 -0
- k_cli/ui/__init__.py +10 -0
- k_cli/ui/simple_repl.py +315 -0
- k_cli/web/__init__.py +7 -0
- k_cli/web/server.py +624 -0
- k_cli/web/static/app.js +830 -0
- k_cli/web/static/index.html +495 -0
- k_cli/web/static/monitor.html +189 -0
- k_cli/web/static/style.css +838 -0
- k_cli_for_devs-1.0.0.dist-info/METADATA +461 -0
- k_cli_for_devs-1.0.0.dist-info/RECORD +75 -0
- k_cli_for_devs-1.0.0.dist-info/WHEEL +5 -0
- k_cli_for_devs-1.0.0.dist-info/entry_points.txt +2 -0
- k_cli_for_devs-1.0.0.dist-info/licenses/LICENSE +21 -0
- k_cli_for_devs-1.0.0.dist-info/top_level.txt +1 -0
k_cli/git/repo_map.py
ADDED
|
@@ -0,0 +1,1780 @@
|
|
|
1
|
+
"""
|
|
2
|
+
repo_map.py - Advanced Semantic Codebase Repository Map & AST Navigator for K-CLI.
|
|
3
|
+
|
|
4
|
+
Features:
|
|
5
|
+
1. Deep AST & Regex Symbol Extraction across Python, JS/TS, C/C++, Rust, and Go
|
|
6
|
+
(Classes, Methods, Async Functions, Structs, Enums, Traits, Interfaces, Type Signatures).
|
|
7
|
+
2. Dependency Graph Analysis (Import tree resolution, caller-callee mapping, cyclic import detection).
|
|
8
|
+
3. Compact Token-Optimized Topological Summary for LLM context injection.
|
|
9
|
+
4. Incremental Hashing Cache (mtime + blake2b/sha256 hashing) for sub-millisecond updates.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import ast
|
|
15
|
+
import hashlib
|
|
16
|
+
import logging
|
|
17
|
+
import os
|
|
18
|
+
import re
|
|
19
|
+
from collections import defaultdict, deque
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any, Dict, List, Optional, Set, Tuple
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class CacheEntry:
|
|
29
|
+
"""Internal cache entry storing file metadata, content hash, and extracted AST symbols."""
|
|
30
|
+
mtime: float
|
|
31
|
+
size: int
|
|
32
|
+
content_hash: str
|
|
33
|
+
symbols: List[Dict[str, Any]]
|
|
34
|
+
file_info: Dict[str, Any]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class RepoMap:
|
|
38
|
+
"""
|
|
39
|
+
AST-driven Codebase Repository Map extractor, dependency analyzer, and ranker.
|
|
40
|
+
|
|
41
|
+
Provides fast, memory-efficient multi-language symbol extraction, dependency graph
|
|
42
|
+
analysis (caller-callee, cyclic imports), topological summary generation, and
|
|
43
|
+
incremental hashing cache for sub-millisecond latency.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
DEFAULT_IGNORED_DIRS: Set[str] = {
|
|
47
|
+
".git",
|
|
48
|
+
".agents",
|
|
49
|
+
".pytest_cache",
|
|
50
|
+
".venv",
|
|
51
|
+
"k_cli_env",
|
|
52
|
+
"venv",
|
|
53
|
+
"env",
|
|
54
|
+
"data",
|
|
55
|
+
".pytest_cache",
|
|
56
|
+
"__pycache__",
|
|
57
|
+
"build",
|
|
58
|
+
"dist",
|
|
59
|
+
"node_modules",
|
|
60
|
+
".tox",
|
|
61
|
+
".idea",
|
|
62
|
+
".vscode",
|
|
63
|
+
".mypy_cache",
|
|
64
|
+
".ruff_cache",
|
|
65
|
+
"site-packages",
|
|
66
|
+
".eggs",
|
|
67
|
+
"target",
|
|
68
|
+
"vendor",
|
|
69
|
+
".next",
|
|
70
|
+
".turbo",
|
|
71
|
+
".cache",
|
|
72
|
+
".agents",
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
SUPPORTED_EXTENSIONS: Set[str] = {
|
|
76
|
+
".py",
|
|
77
|
+
".pyi",
|
|
78
|
+
".js",
|
|
79
|
+
".jsx",
|
|
80
|
+
".ts",
|
|
81
|
+
".tsx",
|
|
82
|
+
".mjs",
|
|
83
|
+
".cjs",
|
|
84
|
+
".c",
|
|
85
|
+
".cc",
|
|
86
|
+
".cpp",
|
|
87
|
+
".cxx",
|
|
88
|
+
".h",
|
|
89
|
+
".hh",
|
|
90
|
+
".hpp",
|
|
91
|
+
".hxx",
|
|
92
|
+
".rs",
|
|
93
|
+
".go",
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
def __init__(
|
|
97
|
+
self,
|
|
98
|
+
root_dir: str = ".",
|
|
99
|
+
ignored_dirs: Optional[Set[str]] = None,
|
|
100
|
+
supported_extensions: Optional[Set[str]] = None,
|
|
101
|
+
) -> None:
|
|
102
|
+
"""
|
|
103
|
+
Initializes the RepoMap with a workspace root directory.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
root_dir: Path to the workspace root directory.
|
|
107
|
+
ignored_dirs: Optional set of directory names to ignore during scanning.
|
|
108
|
+
supported_extensions: Optional set of file extensions to include.
|
|
109
|
+
"""
|
|
110
|
+
self.root_dir = Path(root_dir).resolve()
|
|
111
|
+
if ignored_dirs is not None:
|
|
112
|
+
self.ignored_dirs = set(ignored_dirs)
|
|
113
|
+
else:
|
|
114
|
+
self.ignored_dirs = set(self.DEFAULT_IGNORED_DIRS)
|
|
115
|
+
|
|
116
|
+
if supported_extensions is not None:
|
|
117
|
+
self.supported_extensions = set(supported_extensions)
|
|
118
|
+
else:
|
|
119
|
+
self.supported_extensions = set(self.SUPPORTED_EXTENSIONS)
|
|
120
|
+
|
|
121
|
+
# Incremental Cache: str_path -> CacheEntry
|
|
122
|
+
self._cache: Dict[str, CacheEntry] = {}
|
|
123
|
+
self._cache_hits: int = 0
|
|
124
|
+
self._cache_misses: int = 0
|
|
125
|
+
|
|
126
|
+
# ==========================================================================
|
|
127
|
+
# Workspace Traversal & File Filtering
|
|
128
|
+
# ==========================================================================
|
|
129
|
+
|
|
130
|
+
def _should_skip_dir(self, dir_name: str, full_dir_path: Optional[str] = None) -> bool:
|
|
131
|
+
"""Determines if a directory should be skipped during workspace traversal."""
|
|
132
|
+
if dir_name.startswith("."):
|
|
133
|
+
return True
|
|
134
|
+
if dir_name.endswith(".egg-info"):
|
|
135
|
+
return True
|
|
136
|
+
if dir_name in self.ignored_dirs:
|
|
137
|
+
return True
|
|
138
|
+
if full_dir_path:
|
|
139
|
+
# Detect virtualenvs dynamically by checking common venv signatures
|
|
140
|
+
if os.path.isfile(os.path.join(full_dir_path, "pyvenv.cfg")):
|
|
141
|
+
return True
|
|
142
|
+
if os.path.isfile(os.path.join(full_dir_path, "bin", "activate")) or os.path.isfile(
|
|
143
|
+
os.path.join(full_dir_path, "Scripts", "activate")
|
|
144
|
+
):
|
|
145
|
+
return True
|
|
146
|
+
return False
|
|
147
|
+
|
|
148
|
+
def _should_skip_file(self, file_name: str) -> bool:
|
|
149
|
+
"""Determines if a file should be skipped."""
|
|
150
|
+
if file_name.startswith("."):
|
|
151
|
+
return True
|
|
152
|
+
_, ext = os.path.splitext(file_name)
|
|
153
|
+
return ext.lower() not in self.supported_extensions
|
|
154
|
+
|
|
155
|
+
def _is_binary(self, bytes_sample: bytes) -> bool:
|
|
156
|
+
"""Checks if a byte sample contains null bytes or binary characters."""
|
|
157
|
+
return b"\x00" in bytes_sample
|
|
158
|
+
|
|
159
|
+
def scan_workspace_files(self) -> List[str]:
|
|
160
|
+
"""
|
|
161
|
+
Recursively scans workspace directory for candidate source code files,
|
|
162
|
+
skipping ignored directories and hidden/binary files.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
Sorted list of absolute file paths to valid source files.
|
|
166
|
+
"""
|
|
167
|
+
if not self.root_dir.exists() or not self.root_dir.is_dir():
|
|
168
|
+
return []
|
|
169
|
+
|
|
170
|
+
source_files: List[str] = []
|
|
171
|
+
for root, dirs, files in os.walk(str(self.root_dir)):
|
|
172
|
+
# Filter dirs in-place to prevent walking ignored subtrees
|
|
173
|
+
dirs[:] = [d for d in dirs if not self._should_skip_dir(d, os.path.join(root, d))]
|
|
174
|
+
for file_name in files:
|
|
175
|
+
if not self._should_skip_file(file_name):
|
|
176
|
+
full_path = os.path.join(root, file_name)
|
|
177
|
+
source_files.append(full_path)
|
|
178
|
+
|
|
179
|
+
source_files.sort()
|
|
180
|
+
return source_files
|
|
181
|
+
|
|
182
|
+
# ==========================================================================
|
|
183
|
+
# Incremental Cache Management
|
|
184
|
+
# ==========================================================================
|
|
185
|
+
|
|
186
|
+
def invalidate_cache(self, file_path: Optional[str] = None) -> None:
|
|
187
|
+
"""Invalidates cache for a specific file or clears entire cache if None."""
|
|
188
|
+
if file_path is None:
|
|
189
|
+
self._cache.clear()
|
|
190
|
+
self._cache_hits = 0
|
|
191
|
+
self._cache_misses = 0
|
|
192
|
+
else:
|
|
193
|
+
path = Path(file_path)
|
|
194
|
+
if not path.is_absolute():
|
|
195
|
+
path = (self.root_dir / path).resolve()
|
|
196
|
+
self._cache.pop(str(path), None)
|
|
197
|
+
|
|
198
|
+
def clear_cache(self) -> None:
|
|
199
|
+
"""Clears all cached symbols and statistics."""
|
|
200
|
+
self.invalidate_cache(None)
|
|
201
|
+
|
|
202
|
+
def get_cache_stats(self) -> Dict[str, int]:
|
|
203
|
+
"""Returns cache hit/miss statistics and entry count."""
|
|
204
|
+
return {
|
|
205
|
+
"hits": self._cache_hits,
|
|
206
|
+
"misses": self._cache_misses,
|
|
207
|
+
"cached_files": len(self._cache),
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
# ==========================================================================
|
|
211
|
+
# Symbol & Metadata Extraction (Multi-Language)
|
|
212
|
+
# ==========================================================================
|
|
213
|
+
|
|
214
|
+
def extract_symbols(self, file_path: str) -> List[Dict[str, Any]]:
|
|
215
|
+
"""
|
|
216
|
+
Extracts structured symbol metadata from a source file.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
file_path: Relative or absolute path to the source file.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
List of symbol dictionaries for classes, structs, methods, functions,
|
|
223
|
+
enums, traits, interfaces, and type aliases.
|
|
224
|
+
Returns an empty list on syntax errors, missing files, or binary files.
|
|
225
|
+
"""
|
|
226
|
+
symbols, _ = self._extract_file_info(file_path)
|
|
227
|
+
return symbols
|
|
228
|
+
|
|
229
|
+
def _extract_file_info(self, file_path: str) -> Tuple[List[Dict[str, Any]], Dict[str, Any]]:
|
|
230
|
+
"""
|
|
231
|
+
Extracts symbols and cross-file reference metadata from a file.
|
|
232
|
+
Uses dual-tier fast stat + content hash caching for sub-millisecond lookups.
|
|
233
|
+
"""
|
|
234
|
+
path = Path(file_path)
|
|
235
|
+
if not path.is_absolute():
|
|
236
|
+
path = (self.root_dir / path).resolve()
|
|
237
|
+
|
|
238
|
+
if not path.exists() or not path.is_file():
|
|
239
|
+
return [], {}
|
|
240
|
+
|
|
241
|
+
try:
|
|
242
|
+
stat_res = path.stat()
|
|
243
|
+
mtime = stat_res.st_mtime
|
|
244
|
+
size = stat_res.st_size
|
|
245
|
+
except OSError:
|
|
246
|
+
return [], {}
|
|
247
|
+
|
|
248
|
+
str_path = str(path)
|
|
249
|
+
cached = self._cache.get(str_path)
|
|
250
|
+
|
|
251
|
+
# Tier 1: Fast stat check (sub-millisecond)
|
|
252
|
+
if cached is not None and cached.mtime == mtime and cached.size == size:
|
|
253
|
+
self._cache_hits += 1
|
|
254
|
+
return cached.symbols, cached.file_info
|
|
255
|
+
|
|
256
|
+
# Read content safely
|
|
257
|
+
try:
|
|
258
|
+
with open(path, "rb") as f:
|
|
259
|
+
raw_bytes = f.read()
|
|
260
|
+
except OSError:
|
|
261
|
+
return [], {}
|
|
262
|
+
|
|
263
|
+
if self._is_binary(raw_bytes[:8192]):
|
|
264
|
+
return [], {}
|
|
265
|
+
|
|
266
|
+
# Tier 2: Content Hash check (SHA-256 / Blake2b)
|
|
267
|
+
content_hash = hashlib.sha256(raw_bytes).hexdigest()
|
|
268
|
+
if cached is not None and cached.content_hash == content_hash:
|
|
269
|
+
self._cache_hits += 1
|
|
270
|
+
# Update mtime and size in cache without re-parsing
|
|
271
|
+
cached.mtime = mtime
|
|
272
|
+
cached.size = size
|
|
273
|
+
return cached.symbols, cached.file_info
|
|
274
|
+
|
|
275
|
+
self._cache_misses += 1
|
|
276
|
+
|
|
277
|
+
try:
|
|
278
|
+
source = raw_bytes.decode("utf-8-sig")
|
|
279
|
+
except UnicodeDecodeError:
|
|
280
|
+
try:
|
|
281
|
+
source = raw_bytes.decode("latin-1")
|
|
282
|
+
except Exception:
|
|
283
|
+
return [], {}
|
|
284
|
+
|
|
285
|
+
source = source.lstrip("\ufeff")
|
|
286
|
+
if not source.strip():
|
|
287
|
+
entry = CacheEntry(
|
|
288
|
+
mtime=mtime,
|
|
289
|
+
size=size,
|
|
290
|
+
content_hash=content_hash,
|
|
291
|
+
symbols=[],
|
|
292
|
+
file_info={},
|
|
293
|
+
)
|
|
294
|
+
self._cache[str_path] = entry
|
|
295
|
+
return [], {}
|
|
296
|
+
|
|
297
|
+
ext = path.suffix.lower()
|
|
298
|
+
symbols: List[Dict[str, Any]] = []
|
|
299
|
+
file_info: Dict[str, Any] = {}
|
|
300
|
+
|
|
301
|
+
if ext in (".py", ".pyi"):
|
|
302
|
+
symbols, file_info = self._parse_python(source, str_path)
|
|
303
|
+
elif ext in (".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"):
|
|
304
|
+
symbols, file_info = self._parse_javascript_typescript(source, str_path, ext)
|
|
305
|
+
elif ext == ".rs":
|
|
306
|
+
symbols, file_info = self._parse_rust(source, str_path)
|
|
307
|
+
elif ext == ".go":
|
|
308
|
+
symbols, file_info = self._parse_go(source, str_path)
|
|
309
|
+
elif ext in (".c", ".cc", ".cpp", ".cxx", ".h", ".hh", ".hpp", ".hxx"):
|
|
310
|
+
symbols, file_info = self._parse_cpp(source, str_path)
|
|
311
|
+
else:
|
|
312
|
+
symbols, file_info = [], {}
|
|
313
|
+
|
|
314
|
+
entry = CacheEntry(
|
|
315
|
+
mtime=mtime,
|
|
316
|
+
size=size,
|
|
317
|
+
content_hash=content_hash,
|
|
318
|
+
symbols=symbols,
|
|
319
|
+
file_info=file_info,
|
|
320
|
+
)
|
|
321
|
+
self._cache[str_path] = entry
|
|
322
|
+
return symbols, file_info
|
|
323
|
+
|
|
324
|
+
# --------------------------------------------------------------------------
|
|
325
|
+
# Language Parsers
|
|
326
|
+
# --------------------------------------------------------------------------
|
|
327
|
+
|
|
328
|
+
def _parse_python(self, source: str, str_path: str) -> Tuple[List[Dict[str, Any]], Dict[str, Any]]:
|
|
329
|
+
"""Parses Python source using the native `ast` module."""
|
|
330
|
+
try:
|
|
331
|
+
tree = ast.parse(source, filename=str_path)
|
|
332
|
+
except (SyntaxError, ValueError, MemoryError, RecursionError) as e:
|
|
333
|
+
logger.debug("Skipping syntax error / invalid AST in %s: %s", str_path, e)
|
|
334
|
+
return [], {}
|
|
335
|
+
|
|
336
|
+
symbols: List[Dict[str, Any]] = []
|
|
337
|
+
defined_names: Set[str] = set()
|
|
338
|
+
imported_names: Set[str] = set()
|
|
339
|
+
referenced_names: Set[str] = set()
|
|
340
|
+
raw_imports: List[str] = []
|
|
341
|
+
caller_callees: Dict[str, Set[str]] = defaultdict(set)
|
|
342
|
+
|
|
343
|
+
# Collect references, imports, and calls from full AST
|
|
344
|
+
for node in ast.walk(tree):
|
|
345
|
+
if isinstance(node, ast.Import):
|
|
346
|
+
for alias in node.names:
|
|
347
|
+
imported_names.add(alias.name.split(".")[0])
|
|
348
|
+
raw_imports.append(alias.name)
|
|
349
|
+
if alias.asname:
|
|
350
|
+
imported_names.add(alias.asname)
|
|
351
|
+
elif isinstance(node, ast.ImportFrom):
|
|
352
|
+
mod = node.module or ""
|
|
353
|
+
if mod:
|
|
354
|
+
imported_names.add(mod.split(".")[0])
|
|
355
|
+
raw_imports.append(mod)
|
|
356
|
+
for alias in node.names:
|
|
357
|
+
imported_names.add(alias.name)
|
|
358
|
+
if mod:
|
|
359
|
+
raw_imports.append(f"{mod}.{alias.name}")
|
|
360
|
+
if alias.asname:
|
|
361
|
+
imported_names.add(alias.asname)
|
|
362
|
+
elif isinstance(node, ast.Name):
|
|
363
|
+
referenced_names.add(node.id)
|
|
364
|
+
elif isinstance(node, ast.Attribute):
|
|
365
|
+
referenced_names.add(node.attr)
|
|
366
|
+
|
|
367
|
+
# Helper to extract calls inside a function or method body
|
|
368
|
+
def _extract_body_calls(body_nodes: List[ast.stmt]) -> Set[str]:
|
|
369
|
+
calls: Set[str] = set()
|
|
370
|
+
for b_node in body_nodes:
|
|
371
|
+
for sub in ast.walk(b_node):
|
|
372
|
+
if isinstance(sub, ast.Call):
|
|
373
|
+
if isinstance(sub.func, ast.Name):
|
|
374
|
+
calls.add(sub.func.id)
|
|
375
|
+
elif isinstance(sub.func, ast.Attribute):
|
|
376
|
+
calls.add(sub.func.attr)
|
|
377
|
+
return calls
|
|
378
|
+
|
|
379
|
+
# Traverse top-level nodes for symbol extraction
|
|
380
|
+
for node in tree.body:
|
|
381
|
+
if isinstance(node, ast.ClassDef):
|
|
382
|
+
class_sym, class_methods = self._parse_python_class(node)
|
|
383
|
+
symbols.append(class_sym)
|
|
384
|
+
symbols.extend(class_methods)
|
|
385
|
+
defined_names.add(node.name)
|
|
386
|
+
for m in node.body:
|
|
387
|
+
if isinstance(m, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
388
|
+
full_name = f"{node.name}.{m.name}"
|
|
389
|
+
defined_names.add(m.name)
|
|
390
|
+
defined_names.add(full_name)
|
|
391
|
+
caller_callees[full_name].update(_extract_body_calls(m.body))
|
|
392
|
+
elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
393
|
+
func_sym = self._parse_python_func(node, parent_class=None)
|
|
394
|
+
symbols.append(func_sym)
|
|
395
|
+
defined_names.add(node.name)
|
|
396
|
+
caller_callees[node.name].update(_extract_body_calls(node.body))
|
|
397
|
+
|
|
398
|
+
file_info: Dict[str, Any] = {
|
|
399
|
+
"defined_names": defined_names,
|
|
400
|
+
"imported_names": imported_names,
|
|
401
|
+
"referenced_names": referenced_names,
|
|
402
|
+
"raw_imports": raw_imports,
|
|
403
|
+
"caller_callee": {k: sorted(v) for k, v in caller_callees.items()},
|
|
404
|
+
"line_count": len(source.splitlines()),
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return symbols, file_info
|
|
408
|
+
|
|
409
|
+
def _parse_python_class(self, node: ast.ClassDef) -> Tuple[Dict[str, Any], List[Dict[str, Any]]]:
|
|
410
|
+
"""Parses a Python ClassDef AST node into a class symbol and method symbols."""
|
|
411
|
+
class_name = node.name
|
|
412
|
+
lineno = node.lineno
|
|
413
|
+
end_lineno = getattr(node, "end_lineno", lineno)
|
|
414
|
+
docstring = ast.get_docstring(node)
|
|
415
|
+
bases = [ast.unparse(b) for b in node.bases]
|
|
416
|
+
decorators = [ast.unparse(d) for d in node.decorator_list]
|
|
417
|
+
|
|
418
|
+
bases_suffix = f"({', '.join(bases)})" if bases else ""
|
|
419
|
+
signature = f"class {class_name}{bases_suffix}:"
|
|
420
|
+
|
|
421
|
+
methods: List[Dict[str, Any]] = []
|
|
422
|
+
for item in node.body:
|
|
423
|
+
if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
424
|
+
method_sym = self._parse_python_func(item, parent_class=class_name)
|
|
425
|
+
methods.append(method_sym)
|
|
426
|
+
|
|
427
|
+
class_sym: Dict[str, Any] = {
|
|
428
|
+
"name": class_name,
|
|
429
|
+
"type": "class",
|
|
430
|
+
"parent": None,
|
|
431
|
+
"class_name": None,
|
|
432
|
+
"lineno": lineno,
|
|
433
|
+
"line_number": lineno,
|
|
434
|
+
"end_lineno": end_lineno,
|
|
435
|
+
"signature": signature,
|
|
436
|
+
"docstring": docstring,
|
|
437
|
+
"bases": bases,
|
|
438
|
+
"decorators": decorators,
|
|
439
|
+
"is_async": False,
|
|
440
|
+
"methods": methods,
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return class_sym, methods
|
|
444
|
+
|
|
445
|
+
def _parse_python_func(
|
|
446
|
+
self,
|
|
447
|
+
node: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
448
|
+
parent_class: Optional[str] = None,
|
|
449
|
+
) -> Dict[str, Any]:
|
|
450
|
+
"""Parses a Python FunctionDef or AsyncFunctionDef AST node."""
|
|
451
|
+
is_async = isinstance(node, ast.AsyncFunctionDef)
|
|
452
|
+
name = node.name
|
|
453
|
+
lineno = node.lineno
|
|
454
|
+
end_lineno = getattr(node, "end_lineno", lineno)
|
|
455
|
+
docstring = ast.get_docstring(node)
|
|
456
|
+
decorators = [ast.unparse(d) for d in node.decorator_list]
|
|
457
|
+
|
|
458
|
+
args_str = ast.unparse(node.args)
|
|
459
|
+
ret_str = f" -> {ast.unparse(node.returns)}" if node.returns else ""
|
|
460
|
+
prefix = "async def" if is_async else "def"
|
|
461
|
+
signature = f"{prefix} {name}({args_str}){ret_str}:"
|
|
462
|
+
|
|
463
|
+
all_args = [a.arg for a in node.args.posonlyargs + node.args.args + node.args.kwonlyargs]
|
|
464
|
+
return_type = ast.unparse(node.returns) if node.returns else None
|
|
465
|
+
|
|
466
|
+
node_type: str
|
|
467
|
+
if parent_class:
|
|
468
|
+
node_type = "async_method" if is_async else "method"
|
|
469
|
+
else:
|
|
470
|
+
node_type = "async_function" if is_async else "function"
|
|
471
|
+
|
|
472
|
+
return {
|
|
473
|
+
"name": name,
|
|
474
|
+
"type": node_type,
|
|
475
|
+
"parent": parent_class,
|
|
476
|
+
"class_name": parent_class,
|
|
477
|
+
"lineno": lineno,
|
|
478
|
+
"line_number": lineno,
|
|
479
|
+
"end_lineno": end_lineno,
|
|
480
|
+
"signature": signature,
|
|
481
|
+
"docstring": docstring,
|
|
482
|
+
"decorators": decorators,
|
|
483
|
+
"is_async": is_async,
|
|
484
|
+
"args": all_args,
|
|
485
|
+
"return_type": return_type,
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
# --------------------------------------------------------------------------
|
|
489
|
+
# JavaScript & TypeScript Parser
|
|
490
|
+
# --------------------------------------------------------------------------
|
|
491
|
+
|
|
492
|
+
def _parse_javascript_typescript(
|
|
493
|
+
self, source: str, str_path: str, ext: str
|
|
494
|
+
) -> Tuple[List[Dict[str, Any]], Dict[str, Any]]:
|
|
495
|
+
"""Parses JavaScript / TypeScript source for classes, interfaces, enums, functions, and imports."""
|
|
496
|
+
symbols: List[Dict[str, Any]] = []
|
|
497
|
+
defined_names: Set[str] = set()
|
|
498
|
+
imported_names: Set[str] = set()
|
|
499
|
+
referenced_names: Set[str] = set()
|
|
500
|
+
raw_imports: List[str] = []
|
|
501
|
+
caller_callees: Dict[str, Set[str]] = defaultdict(set)
|
|
502
|
+
|
|
503
|
+
lines = source.splitlines()
|
|
504
|
+
|
|
505
|
+
import_from_re = re.compile(r"""(?:import|export)\s+(?:(?:(?:\*\s+as\s+[\w$]+|[\w$,\s{}]+)\s+from\s+)?['"]([^'"]+)['"]|['"]([^'"]+)['"])""")
|
|
506
|
+
require_re = re.compile(r"""(?:const|let|var)\s+(?:[\w$,\s{}]+)\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)""")
|
|
507
|
+
|
|
508
|
+
for line in lines:
|
|
509
|
+
for match in import_from_re.finditer(line):
|
|
510
|
+
mod = match.group(1) or match.group(2)
|
|
511
|
+
if mod:
|
|
512
|
+
raw_imports.append(mod)
|
|
513
|
+
imported_names.add(os.path.basename(mod).split(".")[0])
|
|
514
|
+
for match in require_re.finditer(line):
|
|
515
|
+
mod = match.group(1)
|
|
516
|
+
if mod:
|
|
517
|
+
raw_imports.append(mod)
|
|
518
|
+
imported_names.add(os.path.basename(mod).split(".")[0])
|
|
519
|
+
|
|
520
|
+
class_re = re.compile(
|
|
521
|
+
r"^\s*(?:export\s+)?(?:default\s+)?(?:abstract\s+)?class\s+([A-Za-z0-9_$]+)(?:<[^>]+>)?(?:\s+extends\s+([A-Za-z0-9_$.<>]+))?(?:\s+implements\s+([A-Za-z0-9_$,.<>\s]+))?"
|
|
522
|
+
)
|
|
523
|
+
interface_re = re.compile(
|
|
524
|
+
r"^\s*(?:export\s+)?interface\s+([A-Za-z0-9_$]+)(?:<[^>]+>)?(?:\s+extends\s+([^{]+))?"
|
|
525
|
+
)
|
|
526
|
+
enum_re = re.compile(r"^\s*(?:export\s+)?(?:const\s+)?enum\s+([A-Za-z0-9_$]+)")
|
|
527
|
+
type_alias_re = re.compile(r"^\s*(?:export\s+)?type\s+([A-Za-z0-9_$]+)(?:<[^>]+>)?\s*=")
|
|
528
|
+
func_re = re.compile(
|
|
529
|
+
r"^\s*(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s*(?:\*\s*)?([A-Za-z0-9_$]+)\s*(?:<[^>]+>)?\s*\(([^)]*)\)(?:\s*:\s*([^{;]+))?"
|
|
530
|
+
)
|
|
531
|
+
arrow_func_re = re.compile(
|
|
532
|
+
r"^\s*(?:export\s+)?(?:const|let|var)\s+([A-Za-z0-9_$]+)\s*(?::\s*[^=]+)?\s*=\s*(?:async\s*)?(?:\(([^)]*)\)|([A-Za-z0-9_$]+))(?:\s*:\s*(.*?))?\s*=>"
|
|
533
|
+
)
|
|
534
|
+
method_re = re.compile(
|
|
535
|
+
r"^\s*(?:(?:public|private|protected|static|readonly|override|async)\s+)*(?:get\s+|set\s+)?([A-Za-z0-9_$]+)\s*(?:<[^>]+>)?\s*\(([^)]*)\)(?:\s*:\s*([^{;]+))?"
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
non_methods = {
|
|
539
|
+
"if", "for", "while", "switch", "catch", "return", "super", "require",
|
|
540
|
+
"import", "export", "typeof", "delete", "this", "new", "throw", "yield", "await"
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
current_class: Optional[Dict[str, Any]] = None
|
|
544
|
+
current_class_methods: List[Dict[str, Any]] = []
|
|
545
|
+
brace_depth = 0
|
|
546
|
+
class_brace_start = 0
|
|
547
|
+
|
|
548
|
+
for i, line in enumerate(lines, start=1):
|
|
549
|
+
trimmed = line.strip()
|
|
550
|
+
if not trimmed or trimmed.startswith("//") or trimmed.startswith("/*") or trimmed.startswith("*"):
|
|
551
|
+
continue
|
|
552
|
+
|
|
553
|
+
# Check for Class
|
|
554
|
+
m_class = class_re.match(line)
|
|
555
|
+
if m_class and not current_class:
|
|
556
|
+
name = m_class.group(1)
|
|
557
|
+
base = m_class.group(2)
|
|
558
|
+
impl = m_class.group(3)
|
|
559
|
+
bases = []
|
|
560
|
+
if base:
|
|
561
|
+
bases.append(base.strip())
|
|
562
|
+
if impl:
|
|
563
|
+
bases.extend([x.strip() for x in impl.split(",") if x.strip()])
|
|
564
|
+
bases_str = f" extends {base.strip()}" if base else ""
|
|
565
|
+
sig = f"class {name}{bases_str}:"
|
|
566
|
+
current_class = {
|
|
567
|
+
"name": name,
|
|
568
|
+
"type": "class",
|
|
569
|
+
"parent": None,
|
|
570
|
+
"class_name": None,
|
|
571
|
+
"lineno": i,
|
|
572
|
+
"line_number": i,
|
|
573
|
+
"end_lineno": i,
|
|
574
|
+
"signature": sig,
|
|
575
|
+
"docstring": None,
|
|
576
|
+
"bases": bases,
|
|
577
|
+
"decorators": [],
|
|
578
|
+
"is_async": False,
|
|
579
|
+
"methods": [],
|
|
580
|
+
}
|
|
581
|
+
defined_names.add(name)
|
|
582
|
+
current_class_methods = []
|
|
583
|
+
class_brace_start = brace_depth + 1
|
|
584
|
+
brace_depth += line.count("{") - line.count("}")
|
|
585
|
+
continue
|
|
586
|
+
|
|
587
|
+
# Check for Interface
|
|
588
|
+
m_iface = interface_re.match(line)
|
|
589
|
+
if m_iface and not current_class:
|
|
590
|
+
name = m_iface.group(1)
|
|
591
|
+
ext_str = m_iface.group(2)
|
|
592
|
+
bases = [x.strip() for x in ext_str.split(",") if x.strip()] if ext_str else []
|
|
593
|
+
sig = f"interface {name}:"
|
|
594
|
+
symbols.append({
|
|
595
|
+
"name": name,
|
|
596
|
+
"type": "interface",
|
|
597
|
+
"parent": None,
|
|
598
|
+
"class_name": None,
|
|
599
|
+
"lineno": i,
|
|
600
|
+
"line_number": i,
|
|
601
|
+
"end_lineno": i,
|
|
602
|
+
"signature": sig,
|
|
603
|
+
"docstring": None,
|
|
604
|
+
"bases": bases,
|
|
605
|
+
"decorators": [],
|
|
606
|
+
"is_async": False,
|
|
607
|
+
"methods": [],
|
|
608
|
+
})
|
|
609
|
+
defined_names.add(name)
|
|
610
|
+
|
|
611
|
+
# Check for Enum
|
|
612
|
+
m_enum = enum_re.match(line)
|
|
613
|
+
if m_enum and not current_class:
|
|
614
|
+
name = m_enum.group(1)
|
|
615
|
+
symbols.append({
|
|
616
|
+
"name": name,
|
|
617
|
+
"type": "enum",
|
|
618
|
+
"parent": None,
|
|
619
|
+
"class_name": None,
|
|
620
|
+
"lineno": i,
|
|
621
|
+
"line_number": i,
|
|
622
|
+
"end_lineno": i,
|
|
623
|
+
"signature": f"enum {name}:",
|
|
624
|
+
"docstring": None,
|
|
625
|
+
"bases": [],
|
|
626
|
+
"decorators": [],
|
|
627
|
+
"is_async": False,
|
|
628
|
+
"methods": [],
|
|
629
|
+
})
|
|
630
|
+
defined_names.add(name)
|
|
631
|
+
|
|
632
|
+
# Check for Type Alias
|
|
633
|
+
m_type = type_alias_re.match(line)
|
|
634
|
+
if m_type and not current_class:
|
|
635
|
+
name = m_type.group(1)
|
|
636
|
+
symbols.append({
|
|
637
|
+
"name": name,
|
|
638
|
+
"type": "type_alias",
|
|
639
|
+
"parent": None,
|
|
640
|
+
"class_name": None,
|
|
641
|
+
"lineno": i,
|
|
642
|
+
"line_number": i,
|
|
643
|
+
"end_lineno": i,
|
|
644
|
+
"signature": f"type {name} = ...",
|
|
645
|
+
"docstring": None,
|
|
646
|
+
"bases": [],
|
|
647
|
+
"decorators": [],
|
|
648
|
+
"is_async": False,
|
|
649
|
+
"methods": [],
|
|
650
|
+
})
|
|
651
|
+
defined_names.add(name)
|
|
652
|
+
|
|
653
|
+
# Check for Methods inside current class (only at class scope)
|
|
654
|
+
if current_class and brace_depth == class_brace_start:
|
|
655
|
+
m_meth = method_re.match(line)
|
|
656
|
+
if m_meth:
|
|
657
|
+
m_name = m_meth.group(1)
|
|
658
|
+
if m_name not in non_methods:
|
|
659
|
+
args_raw = m_meth.group(2) or ""
|
|
660
|
+
ret_raw = m_meth.group(3)
|
|
661
|
+
is_async = "async " in line[: line.find(m_name)]
|
|
662
|
+
ret_str = f" -> {ret_raw.strip()}" if ret_raw else ""
|
|
663
|
+
prefix = "async def" if is_async else "def"
|
|
664
|
+
sig = f"{prefix} {m_name}({args_raw.strip()}){ret_str}:"
|
|
665
|
+
method_sym = {
|
|
666
|
+
"name": m_name,
|
|
667
|
+
"type": "async_method" if is_async else "method",
|
|
668
|
+
"parent": current_class["name"],
|
|
669
|
+
"class_name": current_class["name"],
|
|
670
|
+
"lineno": i,
|
|
671
|
+
"line_number": i,
|
|
672
|
+
"end_lineno": i,
|
|
673
|
+
"signature": sig,
|
|
674
|
+
"docstring": None,
|
|
675
|
+
"decorators": [],
|
|
676
|
+
"is_async": is_async,
|
|
677
|
+
"args": [a.split(":")[0].strip() for a in args_raw.split(",") if a.strip()],
|
|
678
|
+
"return_type": ret_raw.strip() if ret_raw else None,
|
|
679
|
+
}
|
|
680
|
+
current_class_methods.append(method_sym)
|
|
681
|
+
defined_names.add(f"{current_class['name']}.{m_name}")
|
|
682
|
+
defined_names.add(m_name)
|
|
683
|
+
|
|
684
|
+
# Check for Top-Level Functions
|
|
685
|
+
if not current_class:
|
|
686
|
+
m_fn = func_re.match(line)
|
|
687
|
+
if m_fn:
|
|
688
|
+
name = m_fn.group(1)
|
|
689
|
+
args_raw = m_fn.group(2) or ""
|
|
690
|
+
ret_raw = m_fn.group(3)
|
|
691
|
+
is_async = "async " in line[: line.find(name)]
|
|
692
|
+
ret_str = f" -> {ret_raw.strip()}" if ret_raw else ""
|
|
693
|
+
prefix = "async def" if is_async else "def"
|
|
694
|
+
sig = f"{prefix} {name}({args_raw.strip()}){ret_str}:"
|
|
695
|
+
symbols.append({
|
|
696
|
+
"name": name,
|
|
697
|
+
"type": "async_function" if is_async else "function",
|
|
698
|
+
"parent": None,
|
|
699
|
+
"class_name": None,
|
|
700
|
+
"lineno": i,
|
|
701
|
+
"line_number": i,
|
|
702
|
+
"end_lineno": i,
|
|
703
|
+
"signature": sig,
|
|
704
|
+
"docstring": None,
|
|
705
|
+
"decorators": [],
|
|
706
|
+
"is_async": is_async,
|
|
707
|
+
"args": [a.split(":")[0].strip() for a in args_raw.split(",") if a.strip()],
|
|
708
|
+
"return_type": ret_raw.strip() if ret_raw else None,
|
|
709
|
+
})
|
|
710
|
+
defined_names.add(name)
|
|
711
|
+
|
|
712
|
+
# Check for Arrow Functions
|
|
713
|
+
m_arrow = arrow_func_re.match(line)
|
|
714
|
+
if m_arrow:
|
|
715
|
+
name = m_arrow.group(1)
|
|
716
|
+
args_raw = m_arrow.group(2) or m_arrow.group(3) or ""
|
|
717
|
+
ret_raw = m_arrow.group(4)
|
|
718
|
+
is_async = "async" in line[: line.find("=>")]
|
|
719
|
+
ret_str = f" -> {ret_raw.strip()}" if ret_raw else ""
|
|
720
|
+
prefix = "async def" if is_async else "def"
|
|
721
|
+
sig = f"{prefix} {name}({args_raw.strip()}){ret_str}:"
|
|
722
|
+
symbols.append({
|
|
723
|
+
"name": name,
|
|
724
|
+
"type": "async_function" if is_async else "function",
|
|
725
|
+
"parent": None,
|
|
726
|
+
"class_name": None,
|
|
727
|
+
"lineno": i,
|
|
728
|
+
"line_number": i,
|
|
729
|
+
"end_lineno": i,
|
|
730
|
+
"signature": sig,
|
|
731
|
+
"docstring": None,
|
|
732
|
+
"decorators": [],
|
|
733
|
+
"is_async": is_async,
|
|
734
|
+
"args": [a.split(":")[0].strip() for a in args_raw.split(",") if a.strip()],
|
|
735
|
+
"return_type": ret_raw.strip() if ret_raw else None,
|
|
736
|
+
})
|
|
737
|
+
defined_names.add(name)
|
|
738
|
+
|
|
739
|
+
# Update brace depth and check for class end
|
|
740
|
+
brace_depth += line.count("{") - line.count("}")
|
|
741
|
+
if current_class and brace_depth < class_brace_start:
|
|
742
|
+
current_class["end_lineno"] = i
|
|
743
|
+
current_class["methods"] = current_class_methods
|
|
744
|
+
symbols.append(current_class)
|
|
745
|
+
symbols.extend(current_class_methods)
|
|
746
|
+
current_class = None
|
|
747
|
+
current_class_methods = []
|
|
748
|
+
|
|
749
|
+
if current_class:
|
|
750
|
+
current_class["end_lineno"] = len(lines)
|
|
751
|
+
current_class["methods"] = current_class_methods
|
|
752
|
+
symbols.append(current_class)
|
|
753
|
+
symbols.extend(current_class_methods)
|
|
754
|
+
|
|
755
|
+
# Collect identifier references
|
|
756
|
+
ident_re = re.compile(r"\b([A-Za-z_$][A-Za-z0-9_$]*)\b")
|
|
757
|
+
for word in ident_re.findall(source):
|
|
758
|
+
referenced_names.add(word)
|
|
759
|
+
|
|
760
|
+
file_info = {
|
|
761
|
+
"defined_names": defined_names,
|
|
762
|
+
"imported_names": imported_names,
|
|
763
|
+
"referenced_names": referenced_names,
|
|
764
|
+
"raw_imports": raw_imports,
|
|
765
|
+
"caller_callee": {k: sorted(v) for k, v in caller_callees.items()},
|
|
766
|
+
"line_count": len(lines),
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
return symbols, file_info
|
|
770
|
+
|
|
771
|
+
# --------------------------------------------------------------------------
|
|
772
|
+
# Rust Parser
|
|
773
|
+
# --------------------------------------------------------------------------
|
|
774
|
+
|
|
775
|
+
def _parse_rust(self, source: str, str_path: str) -> Tuple[List[Dict[str, Any]], Dict[str, Any]]:
|
|
776
|
+
"""Parses Rust source for structs, enums, traits, impls, functions, and use statements."""
|
|
777
|
+
symbols: List[Dict[str, Any]] = []
|
|
778
|
+
defined_names: Set[str] = set()
|
|
779
|
+
imported_names: Set[str] = set()
|
|
780
|
+
referenced_names: Set[str] = set()
|
|
781
|
+
raw_imports: List[str] = []
|
|
782
|
+
caller_callees: Dict[str, Set[str]] = defaultdict(set)
|
|
783
|
+
|
|
784
|
+
lines = source.splitlines()
|
|
785
|
+
|
|
786
|
+
use_re = re.compile(r"^\s*use\s+([^;]+);")
|
|
787
|
+
mod_re = re.compile(r"^\s*(?:pub(?:\([^)]+\))?\s+)?mod\s+([A-Za-z0-9_]+);")
|
|
788
|
+
|
|
789
|
+
for line in lines:
|
|
790
|
+
m_use = use_re.match(line)
|
|
791
|
+
if m_use:
|
|
792
|
+
raw_imports.append(m_use.group(1).strip())
|
|
793
|
+
imported_names.add(m_use.group(1).split("::")[-1].strip())
|
|
794
|
+
m_mod = mod_re.match(line)
|
|
795
|
+
if m_mod:
|
|
796
|
+
raw_imports.append(m_mod.group(1).strip())
|
|
797
|
+
imported_names.add(m_mod.group(1).strip())
|
|
798
|
+
|
|
799
|
+
struct_re = re.compile(r"^\s*(?:pub(?:\([^)]+\))?\s+)?struct\s+([A-Za-z0-9_]+)(?:<[^>]+>)?")
|
|
800
|
+
enum_re = re.compile(r"^\s*(?:pub(?:\([^)]+\))?\s+)?enum\s+([A-Za-z0-9_]+)(?:<[^>]+>)?")
|
|
801
|
+
trait_re = re.compile(r"^\s*(?:pub(?:\([^)]+\))?\s+)?trait\s+([A-Za-z0-9_]+)(?:<[^>]+>)?")
|
|
802
|
+
impl_re = re.compile(r"^\s*impl(?:<[^>]+>)?\s+(?:([A-Za-z0-9_:]+(?:<[^>]+>)?)\s+for\s+)?([A-Za-z0-9_:]+(?:<[^>]+>)?)(?:\s+where\s+[^{]+)?\s*\{")
|
|
803
|
+
fn_re = re.compile(r"^\s*(?:pub(?:\([^)]+\))?\s+)?(?:async\s+)?(?:unsafe\s+)?(?:extern(?:\s+\"[^\"]+\")?\s+)?(?:const\s+)?fn\s+([A-Za-z0-9_]+)(?:<[^>]+>)?\s*\(([^)]*)\)(?:\s*->\s*([^{;]+))?")
|
|
804
|
+
|
|
805
|
+
current_impl_target: Optional[str] = None
|
|
806
|
+
brace_depth = 0
|
|
807
|
+
impl_brace_start = 0
|
|
808
|
+
|
|
809
|
+
struct_map: Dict[str, Dict[str, Any]] = {}
|
|
810
|
+
|
|
811
|
+
for i, line in enumerate(lines, start=1):
|
|
812
|
+
trimmed = line.strip()
|
|
813
|
+
if not trimmed or trimmed.startswith("//") or trimmed.startswith("/*") or trimmed.startswith("*"):
|
|
814
|
+
continue
|
|
815
|
+
|
|
816
|
+
# Struct
|
|
817
|
+
m_struct = struct_re.match(line)
|
|
818
|
+
if m_struct:
|
|
819
|
+
name = m_struct.group(1)
|
|
820
|
+
struct_sym = {
|
|
821
|
+
"name": name,
|
|
822
|
+
"type": "struct",
|
|
823
|
+
"parent": None,
|
|
824
|
+
"class_name": None,
|
|
825
|
+
"lineno": i,
|
|
826
|
+
"line_number": i,
|
|
827
|
+
"end_lineno": i,
|
|
828
|
+
"signature": f"struct {name}:",
|
|
829
|
+
"docstring": None,
|
|
830
|
+
"bases": [],
|
|
831
|
+
"decorators": [],
|
|
832
|
+
"is_async": False,
|
|
833
|
+
"methods": [],
|
|
834
|
+
}
|
|
835
|
+
symbols.append(struct_sym)
|
|
836
|
+
struct_map[name] = struct_sym
|
|
837
|
+
defined_names.add(name)
|
|
838
|
+
|
|
839
|
+
# Enum
|
|
840
|
+
m_enum = enum_re.match(line)
|
|
841
|
+
if m_enum:
|
|
842
|
+
name = m_enum.group(1)
|
|
843
|
+
symbols.append({
|
|
844
|
+
"name": name,
|
|
845
|
+
"type": "enum",
|
|
846
|
+
"parent": None,
|
|
847
|
+
"class_name": None,
|
|
848
|
+
"lineno": i,
|
|
849
|
+
"line_number": i,
|
|
850
|
+
"end_lineno": i,
|
|
851
|
+
"signature": f"enum {name}:",
|
|
852
|
+
"docstring": None,
|
|
853
|
+
"bases": [],
|
|
854
|
+
"decorators": [],
|
|
855
|
+
"is_async": False,
|
|
856
|
+
"methods": [],
|
|
857
|
+
})
|
|
858
|
+
defined_names.add(name)
|
|
859
|
+
|
|
860
|
+
# Trait
|
|
861
|
+
m_trait = trait_re.match(line)
|
|
862
|
+
if m_trait:
|
|
863
|
+
name = m_trait.group(1)
|
|
864
|
+
symbols.append({
|
|
865
|
+
"name": name,
|
|
866
|
+
"type": "trait",
|
|
867
|
+
"parent": None,
|
|
868
|
+
"class_name": None,
|
|
869
|
+
"lineno": i,
|
|
870
|
+
"line_number": i,
|
|
871
|
+
"end_lineno": i,
|
|
872
|
+
"signature": f"trait {name}:",
|
|
873
|
+
"docstring": None,
|
|
874
|
+
"bases": [],
|
|
875
|
+
"decorators": [],
|
|
876
|
+
"is_async": False,
|
|
877
|
+
"methods": [],
|
|
878
|
+
})
|
|
879
|
+
defined_names.add(name)
|
|
880
|
+
|
|
881
|
+
# Impl Block
|
|
882
|
+
m_impl = impl_re.match(line)
|
|
883
|
+
if m_impl:
|
|
884
|
+
trait_name = m_impl.group(1)
|
|
885
|
+
target_type = m_impl.group(2).split("<")[0].strip()
|
|
886
|
+
current_impl_target = target_type
|
|
887
|
+
impl_brace_start = brace_depth + line.count("{") - line.count("}")
|
|
888
|
+
|
|
889
|
+
# Function / Method
|
|
890
|
+
m_fn = fn_re.match(line)
|
|
891
|
+
if m_fn:
|
|
892
|
+
name = m_fn.group(1)
|
|
893
|
+
args_raw = m_fn.group(2) or ""
|
|
894
|
+
ret_raw = m_fn.group(3)
|
|
895
|
+
is_async = "async " in line[: line.find(name)]
|
|
896
|
+
ret_str = f" -> {ret_raw.strip()}" if ret_raw else ""
|
|
897
|
+
prefix = "async fn" if is_async else "fn"
|
|
898
|
+
sig = f"{prefix} {name}({args_raw.strip()}){ret_str}:"
|
|
899
|
+
|
|
900
|
+
if current_impl_target:
|
|
901
|
+
# Method inside impl
|
|
902
|
+
method_sym = {
|
|
903
|
+
"name": name,
|
|
904
|
+
"type": "async_method" if is_async else "method",
|
|
905
|
+
"parent": current_impl_target,
|
|
906
|
+
"class_name": current_impl_target,
|
|
907
|
+
"lineno": i,
|
|
908
|
+
"line_number": i,
|
|
909
|
+
"end_lineno": i,
|
|
910
|
+
"signature": sig,
|
|
911
|
+
"docstring": None,
|
|
912
|
+
"decorators": [],
|
|
913
|
+
"is_async": is_async,
|
|
914
|
+
"args": [a.split(":")[0].strip() for a in args_raw.split(",") if a.strip()],
|
|
915
|
+
"return_type": ret_raw.strip() if ret_raw else None,
|
|
916
|
+
}
|
|
917
|
+
symbols.append(method_sym)
|
|
918
|
+
if current_impl_target in struct_map:
|
|
919
|
+
struct_map[current_impl_target]["methods"].append(method_sym)
|
|
920
|
+
defined_names.add(f"{current_impl_target}.{name}")
|
|
921
|
+
defined_names.add(name)
|
|
922
|
+
else:
|
|
923
|
+
# Free Function
|
|
924
|
+
symbols.append({
|
|
925
|
+
"name": name,
|
|
926
|
+
"type": "async_function" if is_async else "function",
|
|
927
|
+
"parent": None,
|
|
928
|
+
"class_name": None,
|
|
929
|
+
"lineno": i,
|
|
930
|
+
"line_number": i,
|
|
931
|
+
"end_lineno": i,
|
|
932
|
+
"signature": sig,
|
|
933
|
+
"docstring": None,
|
|
934
|
+
"decorators": [],
|
|
935
|
+
"is_async": is_async,
|
|
936
|
+
"args": [a.split(":")[0].strip() for a in args_raw.split(",") if a.strip()],
|
|
937
|
+
"return_type": ret_raw.strip() if ret_raw else None,
|
|
938
|
+
})
|
|
939
|
+
defined_names.add(name)
|
|
940
|
+
|
|
941
|
+
brace_depth += line.count("{") - line.count("}")
|
|
942
|
+
if current_impl_target and brace_depth < impl_brace_start:
|
|
943
|
+
current_impl_target = None
|
|
944
|
+
|
|
945
|
+
# Identifiers
|
|
946
|
+
ident_re = re.compile(r"\b([A-Za-z_][A-Za-z0-9_]*)\b")
|
|
947
|
+
for word in ident_re.findall(source):
|
|
948
|
+
referenced_names.add(word)
|
|
949
|
+
|
|
950
|
+
file_info = {
|
|
951
|
+
"defined_names": defined_names,
|
|
952
|
+
"imported_names": imported_names,
|
|
953
|
+
"referenced_names": referenced_names,
|
|
954
|
+
"raw_imports": raw_imports,
|
|
955
|
+
"caller_callee": {k: sorted(v) for k, v in caller_callees.items()},
|
|
956
|
+
"line_count": len(lines),
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
return symbols, file_info
|
|
960
|
+
|
|
961
|
+
# --------------------------------------------------------------------------
|
|
962
|
+
# Go Parser
|
|
963
|
+
# --------------------------------------------------------------------------
|
|
964
|
+
|
|
965
|
+
def _parse_go(self, source: str, str_path: str) -> Tuple[List[Dict[str, Any]], Dict[str, Any]]:
|
|
966
|
+
"""Parses Go source for structs, interfaces, methods, functions, and imports."""
|
|
967
|
+
symbols: List[Dict[str, Any]] = []
|
|
968
|
+
defined_names: Set[str] = set()
|
|
969
|
+
imported_names: Set[str] = set()
|
|
970
|
+
referenced_names: Set[str] = set()
|
|
971
|
+
raw_imports: List[str] = []
|
|
972
|
+
caller_callees: Dict[str, Set[str]] = defaultdict(set)
|
|
973
|
+
|
|
974
|
+
lines = source.splitlines()
|
|
975
|
+
|
|
976
|
+
in_import_block = False
|
|
977
|
+
import_line_re = re.compile(r'^\s*(?:[A-Za-z0-9_.]+\s+)?"([^"]+)"')
|
|
978
|
+
|
|
979
|
+
for line in lines:
|
|
980
|
+
trimmed = line.strip()
|
|
981
|
+
if trimmed.startswith("import ("):
|
|
982
|
+
in_import_block = True
|
|
983
|
+
continue
|
|
984
|
+
if in_import_block:
|
|
985
|
+
if trimmed.startswith(")"):
|
|
986
|
+
in_import_block = False
|
|
987
|
+
else:
|
|
988
|
+
m = import_line_re.match(trimmed)
|
|
989
|
+
if m:
|
|
990
|
+
pkg = m.group(1)
|
|
991
|
+
raw_imports.append(pkg)
|
|
992
|
+
imported_names.add(os.path.basename(pkg))
|
|
993
|
+
elif trimmed.startswith("import "):
|
|
994
|
+
m = import_line_re.match(trimmed[7:].strip())
|
|
995
|
+
if m:
|
|
996
|
+
pkg = m.group(1)
|
|
997
|
+
raw_imports.append(pkg)
|
|
998
|
+
imported_names.add(os.path.basename(pkg))
|
|
999
|
+
|
|
1000
|
+
struct_re = re.compile(r"^\s*type\s+([A-Za-z0-9_]+)\s+struct\s*\{")
|
|
1001
|
+
iface_re = re.compile(r"^\s*type\s+([A-Za-z0-9_]+)\s+interface\s*\{")
|
|
1002
|
+
type_re = re.compile(r"^\s*type\s+([A-Za-z0-9_]+)\s+([^{;=]+)")
|
|
1003
|
+
method_re = re.compile(
|
|
1004
|
+
r"^\s*func\s*\(\s*(?:[A-Za-z0-9_]+\s+)?\*?([A-Za-z0-9_]+)\s*\)\s*([A-Za-z0-9_]+)\s*\(([^)]*)\)(?:\s*(?:\(([^)]*)\)|([^{;]+)))?"
|
|
1005
|
+
)
|
|
1006
|
+
func_re = re.compile(
|
|
1007
|
+
r"^\s*func\s+([A-Za-z0-9_]+)\s*\(([^)]*)\)(?:\s*(?:\(([^)]*)\)|([^{;]+)))?"
|
|
1008
|
+
)
|
|
1009
|
+
|
|
1010
|
+
struct_map: Dict[str, Dict[str, Any]] = {}
|
|
1011
|
+
|
|
1012
|
+
for i, line in enumerate(lines, start=1):
|
|
1013
|
+
trimmed = line.strip()
|
|
1014
|
+
if not trimmed or trimmed.startswith("//") or trimmed.startswith("/*"):
|
|
1015
|
+
continue
|
|
1016
|
+
|
|
1017
|
+
# Struct
|
|
1018
|
+
m_struct = struct_re.match(line)
|
|
1019
|
+
if m_struct:
|
|
1020
|
+
name = m_struct.group(1)
|
|
1021
|
+
struct_sym = {
|
|
1022
|
+
"name": name,
|
|
1023
|
+
"type": "struct",
|
|
1024
|
+
"parent": None,
|
|
1025
|
+
"class_name": None,
|
|
1026
|
+
"lineno": i,
|
|
1027
|
+
"line_number": i,
|
|
1028
|
+
"end_lineno": i,
|
|
1029
|
+
"signature": f"type {name} struct:",
|
|
1030
|
+
"docstring": None,
|
|
1031
|
+
"bases": [],
|
|
1032
|
+
"decorators": [],
|
|
1033
|
+
"is_async": False,
|
|
1034
|
+
"methods": [],
|
|
1035
|
+
}
|
|
1036
|
+
symbols.append(struct_sym)
|
|
1037
|
+
struct_map[name] = struct_sym
|
|
1038
|
+
defined_names.add(name)
|
|
1039
|
+
continue
|
|
1040
|
+
|
|
1041
|
+
# Interface
|
|
1042
|
+
m_iface = iface_re.match(line)
|
|
1043
|
+
if m_iface:
|
|
1044
|
+
name = m_iface.group(1)
|
|
1045
|
+
symbols.append({
|
|
1046
|
+
"name": name,
|
|
1047
|
+
"type": "interface",
|
|
1048
|
+
"parent": None,
|
|
1049
|
+
"class_name": None,
|
|
1050
|
+
"lineno": i,
|
|
1051
|
+
"line_number": i,
|
|
1052
|
+
"end_lineno": i,
|
|
1053
|
+
"signature": f"type {name} interface:",
|
|
1054
|
+
"docstring": None,
|
|
1055
|
+
"bases": [],
|
|
1056
|
+
"decorators": [],
|
|
1057
|
+
"is_async": False,
|
|
1058
|
+
"methods": [],
|
|
1059
|
+
})
|
|
1060
|
+
defined_names.add(name)
|
|
1061
|
+
continue
|
|
1062
|
+
|
|
1063
|
+
# Method (with receiver)
|
|
1064
|
+
m_meth = method_re.match(line)
|
|
1065
|
+
if m_meth:
|
|
1066
|
+
recv = m_meth.group(1)
|
|
1067
|
+
name = m_meth.group(2)
|
|
1068
|
+
args_raw = m_meth.group(3) or ""
|
|
1069
|
+
ret_raw = m_meth.group(4) or m_meth.group(5)
|
|
1070
|
+
ret_str = f" -> {ret_raw.strip()}" if ret_raw else ""
|
|
1071
|
+
sig = f"func (r *{recv}) {name}({args_raw.strip()}){ret_str}:"
|
|
1072
|
+
method_sym = {
|
|
1073
|
+
"name": name,
|
|
1074
|
+
"type": "method",
|
|
1075
|
+
"parent": recv,
|
|
1076
|
+
"class_name": recv,
|
|
1077
|
+
"lineno": i,
|
|
1078
|
+
"line_number": i,
|
|
1079
|
+
"end_lineno": i,
|
|
1080
|
+
"signature": sig,
|
|
1081
|
+
"docstring": None,
|
|
1082
|
+
"decorators": [],
|
|
1083
|
+
"is_async": False,
|
|
1084
|
+
"args": [a.split()[0].strip() for a in args_raw.split(",") if a.strip()],
|
|
1085
|
+
"return_type": ret_raw.strip() if ret_raw else None,
|
|
1086
|
+
}
|
|
1087
|
+
symbols.append(method_sym)
|
|
1088
|
+
if recv in struct_map:
|
|
1089
|
+
struct_map[recv]["methods"].append(method_sym)
|
|
1090
|
+
defined_names.add(f"{recv}.{name}")
|
|
1091
|
+
defined_names.add(name)
|
|
1092
|
+
continue
|
|
1093
|
+
|
|
1094
|
+
# Function
|
|
1095
|
+
m_fn = func_re.match(line)
|
|
1096
|
+
if m_fn:
|
|
1097
|
+
name = m_fn.group(1)
|
|
1098
|
+
args_raw = m_fn.group(2) or ""
|
|
1099
|
+
ret_raw = m_fn.group(3) or m_fn.group(4)
|
|
1100
|
+
ret_str = f" -> {ret_raw.strip()}" if ret_raw else ""
|
|
1101
|
+
sig = f"func {name}({args_raw.strip()}){ret_str}:"
|
|
1102
|
+
symbols.append({
|
|
1103
|
+
"name": name,
|
|
1104
|
+
"type": "function",
|
|
1105
|
+
"parent": None,
|
|
1106
|
+
"class_name": None,
|
|
1107
|
+
"lineno": i,
|
|
1108
|
+
"line_number": i,
|
|
1109
|
+
"end_lineno": i,
|
|
1110
|
+
"signature": sig,
|
|
1111
|
+
"docstring": None,
|
|
1112
|
+
"decorators": [],
|
|
1113
|
+
"is_async": False,
|
|
1114
|
+
"args": [a.split()[0].strip() for a in args_raw.split(",") if a.strip()],
|
|
1115
|
+
"return_type": ret_raw.strip() if ret_raw else None,
|
|
1116
|
+
})
|
|
1117
|
+
defined_names.add(name)
|
|
1118
|
+
|
|
1119
|
+
# Identifiers
|
|
1120
|
+
ident_re = re.compile(r"\b([A-Za-z_][A-Za-z0-9_]*)\b")
|
|
1121
|
+
for word in ident_re.findall(source):
|
|
1122
|
+
referenced_names.add(word)
|
|
1123
|
+
|
|
1124
|
+
file_info = {
|
|
1125
|
+
"defined_names": defined_names,
|
|
1126
|
+
"imported_names": imported_names,
|
|
1127
|
+
"referenced_names": referenced_names,
|
|
1128
|
+
"raw_imports": raw_imports,
|
|
1129
|
+
"caller_callee": {k: sorted(v) for k, v in caller_callees.items()},
|
|
1130
|
+
"line_count": len(lines),
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
return symbols, file_info
|
|
1134
|
+
|
|
1135
|
+
# --------------------------------------------------------------------------
|
|
1136
|
+
# C / C++ Parser
|
|
1137
|
+
# --------------------------------------------------------------------------
|
|
1138
|
+
|
|
1139
|
+
def _parse_cpp(self, source: str, str_path: str) -> Tuple[List[Dict[str, Any]], Dict[str, Any]]:
|
|
1140
|
+
"""Parses C/C++ source for classes, structs, enums, functions, and includes."""
|
|
1141
|
+
symbols: List[Dict[str, Any]] = []
|
|
1142
|
+
defined_names: Set[str] = set()
|
|
1143
|
+
imported_names: Set[str] = set()
|
|
1144
|
+
referenced_names: Set[str] = set()
|
|
1145
|
+
raw_imports: List[str] = []
|
|
1146
|
+
caller_callees: Dict[str, Set[str]] = defaultdict(set)
|
|
1147
|
+
|
|
1148
|
+
lines = source.splitlines()
|
|
1149
|
+
|
|
1150
|
+
include_re = re.compile(r'^\s*#include\s*["<]([^">]+)[">]')
|
|
1151
|
+
for line in lines:
|
|
1152
|
+
m_inc = include_re.match(line)
|
|
1153
|
+
if m_inc:
|
|
1154
|
+
inc_target = m_inc.group(1)
|
|
1155
|
+
raw_imports.append(inc_target)
|
|
1156
|
+
imported_names.add(os.path.basename(inc_target).split(".")[0])
|
|
1157
|
+
|
|
1158
|
+
class_re = re.compile(
|
|
1159
|
+
r"^\s*(?:template\s*<[^>]+>\s*)?(class|struct)\s+([A-Za-z0-9_]+)(?:\s*:\s*([^{;]+))?\s*\{"
|
|
1160
|
+
)
|
|
1161
|
+
enum_re = re.compile(
|
|
1162
|
+
r"^\s*enum\s+(?:class\s+|struct\s+)?([A-Za-z0-9_]+)(?:\s*:\s*[^{;]+)?\s*\{"
|
|
1163
|
+
)
|
|
1164
|
+
fn_re = re.compile(
|
|
1165
|
+
r"^\s*(?:template\s*<[^>]+>\s*)?(?:(?:static|virtual|inline|explicit|constexpr|friend|extern)\s+)*([A-Za-z0-9_<>:~&*]+)\s+([A-Za-z0-9_~]+)\s*\(([^)]*)\)(?:\s*const)?(?:\s*override|\s*final|\s*noexcept)?(?:\s*=\s*0)?\s*[{;]"
|
|
1166
|
+
)
|
|
1167
|
+
|
|
1168
|
+
current_class: Optional[Dict[str, Any]] = None
|
|
1169
|
+
current_class_methods: List[Dict[str, Any]] = []
|
|
1170
|
+
brace_depth = 0
|
|
1171
|
+
class_brace_start = 0
|
|
1172
|
+
|
|
1173
|
+
for i, line in enumerate(lines, start=1):
|
|
1174
|
+
trimmed = line.strip()
|
|
1175
|
+
if not trimmed or trimmed.startswith("//") or trimmed.startswith("/*") or trimmed.startswith("*") or trimmed.startswith("#"):
|
|
1176
|
+
continue
|
|
1177
|
+
|
|
1178
|
+
# Class / Struct
|
|
1179
|
+
m_cls = class_re.match(line)
|
|
1180
|
+
if m_cls and not current_class:
|
|
1181
|
+
kind = m_cls.group(1)
|
|
1182
|
+
name = m_cls.group(2)
|
|
1183
|
+
bases_raw = m_cls.group(3)
|
|
1184
|
+
bases = [b.strip().split()[-1] for b in bases_raw.split(",") if b.strip()] if bases_raw else []
|
|
1185
|
+
bases_str = f"({', '.join(bases)})" if bases else ""
|
|
1186
|
+
sig = f"{kind} {name}{bases_str}:"
|
|
1187
|
+
current_class = {
|
|
1188
|
+
"name": name,
|
|
1189
|
+
"type": "class" if kind == "class" else "struct",
|
|
1190
|
+
"parent": None,
|
|
1191
|
+
"class_name": None,
|
|
1192
|
+
"lineno": i,
|
|
1193
|
+
"line_number": i,
|
|
1194
|
+
"end_lineno": i,
|
|
1195
|
+
"signature": sig,
|
|
1196
|
+
"docstring": None,
|
|
1197
|
+
"bases": bases,
|
|
1198
|
+
"decorators": [],
|
|
1199
|
+
"is_async": False,
|
|
1200
|
+
"methods": [],
|
|
1201
|
+
}
|
|
1202
|
+
defined_names.add(name)
|
|
1203
|
+
current_class_methods = []
|
|
1204
|
+
class_brace_start = brace_depth + line.count("{") - line.count("}")
|
|
1205
|
+
brace_depth += line.count("{") - line.count("}")
|
|
1206
|
+
continue
|
|
1207
|
+
|
|
1208
|
+
# Enum
|
|
1209
|
+
m_enum = enum_re.match(line)
|
|
1210
|
+
if m_enum and not current_class:
|
|
1211
|
+
name = m_enum.group(1)
|
|
1212
|
+
symbols.append({
|
|
1213
|
+
"name": name,
|
|
1214
|
+
"type": "enum",
|
|
1215
|
+
"parent": None,
|
|
1216
|
+
"class_name": None,
|
|
1217
|
+
"lineno": i,
|
|
1218
|
+
"line_number": i,
|
|
1219
|
+
"end_lineno": i,
|
|
1220
|
+
"signature": f"enum {name}:",
|
|
1221
|
+
"docstring": None,
|
|
1222
|
+
"bases": [],
|
|
1223
|
+
"decorators": [],
|
|
1224
|
+
"is_async": False,
|
|
1225
|
+
"methods": [],
|
|
1226
|
+
})
|
|
1227
|
+
defined_names.add(name)
|
|
1228
|
+
|
|
1229
|
+
# Function / Method
|
|
1230
|
+
m_fn = fn_re.match(line)
|
|
1231
|
+
if m_fn:
|
|
1232
|
+
ret_type = m_fn.group(1)
|
|
1233
|
+
name = m_fn.group(2)
|
|
1234
|
+
args_raw = m_fn.group(3) or ""
|
|
1235
|
+
if name not in ("if", "for", "while", "switch", "catch", "return") and ret_type not in ("return", "else", "new"):
|
|
1236
|
+
sig = f"{ret_type} {name}({args_raw.strip()}):"
|
|
1237
|
+
if current_class:
|
|
1238
|
+
method_sym = {
|
|
1239
|
+
"name": name,
|
|
1240
|
+
"type": "method",
|
|
1241
|
+
"parent": current_class["name"],
|
|
1242
|
+
"class_name": current_class["name"],
|
|
1243
|
+
"lineno": i,
|
|
1244
|
+
"line_number": i,
|
|
1245
|
+
"end_lineno": i,
|
|
1246
|
+
"signature": sig,
|
|
1247
|
+
"docstring": None,
|
|
1248
|
+
"decorators": [],
|
|
1249
|
+
"is_async": False,
|
|
1250
|
+
"args": [a.split()[-1].lstrip("*&").strip() for a in args_raw.split(",") if a.strip()],
|
|
1251
|
+
"return_type": ret_type,
|
|
1252
|
+
}
|
|
1253
|
+
current_class_methods.append(method_sym)
|
|
1254
|
+
defined_names.add(f"{current_class['name']}.{name}")
|
|
1255
|
+
defined_names.add(name)
|
|
1256
|
+
else:
|
|
1257
|
+
symbols.append({
|
|
1258
|
+
"name": name,
|
|
1259
|
+
"type": "function",
|
|
1260
|
+
"parent": None,
|
|
1261
|
+
"class_name": None,
|
|
1262
|
+
"lineno": i,
|
|
1263
|
+
"line_number": i,
|
|
1264
|
+
"end_lineno": i,
|
|
1265
|
+
"signature": sig,
|
|
1266
|
+
"docstring": None,
|
|
1267
|
+
"decorators": [],
|
|
1268
|
+
"is_async": False,
|
|
1269
|
+
"args": [a.split()[-1].lstrip("*&").strip() for a in args_raw.split(",") if a.strip()],
|
|
1270
|
+
"return_type": ret_type,
|
|
1271
|
+
})
|
|
1272
|
+
defined_names.add(name)
|
|
1273
|
+
|
|
1274
|
+
brace_depth += line.count("{") - line.count("}")
|
|
1275
|
+
if current_class and brace_depth <= class_brace_start - 1:
|
|
1276
|
+
current_class["end_lineno"] = i
|
|
1277
|
+
current_class["methods"] = current_class_methods
|
|
1278
|
+
symbols.append(current_class)
|
|
1279
|
+
symbols.extend(current_class_methods)
|
|
1280
|
+
current_class = None
|
|
1281
|
+
current_class_methods = []
|
|
1282
|
+
|
|
1283
|
+
if current_class:
|
|
1284
|
+
current_class["end_lineno"] = len(lines)
|
|
1285
|
+
current_class["methods"] = current_class_methods
|
|
1286
|
+
symbols.append(current_class)
|
|
1287
|
+
symbols.extend(current_class_methods)
|
|
1288
|
+
|
|
1289
|
+
ident_re = re.compile(r"\b([A-Za-z_][A-Za-z0-9_]*)\b")
|
|
1290
|
+
for word in ident_re.findall(source):
|
|
1291
|
+
referenced_names.add(word)
|
|
1292
|
+
|
|
1293
|
+
file_info = {
|
|
1294
|
+
"defined_names": defined_names,
|
|
1295
|
+
"imported_names": imported_names,
|
|
1296
|
+
"referenced_names": referenced_names,
|
|
1297
|
+
"raw_imports": raw_imports,
|
|
1298
|
+
"caller_callee": {k: sorted(v) for k, v in caller_callees.items()},
|
|
1299
|
+
"line_count": len(lines),
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
return symbols, file_info
|
|
1303
|
+
|
|
1304
|
+
# ==========================================================================
|
|
1305
|
+
# Dependency Graph & Topological Analysis
|
|
1306
|
+
# ==========================================================================
|
|
1307
|
+
|
|
1308
|
+
def _resolve_relative_path(self, abs_path: str) -> str:
|
|
1309
|
+
"""Converts an absolute path to a normalized relative workspace path."""
|
|
1310
|
+
try:
|
|
1311
|
+
return os.path.relpath(abs_path, str(self.root_dir)).replace("\\", "/")
|
|
1312
|
+
except ValueError:
|
|
1313
|
+
return abs_path.replace("\\", "/")
|
|
1314
|
+
|
|
1315
|
+
def _build_workspace_metadata(self) -> List[Dict[str, Any]]:
|
|
1316
|
+
"""Scans workspace and gathers symbols and metadata for all supported files."""
|
|
1317
|
+
files = self.scan_workspace_files()
|
|
1318
|
+
file_data: List[Dict[str, Any]] = []
|
|
1319
|
+
|
|
1320
|
+
for fpath in files:
|
|
1321
|
+
rel_path = self._resolve_relative_path(fpath)
|
|
1322
|
+
symbols, info = self._extract_file_info(fpath)
|
|
1323
|
+
file_data.append({
|
|
1324
|
+
"full_path": fpath,
|
|
1325
|
+
"rel_path": rel_path,
|
|
1326
|
+
"symbols": symbols,
|
|
1327
|
+
"info": info,
|
|
1328
|
+
})
|
|
1329
|
+
|
|
1330
|
+
return file_data
|
|
1331
|
+
|
|
1332
|
+
def get_dependency_graph(self) -> Dict[str, List[str]]:
|
|
1333
|
+
"""
|
|
1334
|
+
Builds the workspace forward dependency graph (file -> list of files it imports).
|
|
1335
|
+
|
|
1336
|
+
Returns:
|
|
1337
|
+
Dictionary mapping relative file paths to lists of imported relative file paths.
|
|
1338
|
+
"""
|
|
1339
|
+
file_data = self._build_workspace_metadata()
|
|
1340
|
+
all_rel_paths = {item["rel_path"] for item in file_data}
|
|
1341
|
+
|
|
1342
|
+
# Build module resolution index
|
|
1343
|
+
module_index: Dict[str, str] = {}
|
|
1344
|
+
for rel in all_rel_paths:
|
|
1345
|
+
base, _ = os.path.splitext(rel)
|
|
1346
|
+
module_index[rel] = rel
|
|
1347
|
+
module_index[base] = rel
|
|
1348
|
+
module_index[base.replace("/", ".")] = rel
|
|
1349
|
+
module_index[os.path.basename(base)] = rel
|
|
1350
|
+
|
|
1351
|
+
dep_graph: Dict[str, Set[str]] = {item["rel_path"]: set() for item in file_data}
|
|
1352
|
+
|
|
1353
|
+
for item in file_data:
|
|
1354
|
+
src_rel = item["rel_path"]
|
|
1355
|
+
src_dir = os.path.dirname(src_rel)
|
|
1356
|
+
raw_imports = item["info"].get("raw_imports", [])
|
|
1357
|
+
imported_names = item["info"].get("imported_names", set())
|
|
1358
|
+
|
|
1359
|
+
# 1. Resolve raw import strings
|
|
1360
|
+
for raw in raw_imports:
|
|
1361
|
+
clean_raw = raw.strip().lstrip("./").replace("\\", "/")
|
|
1362
|
+
# Try relative to src_dir
|
|
1363
|
+
rel_target = os.path.normpath(os.path.join(src_dir, clean_raw)).replace("\\", "/")
|
|
1364
|
+
resolved = None
|
|
1365
|
+
if rel_target in module_index:
|
|
1366
|
+
resolved = module_index[rel_target]
|
|
1367
|
+
elif clean_raw in module_index:
|
|
1368
|
+
resolved = module_index[clean_raw]
|
|
1369
|
+
elif raw.replace(".", "/") in module_index:
|
|
1370
|
+
resolved = module_index[raw.replace(".", "/")]
|
|
1371
|
+
|
|
1372
|
+
if resolved and resolved != src_rel and resolved in all_rel_paths:
|
|
1373
|
+
dep_graph[src_rel].add(resolved)
|
|
1374
|
+
|
|
1375
|
+
# 2. Resolve cross-file referenced symbols if not already captured
|
|
1376
|
+
for other in file_data:
|
|
1377
|
+
other_rel = other["rel_path"]
|
|
1378
|
+
if other_rel == src_rel:
|
|
1379
|
+
continue
|
|
1380
|
+
other_defs = other["info"].get("defined_names", set())
|
|
1381
|
+
if other_defs & imported_names:
|
|
1382
|
+
dep_graph[src_rel].add(other_rel)
|
|
1383
|
+
|
|
1384
|
+
return {k: sorted(v) for k, v in dep_graph.items()}
|
|
1385
|
+
|
|
1386
|
+
def get_reverse_dependency_graph(self) -> Dict[str, List[str]]:
|
|
1387
|
+
"""
|
|
1388
|
+
Builds the workspace reverse dependency graph (file -> list of files that depend on it).
|
|
1389
|
+
|
|
1390
|
+
Returns:
|
|
1391
|
+
Dictionary mapping relative file paths to lists of dependent relative file paths.
|
|
1392
|
+
"""
|
|
1393
|
+
dep_graph = self.get_dependency_graph()
|
|
1394
|
+
rev_graph: Dict[str, Set[str]] = {k: set() for k in dep_graph}
|
|
1395
|
+
|
|
1396
|
+
for src, targets in dep_graph.items():
|
|
1397
|
+
for tgt in targets:
|
|
1398
|
+
if tgt in rev_graph:
|
|
1399
|
+
rev_graph[tgt].add(src)
|
|
1400
|
+
|
|
1401
|
+
return {k: sorted(v) for k, v in rev_graph.items()}
|
|
1402
|
+
|
|
1403
|
+
def get_import_tree(self, file_path: Optional[str] = None) -> Dict[str, Any]:
|
|
1404
|
+
"""
|
|
1405
|
+
Returns the import tree for a specific file or the whole workspace.
|
|
1406
|
+
"""
|
|
1407
|
+
dep_graph = self.get_dependency_graph()
|
|
1408
|
+
if file_path:
|
|
1409
|
+
norm = self._resolve_relative_path(file_path)
|
|
1410
|
+
return {norm: dep_graph.get(norm, [])}
|
|
1411
|
+
return dep_graph
|
|
1412
|
+
|
|
1413
|
+
def get_caller_callee_map(self) -> Dict[str, List[str]]:
|
|
1414
|
+
"""
|
|
1415
|
+
Builds a mapping of functions/methods to the list of symbols they call.
|
|
1416
|
+
|
|
1417
|
+
Returns:
|
|
1418
|
+
Dictionary mapping caller symbol name to list of callee symbol names.
|
|
1419
|
+
"""
|
|
1420
|
+
file_data = self._build_workspace_metadata()
|
|
1421
|
+
all_callers: Dict[str, Set[str]] = defaultdict(set)
|
|
1422
|
+
|
|
1423
|
+
for item in file_data:
|
|
1424
|
+
cc = item["info"].get("caller_callee", {})
|
|
1425
|
+
for caller, callees in cc.items():
|
|
1426
|
+
all_callers[caller].update(callees)
|
|
1427
|
+
|
|
1428
|
+
return {k: sorted(v) for k, v in all_callers.items()}
|
|
1429
|
+
|
|
1430
|
+
def detect_cyclic_imports(self) -> List[List[str]]:
|
|
1431
|
+
"""
|
|
1432
|
+
Detects circular/cyclic import dependencies across workspace files.
|
|
1433
|
+
|
|
1434
|
+
Returns:
|
|
1435
|
+
List of detected cycles represented as lists of relative file paths.
|
|
1436
|
+
"""
|
|
1437
|
+
dep_graph = self.get_dependency_graph()
|
|
1438
|
+
visited: Dict[str, int] = {} # 0: unvisited, 1: visiting, 2: visited
|
|
1439
|
+
cycles: List[List[str]] = []
|
|
1440
|
+
|
|
1441
|
+
def dfs(node: str, path: List[str]):
|
|
1442
|
+
visited[node] = 1
|
|
1443
|
+
path.append(node)
|
|
1444
|
+
|
|
1445
|
+
for neighbor in dep_graph.get(node, []):
|
|
1446
|
+
if visited.get(neighbor, 0) == 1:
|
|
1447
|
+
# Cycle found!
|
|
1448
|
+
try:
|
|
1449
|
+
idx = path.index(neighbor)
|
|
1450
|
+
cycle = path[idx:] + [neighbor]
|
|
1451
|
+
cycles.append(cycle)
|
|
1452
|
+
except ValueError:
|
|
1453
|
+
pass
|
|
1454
|
+
elif visited.get(neighbor, 0) == 0:
|
|
1455
|
+
dfs(neighbor, path)
|
|
1456
|
+
|
|
1457
|
+
path.pop()
|
|
1458
|
+
visited[node] = 2
|
|
1459
|
+
|
|
1460
|
+
for node in dep_graph:
|
|
1461
|
+
if visited.get(node, 0) == 0:
|
|
1462
|
+
dfs(node, [])
|
|
1463
|
+
|
|
1464
|
+
# Deduplicate and normalize cycles
|
|
1465
|
+
unique_cycles: List[List[str]] = []
|
|
1466
|
+
seen_cycle_keys: Set[str] = set()
|
|
1467
|
+
|
|
1468
|
+
for c in cycles:
|
|
1469
|
+
inner = c[:-1]
|
|
1470
|
+
if not inner:
|
|
1471
|
+
continue
|
|
1472
|
+
min_idx = inner.index(min(inner))
|
|
1473
|
+
canonical = inner[min_idx:] + inner[:min_idx]
|
|
1474
|
+
key = "->".join(canonical)
|
|
1475
|
+
if key not in seen_cycle_keys:
|
|
1476
|
+
seen_cycle_keys.add(key)
|
|
1477
|
+
unique_cycles.append(canonical + [canonical[0]])
|
|
1478
|
+
|
|
1479
|
+
return unique_cycles
|
|
1480
|
+
|
|
1481
|
+
def get_symbol_references(self, symbol_name: str) -> List[str]:
|
|
1482
|
+
"""
|
|
1483
|
+
Finds all workspace files referencing a given symbol.
|
|
1484
|
+
"""
|
|
1485
|
+
file_data = self._build_workspace_metadata()
|
|
1486
|
+
referencing_files: List[str] = []
|
|
1487
|
+
|
|
1488
|
+
for item in file_data:
|
|
1489
|
+
ref_names = item["info"].get("referenced_names", set())
|
|
1490
|
+
imp_names = item["info"].get("imported_names", set())
|
|
1491
|
+
if symbol_name in ref_names or symbol_name in imp_names:
|
|
1492
|
+
referencing_files.append(item["rel_path"])
|
|
1493
|
+
|
|
1494
|
+
referencing_files.sort()
|
|
1495
|
+
return referencing_files
|
|
1496
|
+
|
|
1497
|
+
# ==========================================================================
|
|
1498
|
+
# Map Generation & Token Budgeting
|
|
1499
|
+
# ==========================================================================
|
|
1500
|
+
|
|
1501
|
+
def _matches_focus(self, rel_path: str, full_path: str, focus_files: Optional[List[str]]) -> bool:
|
|
1502
|
+
"""Checks if a relative or absolute file path matches any focus file pattern."""
|
|
1503
|
+
if not focus_files:
|
|
1504
|
+
return False
|
|
1505
|
+
rel_norm = rel_path.replace("\\", "/").strip()
|
|
1506
|
+
full_norm = full_path.replace("\\", "/").strip()
|
|
1507
|
+
base_name = os.path.basename(rel_norm)
|
|
1508
|
+
|
|
1509
|
+
for f in focus_files:
|
|
1510
|
+
f_norm = f.replace("\\", "/").strip()
|
|
1511
|
+
if not f_norm:
|
|
1512
|
+
continue
|
|
1513
|
+
if (
|
|
1514
|
+
rel_norm == f_norm
|
|
1515
|
+
or rel_norm.endswith("/" + f_norm)
|
|
1516
|
+
or base_name == f_norm
|
|
1517
|
+
or full_norm == f_norm
|
|
1518
|
+
or full_norm.endswith("/" + f_norm)
|
|
1519
|
+
):
|
|
1520
|
+
return True
|
|
1521
|
+
return False
|
|
1522
|
+
|
|
1523
|
+
def get_repo_map(
|
|
1524
|
+
self,
|
|
1525
|
+
max_tokens: int = 400,
|
|
1526
|
+
focus_files: Optional[List[str]] = None,
|
|
1527
|
+
) -> str:
|
|
1528
|
+
"""
|
|
1529
|
+
Generates a compact hierarchical tree view of workspace symbols
|
|
1530
|
+
bounded strictly to < max_tokens words/tokens.
|
|
1531
|
+
|
|
1532
|
+
Prioritizes focus files and architecturally significant symbols.
|
|
1533
|
+
|
|
1534
|
+
Args:
|
|
1535
|
+
max_tokens: Maximum token/word budget for the returned map text.
|
|
1536
|
+
focus_files: Optional list of file paths to prioritize.
|
|
1537
|
+
|
|
1538
|
+
Returns:
|
|
1539
|
+
Compact string representation of the codebase map.
|
|
1540
|
+
"""
|
|
1541
|
+
if max_tokens <= 0:
|
|
1542
|
+
return ""
|
|
1543
|
+
|
|
1544
|
+
file_data = self._build_workspace_metadata()
|
|
1545
|
+
if not file_data:
|
|
1546
|
+
return ""
|
|
1547
|
+
|
|
1548
|
+
# Score and rank files
|
|
1549
|
+
rev_graph = self.get_reverse_dependency_graph()
|
|
1550
|
+
|
|
1551
|
+
for item in file_data:
|
|
1552
|
+
rel_path = item["rel_path"]
|
|
1553
|
+
full_path = item["full_path"]
|
|
1554
|
+
score = 0.0
|
|
1555
|
+
|
|
1556
|
+
# 1. Focus file priority boost (+10000)
|
|
1557
|
+
if self._matches_focus(rel_path, full_path, focus_files):
|
|
1558
|
+
score += 10000.0
|
|
1559
|
+
|
|
1560
|
+
# 2. In-degree / Cross-file references score
|
|
1561
|
+
dependents = rev_graph.get(rel_path, [])
|
|
1562
|
+
score += len(dependents) * 20.0
|
|
1563
|
+
|
|
1564
|
+
# 3. Architectural significance
|
|
1565
|
+
base_name = os.path.basename(rel_path)
|
|
1566
|
+
if base_name in {
|
|
1567
|
+
"main.py",
|
|
1568
|
+
"app.py",
|
|
1569
|
+
"cli.py",
|
|
1570
|
+
"orchestrator.py",
|
|
1571
|
+
"service.py",
|
|
1572
|
+
"models.py",
|
|
1573
|
+
"core.py",
|
|
1574
|
+
"verifier.py",
|
|
1575
|
+
"llm_driver.py",
|
|
1576
|
+
"index.ts",
|
|
1577
|
+
"main.rs",
|
|
1578
|
+
"main.go",
|
|
1579
|
+
"main.cpp",
|
|
1580
|
+
}:
|
|
1581
|
+
score += 10.0
|
|
1582
|
+
elif base_name in {"__init__.py", "mod.rs"}:
|
|
1583
|
+
score += 2.0
|
|
1584
|
+
|
|
1585
|
+
# 4. Symbol count contribution
|
|
1586
|
+
score += len(item["symbols"]) * 1.5
|
|
1587
|
+
|
|
1588
|
+
# 5. Path depth penalty (shallow files ranked slightly higher)
|
|
1589
|
+
depth = rel_path.count("/")
|
|
1590
|
+
score -= depth * 0.5
|
|
1591
|
+
|
|
1592
|
+
item["score"] = score
|
|
1593
|
+
|
|
1594
|
+
# Sort files by descending importance score
|
|
1595
|
+
file_data.sort(key=lambda x: (-x["score"], x["rel_path"]))
|
|
1596
|
+
|
|
1597
|
+
# Build hierarchical representation respecting token budget
|
|
1598
|
+
return self._render_tree(file_data, max_tokens)
|
|
1599
|
+
|
|
1600
|
+
def _render_tree(self, file_data: List[Dict[str, Any]], max_tokens: int) -> str:
|
|
1601
|
+
"""
|
|
1602
|
+
Renders a compact hierarchical tree string from sorted file metadata,
|
|
1603
|
+
strictly enforcing the token budget.
|
|
1604
|
+
"""
|
|
1605
|
+
output_lines: List[str] = []
|
|
1606
|
+
current_words = 0
|
|
1607
|
+
|
|
1608
|
+
for item in file_data:
|
|
1609
|
+
symbols = item["symbols"]
|
|
1610
|
+
if not symbols:
|
|
1611
|
+
continue
|
|
1612
|
+
|
|
1613
|
+
rel_path = item["rel_path"]
|
|
1614
|
+
file_header = f"{rel_path}:"
|
|
1615
|
+
header_words = len(file_header.split())
|
|
1616
|
+
|
|
1617
|
+
if current_words + header_words > max_tokens:
|
|
1618
|
+
break
|
|
1619
|
+
|
|
1620
|
+
file_lines: List[str] = [file_header]
|
|
1621
|
+
|
|
1622
|
+
classes: List[Dict[str, Any]] = [
|
|
1623
|
+
s for s in symbols if s.get("type") in ("class", "struct", "interface", "trait", "enum")
|
|
1624
|
+
]
|
|
1625
|
+
top_funcs: List[Dict[str, Any]] = [
|
|
1626
|
+
s for s in symbols if s.get("type") in ("function", "async_function")
|
|
1627
|
+
]
|
|
1628
|
+
|
|
1629
|
+
for cls in classes:
|
|
1630
|
+
cls_sig = cls.get("signature", f"class {cls['name']}:")
|
|
1631
|
+
file_lines.append(f" {cls_sig}")
|
|
1632
|
+
for m in cls.get("methods", []):
|
|
1633
|
+
m_sig = m.get("signature", f"def {m['name']}(...):")
|
|
1634
|
+
file_lines.append(f" {m_sig}")
|
|
1635
|
+
|
|
1636
|
+
for fn in top_funcs:
|
|
1637
|
+
fn_sig = fn.get("signature", f"def {fn['name']}(...):")
|
|
1638
|
+
file_lines.append(f" {fn_sig}")
|
|
1639
|
+
|
|
1640
|
+
# Append lines while within budget
|
|
1641
|
+
for line in file_lines:
|
|
1642
|
+
line_words = len(line.split())
|
|
1643
|
+
if current_words + line_words <= max_tokens:
|
|
1644
|
+
output_lines.append(line)
|
|
1645
|
+
current_words += line_words
|
|
1646
|
+
else:
|
|
1647
|
+
break
|
|
1648
|
+
|
|
1649
|
+
result = "\n".join(output_lines)
|
|
1650
|
+
return result
|
|
1651
|
+
|
|
1652
|
+
# ==========================================================================
|
|
1653
|
+
# Compact Topological Summary for LLM Context Injection
|
|
1654
|
+
# ==========================================================================
|
|
1655
|
+
|
|
1656
|
+
def get_topological_summary(
|
|
1657
|
+
self,
|
|
1658
|
+
max_tokens: int = 400,
|
|
1659
|
+
focus_files: Optional[List[str]] = None,
|
|
1660
|
+
) -> str:
|
|
1661
|
+
"""
|
|
1662
|
+
Generates a token-optimized topological summary of the codebase,
|
|
1663
|
+
ordering files from foundational dependencies up to high-level entrypoints.
|
|
1664
|
+
|
|
1665
|
+
Args:
|
|
1666
|
+
max_tokens: Maximum word/token budget for the summary.
|
|
1667
|
+
focus_files: Optional list of focus files to prioritize.
|
|
1668
|
+
|
|
1669
|
+
Returns:
|
|
1670
|
+
Compact topological summary string suitable for LLM system prompt injection.
|
|
1671
|
+
"""
|
|
1672
|
+
if max_tokens <= 0:
|
|
1673
|
+
return ""
|
|
1674
|
+
|
|
1675
|
+
file_data = self._build_workspace_metadata()
|
|
1676
|
+
if not file_data:
|
|
1677
|
+
return ""
|
|
1678
|
+
|
|
1679
|
+
dep_graph = self.get_dependency_graph()
|
|
1680
|
+
cycles = self.detect_cyclic_imports()
|
|
1681
|
+
|
|
1682
|
+
# Compute In-degrees (how many files depend on this file)
|
|
1683
|
+
in_degrees: Dict[str, int] = {item["rel_path"]: 0 for item in file_data}
|
|
1684
|
+
for src, tgts in dep_graph.items():
|
|
1685
|
+
for t in tgts:
|
|
1686
|
+
if t in in_degrees:
|
|
1687
|
+
in_degrees[t] += 1
|
|
1688
|
+
|
|
1689
|
+
# Layer computation: Leaves (0 outgoing deps in workspace) -> Layer 0
|
|
1690
|
+
layers: Dict[int, List[Dict[str, Any]]] = defaultdict(list)
|
|
1691
|
+
depth_map: Dict[str, int] = {}
|
|
1692
|
+
|
|
1693
|
+
def get_depth(node: str, visited: Set[str]) -> int:
|
|
1694
|
+
if node in depth_map:
|
|
1695
|
+
return depth_map[node]
|
|
1696
|
+
if node in visited:
|
|
1697
|
+
return 0
|
|
1698
|
+
visited.add(node)
|
|
1699
|
+
deps = [d for d in dep_graph.get(node, []) if d != node]
|
|
1700
|
+
if not deps:
|
|
1701
|
+
depth_map[node] = 0
|
|
1702
|
+
return 0
|
|
1703
|
+
max_d = 1 + max(get_depth(d, visited) for d in deps)
|
|
1704
|
+
depth_map[node] = max_d
|
|
1705
|
+
return max_d
|
|
1706
|
+
|
|
1707
|
+
for item in file_data:
|
|
1708
|
+
rel = item["rel_path"]
|
|
1709
|
+
d = get_depth(rel, set())
|
|
1710
|
+
layers[d].append(item)
|
|
1711
|
+
|
|
1712
|
+
output_lines: List[str] = ["[Topological Architecture Map]"]
|
|
1713
|
+
current_words = len(output_lines[0].split())
|
|
1714
|
+
|
|
1715
|
+
# Cycle notice if any
|
|
1716
|
+
if cycles:
|
|
1717
|
+
cycle_str = f"[!] Cyclic Imports Detected: {len(cycles)} cycle(s)"
|
|
1718
|
+
if current_words + len(cycle_str.split()) <= max_tokens:
|
|
1719
|
+
output_lines.append(cycle_str)
|
|
1720
|
+
current_words += len(cycle_str.split())
|
|
1721
|
+
|
|
1722
|
+
# Render layer by layer
|
|
1723
|
+
layer_names = {
|
|
1724
|
+
0: "Base / Foundation Modules",
|
|
1725
|
+
1: "Core Components & Utilities",
|
|
1726
|
+
2: "Services & Domain Logic",
|
|
1727
|
+
3: "Application Orchestrators & APIs",
|
|
1728
|
+
4: "Entrypoints & CLI",
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
for layer_idx in sorted(layers.keys()):
|
|
1732
|
+
layer_items = layers[layer_idx]
|
|
1733
|
+
# Prioritize focus files and high in-degree within layer
|
|
1734
|
+
layer_items.sort(
|
|
1735
|
+
key=lambda x: (
|
|
1736
|
+
-1000 if self._matches_focus(x["rel_path"], x["full_path"], focus_files) else 0,
|
|
1737
|
+
-in_degrees.get(x["rel_path"], 0),
|
|
1738
|
+
x["rel_path"],
|
|
1739
|
+
)
|
|
1740
|
+
)
|
|
1741
|
+
|
|
1742
|
+
layer_title = layer_names.get(layer_idx, f"Layer {layer_idx}")
|
|
1743
|
+
header = f"\n=== {layer_title} ==="
|
|
1744
|
+
header_words = len(header.split())
|
|
1745
|
+
if current_words + header_words > max_tokens:
|
|
1746
|
+
break
|
|
1747
|
+
output_lines.append(header)
|
|
1748
|
+
current_words += header_words
|
|
1749
|
+
|
|
1750
|
+
for item in layer_items:
|
|
1751
|
+
rel = item["rel_path"]
|
|
1752
|
+
deps = dep_graph.get(rel, [])
|
|
1753
|
+
deps_suffix = f" (deps: {', '.join(deps)})" if deps else ""
|
|
1754
|
+
file_line = f" • {rel}{deps_suffix}"
|
|
1755
|
+
file_words = len(file_line.split())
|
|
1756
|
+
if current_words + file_words > max_tokens:
|
|
1757
|
+
break
|
|
1758
|
+
output_lines.append(file_line)
|
|
1759
|
+
current_words += file_words
|
|
1760
|
+
|
|
1761
|
+
# Add compact symbol summary
|
|
1762
|
+
classes = [s for s in item["symbols"] if s.get("type") in ("class", "struct", "interface", "trait", "enum")]
|
|
1763
|
+
funcs = [s for s in item["symbols"] if s.get("type") in ("function", "async_function")]
|
|
1764
|
+
|
|
1765
|
+
for cls in classes:
|
|
1766
|
+
cls_line = f" - {cls.get('signature', cls['name'])}"
|
|
1767
|
+
cls_words = len(cls_line.split())
|
|
1768
|
+
if current_words + cls_words <= max_tokens:
|
|
1769
|
+
output_lines.append(cls_line)
|
|
1770
|
+
current_words += cls_words
|
|
1771
|
+
|
|
1772
|
+
for fn in funcs:
|
|
1773
|
+
fn_line = f" - {fn.get('signature', fn['name'])}"
|
|
1774
|
+
fn_words = len(fn_line.split())
|
|
1775
|
+
if current_words + fn_words <= max_tokens:
|
|
1776
|
+
output_lines.append(fn_line)
|
|
1777
|
+
current_words += fn_words
|
|
1778
|
+
|
|
1779
|
+
return "\n".join(output_lines).strip()
|
|
1780
|
+
|