cortexcode 0.1.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.
- cortexcode/__init__.py +3 -0
- cortexcode/analysis.py +331 -0
- cortexcode/cli.py +845 -0
- cortexcode/context.py +298 -0
- cortexcode/dashboard.py +152 -0
- cortexcode/docs.py +1266 -0
- cortexcode/git_diff.py +157 -0
- cortexcode/indexer.py +1860 -0
- cortexcode/lsp_server.py +315 -0
- cortexcode/mcp_server.py +455 -0
- cortexcode/plugins.py +188 -0
- cortexcode/semantic_search.py +237 -0
- cortexcode/vuln_scan.py +241 -0
- cortexcode/watcher.py +122 -0
- cortexcode/workspace.py +180 -0
- cortexcode-0.1.0.dist-info/METADATA +448 -0
- cortexcode-0.1.0.dist-info/RECORD +21 -0
- cortexcode-0.1.0.dist-info/WHEEL +5 -0
- cortexcode-0.1.0.dist-info/entry_points.txt +2 -0
- cortexcode-0.1.0.dist-info/licenses/LICENSE +21 -0
- cortexcode-0.1.0.dist-info/top_level.txt +1 -0
cortexcode/indexer.py
ADDED
|
@@ -0,0 +1,1860 @@
|
|
|
1
|
+
"""AST Indexer - Parse code files and extract symbols, calls, and relationships."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import hashlib
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from tree_sitter import Language, Parser
|
|
10
|
+
|
|
11
|
+
from cortexcode.plugins import plugin_registry
|
|
12
|
+
|
|
13
|
+
import tree_sitter_python
|
|
14
|
+
import tree_sitter_javascript
|
|
15
|
+
import tree_sitter_typescript
|
|
16
|
+
import tree_sitter_go
|
|
17
|
+
import tree_sitter_rust
|
|
18
|
+
import tree_sitter_java
|
|
19
|
+
import tree_sitter_c_sharp
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
import tree_sitter_kotlin
|
|
23
|
+
_HAS_KOTLIN = True
|
|
24
|
+
except ImportError:
|
|
25
|
+
_HAS_KOTLIN = False
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
import tree_sitter_swift
|
|
29
|
+
_HAS_SWIFT = True
|
|
30
|
+
except ImportError:
|
|
31
|
+
_HAS_SWIFT = False
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
LANGUAGE_MAP = {
|
|
35
|
+
".py": ("python", tree_sitter_python.language),
|
|
36
|
+
".js": ("javascript", tree_sitter_javascript.language),
|
|
37
|
+
".jsx": ("javascript", tree_sitter_javascript.language),
|
|
38
|
+
".ts": ("typescript", tree_sitter_typescript.language_tsx),
|
|
39
|
+
".tsx": ("typescript", tree_sitter_typescript.language_tsx),
|
|
40
|
+
".go": ("go", tree_sitter_go.language),
|
|
41
|
+
".rs": ("rust", tree_sitter_rust.language),
|
|
42
|
+
".java": ("java", tree_sitter_java.language),
|
|
43
|
+
".cs": ("csharp", tree_sitter_c_sharp.language),
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if _HAS_KOTLIN:
|
|
47
|
+
LANGUAGE_MAP[".kt"] = ("kotlin", tree_sitter_kotlin.language)
|
|
48
|
+
LANGUAGE_MAP[".kts"] = ("kotlin", tree_sitter_kotlin.language)
|
|
49
|
+
|
|
50
|
+
if _HAS_SWIFT:
|
|
51
|
+
LANGUAGE_MAP[".swift"] = ("swift", tree_sitter_swift.language)
|
|
52
|
+
|
|
53
|
+
# Dart uses regex-based extraction (no tree-sitter pip package)
|
|
54
|
+
REGEX_LANGUAGES = {
|
|
55
|
+
".dart": "dart",
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class CodeIndexer:
|
|
60
|
+
"""Parse source files and extract symbols, calls, and relationships."""
|
|
61
|
+
|
|
62
|
+
SUPPORTED_EXTENSIONS = set(LANGUAGE_MAP.keys()) | set(REGEX_LANGUAGES.keys())
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def get_all_extensions(cls) -> set[str]:
|
|
66
|
+
"""Get all supported extensions including plugin-registered ones."""
|
|
67
|
+
return cls.SUPPORTED_EXTENSIONS | plugin_registry.registered_extensions
|
|
68
|
+
|
|
69
|
+
def __init__(self):
|
|
70
|
+
self.parsers: dict[str, Parser] = {}
|
|
71
|
+
self.symbols: list[dict[str, Any]] = []
|
|
72
|
+
self.call_graph: dict[str, list[str]] = {}
|
|
73
|
+
self.file_symbols: dict[str, list[dict[str, Any]]] = {}
|
|
74
|
+
self.gitignore_patterns: list[tuple[str, bool]] = []
|
|
75
|
+
self.default_ignore_patterns = {
|
|
76
|
+
"__pycache__", ".git", ".venv", "venv", "node_modules",
|
|
77
|
+
".pytest_cache", ".mypy_cache", ".ruff_cache", ".cortexcode",
|
|
78
|
+
"dist", "build", "target", ".idea", ".vscode", ".next", ".nuxt",
|
|
79
|
+
".svelte-kit", "coverage", ".cache", "*.log", ".env.local"
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
def _get_parser(self, ext: str) -> Parser | None:
|
|
83
|
+
"""Get or create a parser for the given extension."""
|
|
84
|
+
if ext in self.parsers:
|
|
85
|
+
return self.parsers[ext]
|
|
86
|
+
|
|
87
|
+
if ext not in LANGUAGE_MAP:
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
lang_func = LANGUAGE_MAP[ext][1]
|
|
92
|
+
parser = Parser(Language(lang_func()))
|
|
93
|
+
self.parsers[ext] = parser
|
|
94
|
+
return parser
|
|
95
|
+
except Exception as e:
|
|
96
|
+
print(f"Failed to load parser for {ext}: {e}")
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
def index_directory(self, root_path: Path, incremental: bool = False) -> dict[str, Any]:
|
|
100
|
+
"""Index all supported files in a directory.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
root_path: Path to index
|
|
104
|
+
incremental: If True, only re-index changed files based on hash
|
|
105
|
+
"""
|
|
106
|
+
root_path = Path(root_path).resolve()
|
|
107
|
+
self.symbols = []
|
|
108
|
+
self.call_graph = {}
|
|
109
|
+
self.file_symbols = {}
|
|
110
|
+
self.parsers = {}
|
|
111
|
+
|
|
112
|
+
self._load_gitignore(root_path)
|
|
113
|
+
|
|
114
|
+
# Load plugins from config
|
|
115
|
+
plugin_config = root_path / ".cortexcode" / "plugins.json"
|
|
116
|
+
plugin_registry.load_from_config(plugin_config)
|
|
117
|
+
|
|
118
|
+
old_hashes = {}
|
|
119
|
+
if incremental:
|
|
120
|
+
index_path = root_path / ".cortexcode" / "index.json"
|
|
121
|
+
if index_path.exists():
|
|
122
|
+
try:
|
|
123
|
+
old_index = json.loads(index_path.read_text(encoding="utf-8"))
|
|
124
|
+
old_hashes = old_index.get("file_hashes", {})
|
|
125
|
+
except (json.JSONDecodeError, OSError):
|
|
126
|
+
pass
|
|
127
|
+
|
|
128
|
+
for ext in self.get_all_extensions():
|
|
129
|
+
for file_path in root_path.rglob(f"*{ext}"):
|
|
130
|
+
if self._should_ignore(file_path, root_path):
|
|
131
|
+
continue
|
|
132
|
+
|
|
133
|
+
if incremental and old_hashes:
|
|
134
|
+
try:
|
|
135
|
+
current_hash = hashlib.sha256(file_path.read_bytes()).hexdigest()
|
|
136
|
+
rel_path = str(file_path.relative_to(root_path))
|
|
137
|
+
if old_hashes.get(rel_path) == current_hash:
|
|
138
|
+
old_index_path = root_path / ".cortexcode" / "index.json"
|
|
139
|
+
if old_index_path.exists():
|
|
140
|
+
try:
|
|
141
|
+
old_data = json.loads(old_index_path.read_text(encoding="utf-8"))
|
|
142
|
+
if rel_path in old_data.get("files", {}):
|
|
143
|
+
self.file_symbols[rel_path] = old_data["files"][rel_path]
|
|
144
|
+
for sym in old_data["files"][rel_path].get("symbols", []):
|
|
145
|
+
self.symbols.append(sym)
|
|
146
|
+
name = sym.get("name")
|
|
147
|
+
if name:
|
|
148
|
+
if name not in self.call_graph:
|
|
149
|
+
self.call_graph[name] = []
|
|
150
|
+
self.call_graph[name].extend(sym.get("calls", []))
|
|
151
|
+
continue
|
|
152
|
+
except:
|
|
153
|
+
pass
|
|
154
|
+
except OSError:
|
|
155
|
+
pass
|
|
156
|
+
|
|
157
|
+
self._index_file(file_path, root_path)
|
|
158
|
+
|
|
159
|
+
return self._build_index(root_path)
|
|
160
|
+
|
|
161
|
+
def _load_gitignore(self, root: Path) -> None:
|
|
162
|
+
"""Load all .gitignore files from root and subdirectories."""
|
|
163
|
+
self.gitignore_patterns = []
|
|
164
|
+
|
|
165
|
+
for gitignore_path in root.rglob(".gitignore"):
|
|
166
|
+
try:
|
|
167
|
+
gitignore_dir = gitignore_path.parent
|
|
168
|
+
rel_dir = gitignore_dir.relative_to(root) if gitignore_dir != root else Path(".")
|
|
169
|
+
|
|
170
|
+
for line in gitignore_path.read_text(encoding="utf-8").splitlines():
|
|
171
|
+
line = line.strip()
|
|
172
|
+
if not line or line.startswith("#"):
|
|
173
|
+
continue
|
|
174
|
+
|
|
175
|
+
is_negation = line.startswith("!")
|
|
176
|
+
pattern = line[1:].strip() if is_negation else line
|
|
177
|
+
|
|
178
|
+
if pattern:
|
|
179
|
+
full_pattern = str(rel_dir / pattern) if rel_dir != Path(".") else pattern
|
|
180
|
+
self.gitignore_patterns.append((full_pattern, is_negation))
|
|
181
|
+
except (OSError, UnicodeDecodeError):
|
|
182
|
+
continue
|
|
183
|
+
|
|
184
|
+
def _matches_gitignore(self, file_path: Path, root: Path) -> bool:
|
|
185
|
+
"""Check if file matches gitignore patterns."""
|
|
186
|
+
try:
|
|
187
|
+
rel_path = file_path.relative_to(root)
|
|
188
|
+
rel_str = str(rel_path)
|
|
189
|
+
parts = rel_path.parts
|
|
190
|
+
|
|
191
|
+
for pattern, is_negation in self.gitignore_patterns:
|
|
192
|
+
if self._match_pattern(pattern, parts, rel_str):
|
|
193
|
+
return not is_negation
|
|
194
|
+
|
|
195
|
+
return False
|
|
196
|
+
except ValueError:
|
|
197
|
+
return True
|
|
198
|
+
|
|
199
|
+
def _match_pattern(self, pattern: str, parts: tuple, rel_str: str) -> bool:
|
|
200
|
+
"""Match a single gitignore pattern."""
|
|
201
|
+
pattern = pattern.rstrip("/")
|
|
202
|
+
|
|
203
|
+
if "/" in pattern:
|
|
204
|
+
pattern_parts = pattern.split("/")
|
|
205
|
+
if pattern.startswith("/"):
|
|
206
|
+
pattern_parts[0] = pattern_parts[0][1:]
|
|
207
|
+
if parts[:len(pattern_parts)] == tuple(pattern_parts):
|
|
208
|
+
return True
|
|
209
|
+
else:
|
|
210
|
+
for i in range(len(parts) - len(pattern_parts) + 1):
|
|
211
|
+
if parts[i:i+len(pattern_parts)] == tuple(pattern_parts):
|
|
212
|
+
return True
|
|
213
|
+
else:
|
|
214
|
+
for part in parts:
|
|
215
|
+
if part == pattern or (pattern.startswith("*") and part.endswith(pattern[1:])):
|
|
216
|
+
return True
|
|
217
|
+
|
|
218
|
+
if rel_str == pattern:
|
|
219
|
+
return True
|
|
220
|
+
|
|
221
|
+
return False
|
|
222
|
+
|
|
223
|
+
def _should_ignore(self, file_path: Path, root: Path) -> bool:
|
|
224
|
+
"""Check if file should be ignored based on gitignore or defaults."""
|
|
225
|
+
path_str = str(file_path)
|
|
226
|
+
|
|
227
|
+
for pattern in self.default_ignore_patterns:
|
|
228
|
+
if pattern in path_str:
|
|
229
|
+
return True
|
|
230
|
+
|
|
231
|
+
return self._matches_gitignore(file_path, root)
|
|
232
|
+
|
|
233
|
+
def _index_file(self, file_path: Path, root: Path) -> None:
|
|
234
|
+
"""Index a single file."""
|
|
235
|
+
ext = file_path.suffix.lower()
|
|
236
|
+
|
|
237
|
+
try:
|
|
238
|
+
content = file_path.read_text(encoding="utf-8")
|
|
239
|
+
except (UnicodeDecodeError, OSError):
|
|
240
|
+
return
|
|
241
|
+
|
|
242
|
+
rel_path = str(file_path.relative_to(root))
|
|
243
|
+
|
|
244
|
+
# Plugin-based extraction (custom framework plugins)
|
|
245
|
+
plugin_symbols = plugin_registry.extract_symbols(content, ext, rel_path)
|
|
246
|
+
if plugin_symbols is not None:
|
|
247
|
+
plugin_imports = plugin_registry.extract_imports(content, ext) or []
|
|
248
|
+
file_data = {
|
|
249
|
+
"symbols": plugin_symbols,
|
|
250
|
+
"imports": plugin_imports,
|
|
251
|
+
"exports": [],
|
|
252
|
+
"api_routes": [],
|
|
253
|
+
"entities": [],
|
|
254
|
+
}
|
|
255
|
+
self.file_symbols[rel_path] = file_data
|
|
256
|
+
self.symbols.extend(plugin_symbols)
|
|
257
|
+
for sym in plugin_symbols:
|
|
258
|
+
name = sym.get("name", "")
|
|
259
|
+
if name:
|
|
260
|
+
if name not in self.call_graph:
|
|
261
|
+
self.call_graph[name] = []
|
|
262
|
+
self.call_graph[name].extend(sym.get("calls", []))
|
|
263
|
+
return
|
|
264
|
+
|
|
265
|
+
# Regex-based languages (Dart)
|
|
266
|
+
if ext in REGEX_LANGUAGES:
|
|
267
|
+
symbols = self._extract_regex(content, ext, rel_path)
|
|
268
|
+
imports = self._extract_imports_regex(content, ext)
|
|
269
|
+
file_data = {
|
|
270
|
+
"symbols": symbols,
|
|
271
|
+
"imports": imports,
|
|
272
|
+
"exports": [],
|
|
273
|
+
"api_routes": [],
|
|
274
|
+
"entities": [],
|
|
275
|
+
}
|
|
276
|
+
self.file_symbols[rel_path] = file_data
|
|
277
|
+
self.symbols.extend(symbols)
|
|
278
|
+
for sym in symbols:
|
|
279
|
+
name = sym["name"]
|
|
280
|
+
if name not in self.call_graph:
|
|
281
|
+
self.call_graph[name] = []
|
|
282
|
+
self.call_graph[name].extend(sym.get("calls", []))
|
|
283
|
+
return
|
|
284
|
+
|
|
285
|
+
parser = self._get_parser(ext)
|
|
286
|
+
if not parser:
|
|
287
|
+
return
|
|
288
|
+
|
|
289
|
+
try:
|
|
290
|
+
tree = parser.parse(bytes(content, "utf8"))
|
|
291
|
+
except Exception:
|
|
292
|
+
return
|
|
293
|
+
|
|
294
|
+
symbols = self._extract_symbols(content, tree.root_node, ext)
|
|
295
|
+
|
|
296
|
+
imports = self._extract_imports(content, tree.root_node, ext)
|
|
297
|
+
exports = self._extract_exports(content, tree.root_node, ext)
|
|
298
|
+
api_routes = self._extract_api_routes(content, tree.root_node, ext)
|
|
299
|
+
entities = self._extract_entities(content, tree.root_node, ext)
|
|
300
|
+
|
|
301
|
+
file_data = {
|
|
302
|
+
"symbols": symbols,
|
|
303
|
+
"imports": imports,
|
|
304
|
+
"exports": exports,
|
|
305
|
+
"api_routes": api_routes,
|
|
306
|
+
"entities": entities,
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
self.file_symbols[rel_path] = file_data
|
|
310
|
+
self.symbols.extend(symbols)
|
|
311
|
+
|
|
312
|
+
for sym in symbols:
|
|
313
|
+
name = sym["name"]
|
|
314
|
+
if name not in self.call_graph:
|
|
315
|
+
self.call_graph[name] = []
|
|
316
|
+
self.call_graph[name].extend(sym.get("calls", []))
|
|
317
|
+
|
|
318
|
+
def _extract_symbols(self, source: str, node, ext: str) -> list[dict[str, Any]]:
|
|
319
|
+
"""Extract all symbols from AST based on language."""
|
|
320
|
+
symbols = []
|
|
321
|
+
|
|
322
|
+
if ext == ".py":
|
|
323
|
+
self._extract_python(source, node, symbols, None)
|
|
324
|
+
elif ext in (".js", ".jsx"):
|
|
325
|
+
self._extract_javascript(source, node, symbols, None)
|
|
326
|
+
elif ext in (".ts", ".tsx"):
|
|
327
|
+
self._extract_typescript(source, node, symbols, None)
|
|
328
|
+
elif ext == ".go":
|
|
329
|
+
self._extract_go(source, node, symbols, None)
|
|
330
|
+
elif ext == ".rs":
|
|
331
|
+
self._extract_rust(source, node, symbols, None)
|
|
332
|
+
elif ext == ".java":
|
|
333
|
+
self._extract_java(source, node, symbols, None)
|
|
334
|
+
elif ext == ".cs":
|
|
335
|
+
self._extract_csharp(source, node, symbols, None)
|
|
336
|
+
elif ext in (".kt", ".kts"):
|
|
337
|
+
self._extract_kotlin(source, node, symbols, None)
|
|
338
|
+
elif ext == ".swift":
|
|
339
|
+
self._extract_swift(source, node, symbols, None)
|
|
340
|
+
|
|
341
|
+
return symbols
|
|
342
|
+
|
|
343
|
+
def _extract_python(self, source: str, node, symbols: list, current_class: str | None) -> None:
|
|
344
|
+
"""Extract Python symbols."""
|
|
345
|
+
self._extract_generic(source, node, symbols, current_class, "function_definition", "class_definition")
|
|
346
|
+
|
|
347
|
+
def _extract_javascript(self, source: str, node, symbols: list, current_class: str | None) -> None:
|
|
348
|
+
"""Extract JavaScript/React symbols."""
|
|
349
|
+
self._extract_js_ts_generic(source, node, symbols, current_class, is_ts=False)
|
|
350
|
+
|
|
351
|
+
def _extract_typescript(self, source: str, node, symbols: list, current_class: str | None) -> None:
|
|
352
|
+
"""Extract TypeScript/Angular/Next.js symbols."""
|
|
353
|
+
self._extract_js_ts_generic(source, node, symbols, current_class, is_ts=True)
|
|
354
|
+
|
|
355
|
+
def _extract_js_ts_generic(self, source: str, node, symbols: list, current_class: str | None, is_ts: bool) -> None:
|
|
356
|
+
"""Extract JavaScript/TypeScript with framework support."""
|
|
357
|
+
node_type = node.type
|
|
358
|
+
|
|
359
|
+
if node_type == "function_declaration":
|
|
360
|
+
name = self._get_node_name(node, source)
|
|
361
|
+
if name:
|
|
362
|
+
params = self._extract_params(node, source, node_type)
|
|
363
|
+
calls = self._extract_calls(node, source)
|
|
364
|
+
doc = self._extract_jsdoc(node, source)
|
|
365
|
+
|
|
366
|
+
sym_type = "method" if current_class else "function"
|
|
367
|
+
framework = self._detect_framework(name, node, source)
|
|
368
|
+
|
|
369
|
+
sym = {
|
|
370
|
+
"name": name,
|
|
371
|
+
"type": sym_type,
|
|
372
|
+
"line": node.start_point.row + 1,
|
|
373
|
+
"params": params,
|
|
374
|
+
"calls": calls,
|
|
375
|
+
"class": current_class,
|
|
376
|
+
"framework": framework,
|
|
377
|
+
}
|
|
378
|
+
if doc:
|
|
379
|
+
sym["doc"] = doc
|
|
380
|
+
symbols.append(sym)
|
|
381
|
+
|
|
382
|
+
elif node_type in ("lexical_declaration", "variable_declaration"):
|
|
383
|
+
for child in node.children:
|
|
384
|
+
if child.type == "variable_declarator":
|
|
385
|
+
name_node = child.child_by_field_name("name")
|
|
386
|
+
value_node = child.child_by_field_name("value")
|
|
387
|
+
|
|
388
|
+
if name_node and value_node:
|
|
389
|
+
name = name_node.text.decode("utf-8")
|
|
390
|
+
|
|
391
|
+
if value_node.type in ("arrow_function", "function_expression", "function"):
|
|
392
|
+
params = self._extract_params(value_node, source, value_node.type)
|
|
393
|
+
calls = self._extract_calls(value_node, source)
|
|
394
|
+
return_type = self._extract_return_type(value_node, source) if is_ts else None
|
|
395
|
+
doc = self._extract_jsdoc(node, source)
|
|
396
|
+
|
|
397
|
+
sym_type = "method" if current_class else "function"
|
|
398
|
+
framework = self._detect_framework(name, value_node, source)
|
|
399
|
+
|
|
400
|
+
sym = {
|
|
401
|
+
"name": name,
|
|
402
|
+
"type": sym_type,
|
|
403
|
+
"line": node.start_point.row + 1,
|
|
404
|
+
"params": params,
|
|
405
|
+
"calls": calls,
|
|
406
|
+
"class": current_class,
|
|
407
|
+
"framework": framework,
|
|
408
|
+
}
|
|
409
|
+
if return_type:
|
|
410
|
+
sym["return_type"] = return_type
|
|
411
|
+
if doc:
|
|
412
|
+
sym["doc"] = doc
|
|
413
|
+
symbols.append(sym)
|
|
414
|
+
|
|
415
|
+
elif value_node.type == "call_expression":
|
|
416
|
+
# const router = express.Router() or similar
|
|
417
|
+
pass
|
|
418
|
+
|
|
419
|
+
elif node_type in ("export_statement", "export_default_declaration"):
|
|
420
|
+
for child in node.children:
|
|
421
|
+
self._extract_js_ts_generic(source, child, symbols, current_class, is_ts)
|
|
422
|
+
return
|
|
423
|
+
|
|
424
|
+
elif node_type in ("class_declaration", "class_expression"):
|
|
425
|
+
name = self._get_node_name(node, source)
|
|
426
|
+
if name:
|
|
427
|
+
methods = []
|
|
428
|
+
class_calls = []
|
|
429
|
+
|
|
430
|
+
for child in node.children:
|
|
431
|
+
if child.type == "class_body":
|
|
432
|
+
for member in child.children:
|
|
433
|
+
if member.type in ("method_definition", "public_field_definition", "field_definition"):
|
|
434
|
+
method_name = self._get_node_name(member, source)
|
|
435
|
+
if method_name:
|
|
436
|
+
params = self._extract_params(member, source, member.type)
|
|
437
|
+
method_calls = self._extract_calls(member, source)
|
|
438
|
+
methods.append({
|
|
439
|
+
"name": method_name,
|
|
440
|
+
"type": "method",
|
|
441
|
+
"line": member.start_point.row + 1,
|
|
442
|
+
"params": params,
|
|
443
|
+
"calls": method_calls,
|
|
444
|
+
})
|
|
445
|
+
class_calls.extend(method_calls)
|
|
446
|
+
|
|
447
|
+
framework = self._detect_class_framework(name, node, source)
|
|
448
|
+
|
|
449
|
+
symbols.append({
|
|
450
|
+
"name": name,
|
|
451
|
+
"type": "class",
|
|
452
|
+
"line": node.start_point.row + 1,
|
|
453
|
+
"methods": methods,
|
|
454
|
+
"calls": list(set(class_calls)),
|
|
455
|
+
"framework": framework,
|
|
456
|
+
})
|
|
457
|
+
|
|
458
|
+
current_class = name
|
|
459
|
+
|
|
460
|
+
elif is_ts and node_type == "interface_declaration":
|
|
461
|
+
name = self._get_node_name(node, source)
|
|
462
|
+
if name:
|
|
463
|
+
members = []
|
|
464
|
+
for child in node.children:
|
|
465
|
+
if child.type == "object_type" or child.type == "interface_body":
|
|
466
|
+
for member in child.children:
|
|
467
|
+
prop_name = self._get_node_name(member, source)
|
|
468
|
+
if prop_name:
|
|
469
|
+
members.append(prop_name)
|
|
470
|
+
symbols.append({
|
|
471
|
+
"name": name,
|
|
472
|
+
"type": "interface",
|
|
473
|
+
"line": node.start_point.row + 1,
|
|
474
|
+
"members": members if members else None,
|
|
475
|
+
"framework": "typescript",
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
elif is_ts and node_type == "type_alias_declaration":
|
|
479
|
+
name = self._get_node_name(node, source)
|
|
480
|
+
if name:
|
|
481
|
+
symbols.append({
|
|
482
|
+
"name": name,
|
|
483
|
+
"type": "type",
|
|
484
|
+
"line": node.start_point.row + 1,
|
|
485
|
+
"framework": "typescript",
|
|
486
|
+
})
|
|
487
|
+
|
|
488
|
+
elif is_ts and node_type == "enum_declaration":
|
|
489
|
+
name = self._get_node_name(node, source)
|
|
490
|
+
if name:
|
|
491
|
+
symbols.append({
|
|
492
|
+
"name": name,
|
|
493
|
+
"type": "enum",
|
|
494
|
+
"line": node.start_point.row + 1,
|
|
495
|
+
"framework": "typescript",
|
|
496
|
+
})
|
|
497
|
+
|
|
498
|
+
for child in node.children:
|
|
499
|
+
self._extract_js_ts_generic(source, child, symbols, current_class, is_ts)
|
|
500
|
+
|
|
501
|
+
def _extract_return_type(self, node, source: str) -> str | None:
|
|
502
|
+
"""Extract return type annotation from a function node."""
|
|
503
|
+
type_ann = node.child_by_field_name("return_type")
|
|
504
|
+
if type_ann:
|
|
505
|
+
return type_ann.text.decode("utf-8").lstrip(": ").strip()
|
|
506
|
+
return None
|
|
507
|
+
|
|
508
|
+
def _detect_framework(self, name: str, node, source: str) -> str | None:
|
|
509
|
+
"""Detect framework: React, React Native, Expo, Next.js, NestJS, Express, FastAPI, Django, Flask."""
|
|
510
|
+
source_bytes = node.text if hasattr(node, 'text') else b''
|
|
511
|
+
source_str = source_bytes.decode("utf-8", errors="ignore")
|
|
512
|
+
|
|
513
|
+
# React Native specific hooks/APIs
|
|
514
|
+
if any(rn in source_str for rn in ("useNavigation", "useRoute", "useAnimatedStyle", "useSharedValue")):
|
|
515
|
+
return "react-native-hook"
|
|
516
|
+
if any(rn in source_str for rn in ("StyleSheet.create", "Dimensions.get", "PixelRatio")):
|
|
517
|
+
return "react-native-util"
|
|
518
|
+
|
|
519
|
+
# React Native components (import check)
|
|
520
|
+
rn_components = ("View", "Text", "TouchableOpacity", "FlatList", "ScrollView", "SafeAreaView", "StatusBar", "Alert", "Modal")
|
|
521
|
+
if name and name[0].isupper() and any(f"<{c}" in source_str or f"{c}>" in source_str for c in rn_components):
|
|
522
|
+
return "react-native-component"
|
|
523
|
+
|
|
524
|
+
# Expo
|
|
525
|
+
if any(expo in source_str for expo in ("expo-", "usePermissions", "useCameraPermissions", "useAssets", "Notifications.schedule")):
|
|
526
|
+
return "expo"
|
|
527
|
+
if "expo-router" in source_str or "useLocalSearchParams" in source_str or "useGlobalSearchParams" in source_str:
|
|
528
|
+
return "expo-router"
|
|
529
|
+
|
|
530
|
+
# React hooks
|
|
531
|
+
if "useState" in source_str or "useEffect" in source_str or "useContext" in source_str or "useReducer" in source_str or "useMemo" in source_str:
|
|
532
|
+
if name and name[0].isupper():
|
|
533
|
+
return "react-component"
|
|
534
|
+
if name and name.startswith("use"):
|
|
535
|
+
return "react-hook"
|
|
536
|
+
return "react-hook"
|
|
537
|
+
|
|
538
|
+
# Next.js App Router
|
|
539
|
+
if name in ("generateMetadata", "generateStaticParams"):
|
|
540
|
+
return "nextjs-app-router"
|
|
541
|
+
if "'use server'" in source_str or '"use server"' in source_str:
|
|
542
|
+
return "nextjs-server-action"
|
|
543
|
+
if "'use client'" in source_str or '"use client"' in source_str:
|
|
544
|
+
return "nextjs-client"
|
|
545
|
+
|
|
546
|
+
# Next.js Pages Router
|
|
547
|
+
if "getServerSideProps" in name or "getStaticProps" in name or "getStaticPaths" in name:
|
|
548
|
+
return "nextjs-ssg"
|
|
549
|
+
if "getServerSideProps" in source_str or "getStaticProps" in source_str:
|
|
550
|
+
return "nextjs-page"
|
|
551
|
+
|
|
552
|
+
# NestJS
|
|
553
|
+
if "@Get(" in source_str or "@Post(" in source_str or "@Put(" in source_str or "@Delete(" in source_str or "@Patch(" in source_str:
|
|
554
|
+
return "nestjs-controller"
|
|
555
|
+
if "@Injectable" in source_str:
|
|
556
|
+
return "nestjs-service"
|
|
557
|
+
if "@Controller" in source_str and "nestjs" not in source_str.lower():
|
|
558
|
+
return "nestjs-controller"
|
|
559
|
+
if "@Guard" in source_str or "CanActivate" in source_str:
|
|
560
|
+
return "nestjs-guard"
|
|
561
|
+
if "@Pipe" in source_str or "PipeTransform" in source_str:
|
|
562
|
+
return "nestjs-pipe"
|
|
563
|
+
|
|
564
|
+
# Express
|
|
565
|
+
if "app.get(" in source_str or "app.post(" in source_str or "router.get(" in source_str or "router.post(" in source_str:
|
|
566
|
+
return "express-route"
|
|
567
|
+
if "app.use(" in source_str and "router" not in name:
|
|
568
|
+
return "express-middleware"
|
|
569
|
+
|
|
570
|
+
# FastAPI (Python)
|
|
571
|
+
if "@app.get(" in source_str or "@app.post(" in source_str or "@router.get(" in source_str or "@router.post(" in source_str:
|
|
572
|
+
return "fastapi-endpoint"
|
|
573
|
+
if "Depends(" in source_str and ("async def" in source_str or "def " in source_str):
|
|
574
|
+
return "fastapi-dependency"
|
|
575
|
+
|
|
576
|
+
# Django
|
|
577
|
+
if "request.method" in source_str or "HttpResponse" in source_str or "JsonResponse" in source_str:
|
|
578
|
+
return "django-view"
|
|
579
|
+
if "@api_view" in source_str or "APIView" in source_str:
|
|
580
|
+
return "django-rest"
|
|
581
|
+
|
|
582
|
+
# Flask
|
|
583
|
+
if "@app.route(" in source_str or "@blueprint.route(" in source_str:
|
|
584
|
+
return "flask-route"
|
|
585
|
+
|
|
586
|
+
# Remix
|
|
587
|
+
if name in ("loader", "action") and ("json(" in source_str or "redirect(" in source_str):
|
|
588
|
+
return "remix-loader"
|
|
589
|
+
|
|
590
|
+
# React component (PascalCase + return JSX) — must be last React check
|
|
591
|
+
if name and name[0].isupper() and ("return" in source_str or "=>" in source_str):
|
|
592
|
+
if "<" in source_str:
|
|
593
|
+
return "react-component"
|
|
594
|
+
|
|
595
|
+
return None
|
|
596
|
+
|
|
597
|
+
def _detect_class_framework(self, name: str, node, source: str) -> str | None:
|
|
598
|
+
"""Detect class-level framework: Angular, React Native, NestJS, etc."""
|
|
599
|
+
source_bytes = node.text if hasattr(node, 'text') else b''
|
|
600
|
+
source_str = source_bytes.decode("utf-8", errors="ignore")
|
|
601
|
+
|
|
602
|
+
# Angular
|
|
603
|
+
if "@Component" in source_str:
|
|
604
|
+
return "angular-component"
|
|
605
|
+
elif "@Injectable" in source_str:
|
|
606
|
+
return "angular-service"
|
|
607
|
+
elif "@NgModule" in source_str:
|
|
608
|
+
return "angular-module"
|
|
609
|
+
elif "@Directive" in source_str:
|
|
610
|
+
return "angular-directive"
|
|
611
|
+
elif "@Pipe" in source_str and "PipeTransform" in source_str:
|
|
612
|
+
return "angular-pipe"
|
|
613
|
+
|
|
614
|
+
# React Native class component
|
|
615
|
+
elif "extends Component" in source_str or "extends PureComponent" in source_str:
|
|
616
|
+
rn_indicators = ("View", "Text", "TouchableOpacity", "FlatList", "StyleSheet")
|
|
617
|
+
if any(ind in source_str for ind in rn_indicators):
|
|
618
|
+
return "react-native-component"
|
|
619
|
+
return "react-class-component"
|
|
620
|
+
|
|
621
|
+
# NestJS
|
|
622
|
+
elif "@Controller" in source_str:
|
|
623
|
+
return "nestjs-controller"
|
|
624
|
+
elif "@Injectable" in source_str and "nestjs" not in source_str.lower():
|
|
625
|
+
return "nestjs-service"
|
|
626
|
+
elif "@Module" in source_str and "imports:" in source_str:
|
|
627
|
+
return "nestjs-module"
|
|
628
|
+
|
|
629
|
+
# Spring Boot
|
|
630
|
+
elif "@Controller" in source_str or "@RestController" in source_str:
|
|
631
|
+
return "spring-boot"
|
|
632
|
+
|
|
633
|
+
return None
|
|
634
|
+
|
|
635
|
+
def _extract_go(self, source: str, node, symbols: list, current_class: str | None) -> None:
|
|
636
|
+
"""Extract Go symbols."""
|
|
637
|
+
self._extract_generic(source, node, symbols, current_class, "function_declaration", "method_declaration", "type_declaration")
|
|
638
|
+
|
|
639
|
+
def _extract_rust(self, source: str, node, symbols: list, current_class: str | None) -> None:
|
|
640
|
+
"""Extract Rust symbols."""
|
|
641
|
+
self._extract_generic(source, node, symbols, current_class, "function_item", "struct_item", "impl_item", "enum_item")
|
|
642
|
+
|
|
643
|
+
def _extract_java(self, source: str, node, symbols: list, current_class: str | None) -> None:
|
|
644
|
+
"""Extract Java symbols with Spring Boot detection."""
|
|
645
|
+
self._extract_java_with_framework(source, node, symbols, current_class)
|
|
646
|
+
|
|
647
|
+
def _extract_java_with_framework(self, source: str, node, symbols: list, current_class: str | None) -> None:
|
|
648
|
+
"""Extract Java with Spring Boot framework detection."""
|
|
649
|
+
node_type = node.type
|
|
650
|
+
|
|
651
|
+
if node_type == "method_declaration":
|
|
652
|
+
name = self._get_node_name(node, source)
|
|
653
|
+
if name:
|
|
654
|
+
params = self._extract_params(node, source, node_type)
|
|
655
|
+
calls = self._extract_calls(node, source)
|
|
656
|
+
|
|
657
|
+
symbols.append({
|
|
658
|
+
"name": name,
|
|
659
|
+
"type": "method",
|
|
660
|
+
"line": node.start_point.row + 1,
|
|
661
|
+
"params": params,
|
|
662
|
+
"calls": calls,
|
|
663
|
+
"class": current_class,
|
|
664
|
+
})
|
|
665
|
+
|
|
666
|
+
elif node_type == "class_declaration":
|
|
667
|
+
name = self._get_node_name(node, source)
|
|
668
|
+
if name:
|
|
669
|
+
methods = []
|
|
670
|
+
class_calls = []
|
|
671
|
+
|
|
672
|
+
for child in node.children:
|
|
673
|
+
if child.type == "method_declaration":
|
|
674
|
+
method_name = self._get_node_name(child, source)
|
|
675
|
+
if method_name:
|
|
676
|
+
params = self._extract_params(child, source, child.type)
|
|
677
|
+
method_calls = self._extract_calls(child, source)
|
|
678
|
+
methods.append({
|
|
679
|
+
"name": method_name,
|
|
680
|
+
"type": "method",
|
|
681
|
+
"line": child.start_point.row + 1,
|
|
682
|
+
"params": params,
|
|
683
|
+
"calls": method_calls,
|
|
684
|
+
})
|
|
685
|
+
class_calls.extend(method_calls)
|
|
686
|
+
|
|
687
|
+
framework = self._detect_java_framework(name, node, source)
|
|
688
|
+
|
|
689
|
+
symbols.append({
|
|
690
|
+
"name": name,
|
|
691
|
+
"type": "class",
|
|
692
|
+
"line": node.start_point.row + 1,
|
|
693
|
+
"methods": methods,
|
|
694
|
+
"calls": list(set(class_calls)),
|
|
695
|
+
"framework": framework,
|
|
696
|
+
})
|
|
697
|
+
|
|
698
|
+
current_class = name
|
|
699
|
+
|
|
700
|
+
elif node_type == "interface_declaration":
|
|
701
|
+
name = self._get_node_name(node, source)
|
|
702
|
+
if name:
|
|
703
|
+
symbols.append({
|
|
704
|
+
"name": name,
|
|
705
|
+
"type": "interface",
|
|
706
|
+
"line": node.start_point.row + 1,
|
|
707
|
+
"framework": self._detect_java_framework(name, node, source),
|
|
708
|
+
})
|
|
709
|
+
|
|
710
|
+
for child in node.children:
|
|
711
|
+
self._extract_java_with_framework(source, child, symbols, current_class)
|
|
712
|
+
|
|
713
|
+
def _detect_java_framework(self, name: str, node, source: str) -> str | None:
|
|
714
|
+
"""Detect Spring Boot and Android framework patterns."""
|
|
715
|
+
source_bytes = node.text if hasattr(node, 'text') else b''
|
|
716
|
+
source_str = source_bytes.decode("utf-8", errors="ignore")
|
|
717
|
+
|
|
718
|
+
# Android
|
|
719
|
+
if "extends AppCompatActivity" in source_str or "extends Activity" in source_str or "extends FragmentActivity" in source_str:
|
|
720
|
+
return "android-activity"
|
|
721
|
+
if "extends Fragment" in source_str or "extends DialogFragment" in source_str:
|
|
722
|
+
return "android-fragment"
|
|
723
|
+
if "extends ViewModel" in source_str or "extends AndroidViewModel" in source_str:
|
|
724
|
+
return "android-viewmodel"
|
|
725
|
+
if "extends Service" in source_str or "extends IntentService" in source_str:
|
|
726
|
+
return "android-service"
|
|
727
|
+
if "extends BroadcastReceiver" in source_str:
|
|
728
|
+
return "android-receiver"
|
|
729
|
+
if "extends ContentProvider" in source_str:
|
|
730
|
+
return "android-provider"
|
|
731
|
+
if "extends RecyclerView.Adapter" in source_str or "extends ArrayAdapter" in source_str:
|
|
732
|
+
return "android-adapter"
|
|
733
|
+
if "@Entity" in source_str and "@ColumnInfo" in source_str:
|
|
734
|
+
return "android-room"
|
|
735
|
+
if "@Dao" in source_str and ("@Query" in source_str or "@Insert" in source_str):
|
|
736
|
+
return "android-room"
|
|
737
|
+
if "@Database" in source_str and "RoomDatabase" in source_str:
|
|
738
|
+
return "android-room-db"
|
|
739
|
+
if "@HiltAndroidApp" in source_str or "@AndroidEntryPoint" in source_str:
|
|
740
|
+
return "android-hilt"
|
|
741
|
+
|
|
742
|
+
# Spring Boot
|
|
743
|
+
if "@Entity" in source_str or "@Table" in source_str:
|
|
744
|
+
return "spring-entity"
|
|
745
|
+
elif "@Repository" in source_str:
|
|
746
|
+
return "spring-repository"
|
|
747
|
+
elif "@Service" in source_str:
|
|
748
|
+
return "spring-service"
|
|
749
|
+
elif "@Controller" in source_str or "@RestController" in source_str:
|
|
750
|
+
return "spring-controller"
|
|
751
|
+
elif "@Component" in source_str:
|
|
752
|
+
return "spring-component"
|
|
753
|
+
elif "@Configuration" in source_str:
|
|
754
|
+
return "spring-config"
|
|
755
|
+
|
|
756
|
+
return None
|
|
757
|
+
|
|
758
|
+
def _extract_csharp(self, source: str, node, symbols: list, current_class: str | None) -> None:
|
|
759
|
+
"""Extract C# with .NET framework detection."""
|
|
760
|
+
self._extract_csharp_with_framework(source, node, symbols, current_class)
|
|
761
|
+
|
|
762
|
+
def _extract_csharp_with_framework(self, source: str, node, symbols: list, current_class: str | None) -> None:
|
|
763
|
+
"""Extract C# with .NET framework detection."""
|
|
764
|
+
node_type = node.type
|
|
765
|
+
|
|
766
|
+
if node_type in ("method_declaration", "local_function_statement"):
|
|
767
|
+
name = self._get_node_name(node, source)
|
|
768
|
+
if name:
|
|
769
|
+
params = self._extract_params(node, source, node_type)
|
|
770
|
+
calls = self._extract_calls(node, source)
|
|
771
|
+
|
|
772
|
+
symbols.append({
|
|
773
|
+
"name": name,
|
|
774
|
+
"type": "method",
|
|
775
|
+
"line": node.start_point.row + 1,
|
|
776
|
+
"params": params,
|
|
777
|
+
"calls": calls,
|
|
778
|
+
"class": current_class,
|
|
779
|
+
})
|
|
780
|
+
|
|
781
|
+
elif node_type == "class_declaration":
|
|
782
|
+
name = self._get_node_name(node, source)
|
|
783
|
+
if name:
|
|
784
|
+
methods = []
|
|
785
|
+
class_calls = []
|
|
786
|
+
|
|
787
|
+
for child in node.children:
|
|
788
|
+
if child.type == "method_declaration":
|
|
789
|
+
method_name = self._get_node_name(child, source)
|
|
790
|
+
if method_name:
|
|
791
|
+
params = self._extract_params(child, source, child.type)
|
|
792
|
+
method_calls = self._extract_calls(child, source)
|
|
793
|
+
methods.append({
|
|
794
|
+
"name": method_name,
|
|
795
|
+
"type": "method",
|
|
796
|
+
"line": child.start_point.row + 1,
|
|
797
|
+
"params": params,
|
|
798
|
+
"calls": method_calls,
|
|
799
|
+
})
|
|
800
|
+
class_calls.extend(method_calls)
|
|
801
|
+
|
|
802
|
+
framework = self._detect_csharp_framework(name, node, source)
|
|
803
|
+
|
|
804
|
+
symbols.append({
|
|
805
|
+
"name": name,
|
|
806
|
+
"type": "class",
|
|
807
|
+
"line": node.start_point.row + 1,
|
|
808
|
+
"methods": methods,
|
|
809
|
+
"calls": list(set(class_calls)),
|
|
810
|
+
"framework": framework,
|
|
811
|
+
})
|
|
812
|
+
|
|
813
|
+
current_class = name
|
|
814
|
+
|
|
815
|
+
elif node_type == "interface_declaration":
|
|
816
|
+
name = self._get_node_name(node, source)
|
|
817
|
+
if name:
|
|
818
|
+
symbols.append({
|
|
819
|
+
"name": name,
|
|
820
|
+
"type": "interface",
|
|
821
|
+
"line": node.start_point.row + 1,
|
|
822
|
+
})
|
|
823
|
+
|
|
824
|
+
for child in node.children:
|
|
825
|
+
self._extract_csharp_with_framework(source, child, symbols, current_class)
|
|
826
|
+
|
|
827
|
+
def _detect_csharp_framework(self, name: str, node, source: str) -> str | None:
|
|
828
|
+
"""Detect .NET framework patterns."""
|
|
829
|
+
source_bytes = node.text if hasattr(node, 'text') else b''
|
|
830
|
+
source_str = source_bytes.decode("utf-8", errors="ignore")
|
|
831
|
+
|
|
832
|
+
if "[ApiController]" in source_str or "ControllerBase" in source_str:
|
|
833
|
+
return "aspnet-controller"
|
|
834
|
+
elif "[Route(" in source_str or "[HttpGet]" in source_str or "[HttpPost]" in source_str:
|
|
835
|
+
return "aspnet-webapi"
|
|
836
|
+
elif "[DataContract]" in source_str or "[DataMember]" in source_str:
|
|
837
|
+
return "wcf-service"
|
|
838
|
+
elif "DbContext" in source_str or "DbSet<" in source_str:
|
|
839
|
+
return "ef-entity"
|
|
840
|
+
|
|
841
|
+
return None
|
|
842
|
+
|
|
843
|
+
def _extract_kotlin(self, source: str, node, symbols: list, current_class: str | None) -> None:
|
|
844
|
+
"""Extract Kotlin symbols with Android framework detection."""
|
|
845
|
+
self._extract_kotlin_recursive(source, node, symbols, current_class)
|
|
846
|
+
|
|
847
|
+
def _extract_kotlin_recursive(self, source: str, node, symbols: list, current_class: str | None) -> None:
|
|
848
|
+
"""Recursively extract Kotlin symbols."""
|
|
849
|
+
node_type = node.type
|
|
850
|
+
|
|
851
|
+
if node_type == "function_declaration":
|
|
852
|
+
name = self._get_node_name(node, source)
|
|
853
|
+
if name:
|
|
854
|
+
params = self._extract_params(node, source, node_type)
|
|
855
|
+
calls = self._extract_calls(node, source)
|
|
856
|
+
framework = self._detect_kotlin_framework(name, node, source)
|
|
857
|
+
sym_type = "method" if current_class else "function"
|
|
858
|
+
symbols.append({
|
|
859
|
+
"name": name, "type": sym_type,
|
|
860
|
+
"line": node.start_point.row + 1,
|
|
861
|
+
"params": params, "calls": calls,
|
|
862
|
+
"class": current_class, "framework": framework,
|
|
863
|
+
})
|
|
864
|
+
|
|
865
|
+
elif node_type == "class_declaration":
|
|
866
|
+
name = self._get_node_name(node, source)
|
|
867
|
+
if name:
|
|
868
|
+
methods = []
|
|
869
|
+
class_calls = []
|
|
870
|
+
for child in node.children:
|
|
871
|
+
if child.type == "class_body":
|
|
872
|
+
for member in child.children:
|
|
873
|
+
if member.type == "function_declaration":
|
|
874
|
+
m_name = self._get_node_name(member, source)
|
|
875
|
+
if m_name:
|
|
876
|
+
m_params = self._extract_params(member, source, member.type)
|
|
877
|
+
m_calls = self._extract_calls(member, source)
|
|
878
|
+
methods.append({"name": m_name, "type": "method", "line": member.start_point.row + 1, "params": m_params, "calls": m_calls})
|
|
879
|
+
class_calls.extend(m_calls)
|
|
880
|
+
|
|
881
|
+
framework = self._detect_kotlin_framework(name, node, source)
|
|
882
|
+
symbols.append({
|
|
883
|
+
"name": name, "type": "class",
|
|
884
|
+
"line": node.start_point.row + 1,
|
|
885
|
+
"methods": methods, "calls": list(set(class_calls)),
|
|
886
|
+
"framework": framework,
|
|
887
|
+
})
|
|
888
|
+
current_class = name
|
|
889
|
+
|
|
890
|
+
elif node_type == "object_declaration":
|
|
891
|
+
name = self._get_node_name(node, source)
|
|
892
|
+
if name:
|
|
893
|
+
symbols.append({
|
|
894
|
+
"name": name, "type": "class",
|
|
895
|
+
"line": node.start_point.row + 1,
|
|
896
|
+
"framework": self._detect_kotlin_framework(name, node, source),
|
|
897
|
+
})
|
|
898
|
+
|
|
899
|
+
for child in node.children:
|
|
900
|
+
self._extract_kotlin_recursive(source, child, symbols, current_class)
|
|
901
|
+
|
|
902
|
+
def _detect_kotlin_framework(self, name: str, node, source: str) -> str | None:
|
|
903
|
+
"""Detect Android/Compose/Ktor framework patterns in Kotlin."""
|
|
904
|
+
src = node.text.decode("utf-8", errors="ignore") if hasattr(node, 'text') else ""
|
|
905
|
+
|
|
906
|
+
# Jetpack Compose
|
|
907
|
+
if "@Composable" in src:
|
|
908
|
+
return "compose-ui"
|
|
909
|
+
if "@Preview" in src:
|
|
910
|
+
return "compose-preview"
|
|
911
|
+
# Android Activity/Fragment/ViewModel
|
|
912
|
+
if ": AppCompatActivity()" in src or ": Activity()" in src or ": ComponentActivity()" in src:
|
|
913
|
+
return "android-activity"
|
|
914
|
+
if ": Fragment()" in src or ": DialogFragment()" in src:
|
|
915
|
+
return "android-fragment"
|
|
916
|
+
if ": ViewModel()" in src or ": AndroidViewModel(" in src:
|
|
917
|
+
return "android-viewmodel"
|
|
918
|
+
if ": Service()" in src or ": IntentService(" in src:
|
|
919
|
+
return "android-service"
|
|
920
|
+
if ": BroadcastReceiver()" in src:
|
|
921
|
+
return "android-receiver"
|
|
922
|
+
if ": ContentProvider()" in src:
|
|
923
|
+
return "android-provider"
|
|
924
|
+
# Ktor
|
|
925
|
+
if "routing {" in src or "get(" in src and "call.respond" in src:
|
|
926
|
+
return "ktor-route"
|
|
927
|
+
# Room database
|
|
928
|
+
if "@Entity" in src or "@Dao" in src:
|
|
929
|
+
return "android-room"
|
|
930
|
+
if "@Database" in src:
|
|
931
|
+
return "android-room-db"
|
|
932
|
+
# Hilt/Dagger
|
|
933
|
+
if "@HiltViewModel" in src or "@HiltAndroidApp" in src:
|
|
934
|
+
return "android-hilt"
|
|
935
|
+
if "@Inject" in src or "@Module" in src:
|
|
936
|
+
return "android-di"
|
|
937
|
+
|
|
938
|
+
return None
|
|
939
|
+
|
|
940
|
+
def _extract_swift(self, source: str, node, symbols: list, current_class: str | None) -> None:
|
|
941
|
+
"""Extract Swift symbols with iOS framework detection."""
|
|
942
|
+
self._extract_swift_recursive(source, node, symbols, current_class)
|
|
943
|
+
|
|
944
|
+
def _extract_swift_recursive(self, source: str, node, symbols: list, current_class: str | None) -> None:
|
|
945
|
+
"""Recursively extract Swift symbols."""
|
|
946
|
+
node_type = node.type
|
|
947
|
+
|
|
948
|
+
if node_type == "function_declaration":
|
|
949
|
+
name = self._get_node_name(node, source)
|
|
950
|
+
if name:
|
|
951
|
+
params = self._extract_params(node, source, node_type)
|
|
952
|
+
calls = self._extract_calls(node, source)
|
|
953
|
+
framework = self._detect_swift_framework(name, node, source)
|
|
954
|
+
sym_type = "method" if current_class else "function"
|
|
955
|
+
symbols.append({
|
|
956
|
+
"name": name, "type": sym_type,
|
|
957
|
+
"line": node.start_point.row + 1,
|
|
958
|
+
"params": params, "calls": calls,
|
|
959
|
+
"class": current_class, "framework": framework,
|
|
960
|
+
})
|
|
961
|
+
|
|
962
|
+
elif node_type == "class_declaration":
|
|
963
|
+
name = self._get_node_name(node, source)
|
|
964
|
+
if name:
|
|
965
|
+
methods = []
|
|
966
|
+
class_calls = []
|
|
967
|
+
for child in node.children:
|
|
968
|
+
if child.type == "class_body":
|
|
969
|
+
for member in child.children:
|
|
970
|
+
if member.type == "function_declaration":
|
|
971
|
+
m_name = self._get_node_name(member, source)
|
|
972
|
+
if m_name:
|
|
973
|
+
m_params = self._extract_params(member, source, member.type)
|
|
974
|
+
m_calls = self._extract_calls(member, source)
|
|
975
|
+
methods.append({"name": m_name, "type": "method", "line": member.start_point.row + 1, "params": m_params, "calls": m_calls})
|
|
976
|
+
class_calls.extend(m_calls)
|
|
977
|
+
|
|
978
|
+
framework = self._detect_swift_framework(name, node, source)
|
|
979
|
+
symbols.append({
|
|
980
|
+
"name": name, "type": "class",
|
|
981
|
+
"line": node.start_point.row + 1,
|
|
982
|
+
"methods": methods, "calls": list(set(class_calls)),
|
|
983
|
+
"framework": framework,
|
|
984
|
+
})
|
|
985
|
+
current_class = name
|
|
986
|
+
|
|
987
|
+
elif node_type == "protocol_declaration":
|
|
988
|
+
name = self._get_node_name(node, source)
|
|
989
|
+
if name:
|
|
990
|
+
symbols.append({
|
|
991
|
+
"name": name, "type": "interface",
|
|
992
|
+
"line": node.start_point.row + 1,
|
|
993
|
+
"framework": "swift",
|
|
994
|
+
})
|
|
995
|
+
|
|
996
|
+
elif node_type in ("struct_declaration",):
|
|
997
|
+
name = self._get_node_name(node, source)
|
|
998
|
+
if name:
|
|
999
|
+
framework = self._detect_swift_framework(name, node, source)
|
|
1000
|
+
symbols.append({
|
|
1001
|
+
"name": name, "type": "class",
|
|
1002
|
+
"line": node.start_point.row + 1,
|
|
1003
|
+
"framework": framework,
|
|
1004
|
+
})
|
|
1005
|
+
|
|
1006
|
+
elif node_type == "enum_declaration":
|
|
1007
|
+
name = self._get_node_name(node, source)
|
|
1008
|
+
if name:
|
|
1009
|
+
symbols.append({
|
|
1010
|
+
"name": name, "type": "enum",
|
|
1011
|
+
"line": node.start_point.row + 1,
|
|
1012
|
+
})
|
|
1013
|
+
|
|
1014
|
+
for child in node.children:
|
|
1015
|
+
self._extract_swift_recursive(source, child, symbols, current_class)
|
|
1016
|
+
|
|
1017
|
+
def _detect_swift_framework(self, name: str, node, source: str) -> str | None:
|
|
1018
|
+
"""Detect iOS/SwiftUI/UIKit framework patterns."""
|
|
1019
|
+
src = node.text.decode("utf-8", errors="ignore") if hasattr(node, 'text') else ""
|
|
1020
|
+
|
|
1021
|
+
# SwiftUI
|
|
1022
|
+
if ": View" in src and "var body:" in src:
|
|
1023
|
+
return "swiftui-view"
|
|
1024
|
+
if "@ObservedObject" in src or "@StateObject" in src or "@EnvironmentObject" in src:
|
|
1025
|
+
return "swiftui-view"
|
|
1026
|
+
if "ObservableObject" in src:
|
|
1027
|
+
return "swiftui-observable"
|
|
1028
|
+
if "@State " in src or "@Binding " in src:
|
|
1029
|
+
return "swiftui-state"
|
|
1030
|
+
if "@main" in src and "App" in name:
|
|
1031
|
+
return "swiftui-app"
|
|
1032
|
+
# UIKit
|
|
1033
|
+
if ": UIViewController" in src:
|
|
1034
|
+
return "uikit-viewcontroller"
|
|
1035
|
+
if ": UITableViewDelegate" in src or ": UITableViewDataSource" in src:
|
|
1036
|
+
return "uikit-tableview"
|
|
1037
|
+
if ": UICollectionViewDelegate" in src:
|
|
1038
|
+
return "uikit-collectionview"
|
|
1039
|
+
if ": UIView" in src and ": UIViewController" not in src:
|
|
1040
|
+
return "uikit-view"
|
|
1041
|
+
# Combine
|
|
1042
|
+
if "AnyPublisher" in src or "@Published" in src or "sink(" in src:
|
|
1043
|
+
return "combine"
|
|
1044
|
+
# Core Data
|
|
1045
|
+
if ": NSManagedObject" in src or "@NSManaged" in src:
|
|
1046
|
+
return "coredata-entity"
|
|
1047
|
+
if "NSPersistentContainer" in src:
|
|
1048
|
+
return "coredata"
|
|
1049
|
+
# Vapor (server-side Swift)
|
|
1050
|
+
if "req.content" in src or "app.get(" in src or "app.post(" in src:
|
|
1051
|
+
return "vapor-route"
|
|
1052
|
+
|
|
1053
|
+
return None
|
|
1054
|
+
|
|
1055
|
+
def _extract_regex(self, source: str, ext: str, rel_path: str) -> list[dict[str, Any]]:
|
|
1056
|
+
"""Regex-based symbol extraction for languages without tree-sitter (Dart)."""
|
|
1057
|
+
if ext == ".dart":
|
|
1058
|
+
return self._extract_dart_regex(source, rel_path)
|
|
1059
|
+
return []
|
|
1060
|
+
|
|
1061
|
+
def _extract_dart_regex(self, source: str, rel_path: str) -> list[dict[str, Any]]:
|
|
1062
|
+
"""Extract Dart/Flutter symbols using regex."""
|
|
1063
|
+
symbols = []
|
|
1064
|
+
lines = source.split("\n")
|
|
1065
|
+
|
|
1066
|
+
# Class pattern: class Name extends/implements/with ... {
|
|
1067
|
+
class_re = re.compile(r'^\s*(?:abstract\s+)?class\s+(\w+)')
|
|
1068
|
+
# Function pattern: ReturnType name(params) { or =>
|
|
1069
|
+
func_re = re.compile(r'^\s*(?:static\s+)?(?:Future<[^>]*>|void|int|double|String|bool|dynamic|List<[^>]*>|Map<[^>]*>|Widget|State<[^>]*>|\w+)\s+(\w+)\s*\(')
|
|
1070
|
+
# Top-level function: type name(
|
|
1071
|
+
top_func_re = re.compile(r'^(?:Future<[^>]*>|void|int|double|String|bool|dynamic|Widget|State<[^>]*>|\w+)\s+(\w+)\s*\(')
|
|
1072
|
+
# Enum
|
|
1073
|
+
enum_re = re.compile(r'^\s*enum\s+(\w+)')
|
|
1074
|
+
# Mixin
|
|
1075
|
+
mixin_re = re.compile(r'^\s*mixin\s+(\w+)')
|
|
1076
|
+
# Extension
|
|
1077
|
+
ext_re = re.compile(r'^\s*extension\s+(\w+)')
|
|
1078
|
+
|
|
1079
|
+
current_class = None
|
|
1080
|
+
|
|
1081
|
+
for i, line in enumerate(lines):
|
|
1082
|
+
stripped = line.strip()
|
|
1083
|
+
|
|
1084
|
+
# Class
|
|
1085
|
+
m = class_re.match(stripped)
|
|
1086
|
+
if m:
|
|
1087
|
+
name = m.group(1)
|
|
1088
|
+
framework = self._detect_dart_framework(name, stripped, source)
|
|
1089
|
+
symbols.append({
|
|
1090
|
+
"name": name, "type": "class",
|
|
1091
|
+
"line": i + 1,
|
|
1092
|
+
"framework": framework,
|
|
1093
|
+
"calls": self._extract_dart_calls(source, i),
|
|
1094
|
+
})
|
|
1095
|
+
current_class = name
|
|
1096
|
+
continue
|
|
1097
|
+
|
|
1098
|
+
# Enum
|
|
1099
|
+
m = enum_re.match(stripped)
|
|
1100
|
+
if m:
|
|
1101
|
+
symbols.append({"name": m.group(1), "type": "enum", "line": i + 1})
|
|
1102
|
+
continue
|
|
1103
|
+
|
|
1104
|
+
# Mixin
|
|
1105
|
+
m = mixin_re.match(stripped)
|
|
1106
|
+
if m:
|
|
1107
|
+
symbols.append({"name": m.group(1), "type": "class", "line": i + 1, "framework": "dart-mixin"})
|
|
1108
|
+
continue
|
|
1109
|
+
|
|
1110
|
+
# Extension
|
|
1111
|
+
m = ext_re.match(stripped)
|
|
1112
|
+
if m:
|
|
1113
|
+
symbols.append({"name": m.group(1), "type": "class", "line": i + 1, "framework": "dart-extension"})
|
|
1114
|
+
continue
|
|
1115
|
+
|
|
1116
|
+
# Function/method
|
|
1117
|
+
m = func_re.match(stripped)
|
|
1118
|
+
if m:
|
|
1119
|
+
name = m.group(1)
|
|
1120
|
+
if name not in ("if", "while", "for", "switch", "catch", "class", "return"):
|
|
1121
|
+
params = self._extract_dart_params(stripped)
|
|
1122
|
+
sym_type = "method" if current_class and line.startswith(" ") else "function"
|
|
1123
|
+
framework = self._detect_dart_framework(name, stripped, source)
|
|
1124
|
+
symbols.append({
|
|
1125
|
+
"name": name, "type": sym_type,
|
|
1126
|
+
"line": i + 1, "params": params,
|
|
1127
|
+
"class": current_class if sym_type == "method" else None,
|
|
1128
|
+
"framework": framework,
|
|
1129
|
+
"calls": self._extract_dart_calls(source, i),
|
|
1130
|
+
})
|
|
1131
|
+
elif not line.startswith(" "):
|
|
1132
|
+
# Top-level function
|
|
1133
|
+
m = top_func_re.match(stripped)
|
|
1134
|
+
if m:
|
|
1135
|
+
name = m.group(1)
|
|
1136
|
+
if name not in ("if", "while", "for", "switch", "catch", "class", "return", "import"):
|
|
1137
|
+
symbols.append({
|
|
1138
|
+
"name": name, "type": "function",
|
|
1139
|
+
"line": i + 1,
|
|
1140
|
+
"params": self._extract_dart_params(stripped),
|
|
1141
|
+
"calls": self._extract_dart_calls(source, i),
|
|
1142
|
+
"framework": self._detect_dart_framework(name, stripped, source),
|
|
1143
|
+
})
|
|
1144
|
+
current_class = None
|
|
1145
|
+
|
|
1146
|
+
return symbols
|
|
1147
|
+
|
|
1148
|
+
def _extract_dart_params(self, line: str) -> list[str]:
|
|
1149
|
+
"""Extract parameters from a Dart function line."""
|
|
1150
|
+
m = re.search(r'\(([^)]*)\)', line)
|
|
1151
|
+
if not m:
|
|
1152
|
+
return []
|
|
1153
|
+
params_str = m.group(1).strip()
|
|
1154
|
+
if not params_str:
|
|
1155
|
+
return []
|
|
1156
|
+
params = []
|
|
1157
|
+
for p in params_str.split(","):
|
|
1158
|
+
p = p.strip().rstrip("?")
|
|
1159
|
+
parts = p.split()
|
|
1160
|
+
if len(parts) >= 2:
|
|
1161
|
+
params.append(parts[-1])
|
|
1162
|
+
elif parts:
|
|
1163
|
+
params.append(parts[0])
|
|
1164
|
+
return params[:8]
|
|
1165
|
+
|
|
1166
|
+
def _extract_dart_calls(self, source: str, line_idx: int) -> list[str]:
|
|
1167
|
+
"""Extract function calls near a Dart function definition."""
|
|
1168
|
+
calls = set()
|
|
1169
|
+
lines = source.split("\n")
|
|
1170
|
+
# Scan next 30 lines for calls
|
|
1171
|
+
for i in range(line_idx + 1, min(line_idx + 30, len(lines))):
|
|
1172
|
+
line = lines[i].strip()
|
|
1173
|
+
if line.startswith("class ") or line.startswith("enum "):
|
|
1174
|
+
break
|
|
1175
|
+
for m in re.finditer(r'(\w+)\s*\(', line):
|
|
1176
|
+
name = m.group(1)
|
|
1177
|
+
if name not in ("if", "while", "for", "switch", "catch", "return", "print"):
|
|
1178
|
+
calls.add(name)
|
|
1179
|
+
return list(calls)[:10]
|
|
1180
|
+
|
|
1181
|
+
def _detect_dart_framework(self, name: str, line: str, source: str) -> str | None:
|
|
1182
|
+
"""Detect Flutter/Dart framework patterns."""
|
|
1183
|
+
# Flutter widgets
|
|
1184
|
+
if "extends StatelessWidget" in line or "extends StatelessWidget" in source[max(0,source.find(name)-10):source.find(name)+200]:
|
|
1185
|
+
return "flutter-widget"
|
|
1186
|
+
if "extends StatefulWidget" in line:
|
|
1187
|
+
return "flutter-stateful"
|
|
1188
|
+
if "extends State<" in line:
|
|
1189
|
+
return "flutter-state"
|
|
1190
|
+
# Flutter specific
|
|
1191
|
+
if "Widget build(" in line:
|
|
1192
|
+
return "flutter-build"
|
|
1193
|
+
if "@override" in source[max(0,source.find(name)-30):source.find(name)+5]:
|
|
1194
|
+
pass # Could be any override
|
|
1195
|
+
# Check class body for Flutter patterns
|
|
1196
|
+
ctx = source[max(0,source.find(f"class {name}")):source.find(f"class {name}")+500] if f"class {name}" in source else ""
|
|
1197
|
+
if "extends ChangeNotifier" in ctx:
|
|
1198
|
+
return "flutter-provider"
|
|
1199
|
+
if "extends GetxController" in ctx or "extends GetxService" in ctx:
|
|
1200
|
+
return "flutter-getx"
|
|
1201
|
+
if "extends Bloc<" in ctx or "extends Cubit<" in ctx:
|
|
1202
|
+
return "flutter-bloc"
|
|
1203
|
+
if "extends Equatable" in ctx:
|
|
1204
|
+
return "dart-equatable"
|
|
1205
|
+
# Firebase
|
|
1206
|
+
if "FirebaseAuth" in ctx or "FirebaseFirestore" in ctx or "FirebaseMessaging" in ctx:
|
|
1207
|
+
return "flutter-firebase"
|
|
1208
|
+
# Dio/HTTP
|
|
1209
|
+
if "Dio()" in ctx or "http.get" in ctx or "http.post" in ctx:
|
|
1210
|
+
return "dart-http"
|
|
1211
|
+
# Riverpod
|
|
1212
|
+
if "extends StateNotifier" in ctx or "extends AsyncNotifier" in ctx:
|
|
1213
|
+
return "flutter-riverpod"
|
|
1214
|
+
if "extends ConsumerWidget" in ctx or "extends ConsumerStatefulWidget" in ctx:
|
|
1215
|
+
return "flutter-riverpod"
|
|
1216
|
+
|
|
1217
|
+
return None
|
|
1218
|
+
|
|
1219
|
+
def _extract_imports_regex(self, source: str, ext: str) -> list[dict]:
|
|
1220
|
+
"""Extract imports using regex for non-tree-sitter languages."""
|
|
1221
|
+
imports = []
|
|
1222
|
+
if ext == ".dart":
|
|
1223
|
+
for m in re.finditer(r"import\s+'([^']+)'", source):
|
|
1224
|
+
module = m.group(1)
|
|
1225
|
+
imports.append({"module": module, "imported": []})
|
|
1226
|
+
return imports
|
|
1227
|
+
|
|
1228
|
+
def _extract_generic(self, source: str, node, symbols: list, current_class: str | None,
|
|
1229
|
+
func_types: str, class_types: str, *extra_types: str) -> None:
|
|
1230
|
+
"""Generic symbol extraction for multiple node types."""
|
|
1231
|
+
node_type = node.type
|
|
1232
|
+
|
|
1233
|
+
func_type_set = {func_types} if isinstance(func_types, str) else set(func_types)
|
|
1234
|
+
class_type_set = {class_types} if isinstance(class_types, str) else set(class_types)
|
|
1235
|
+
all_type_set = func_type_set | class_type_set | set(extra_types)
|
|
1236
|
+
|
|
1237
|
+
if node_type in func_type_set:
|
|
1238
|
+
name = self._get_node_name(node, source)
|
|
1239
|
+
if name:
|
|
1240
|
+
params = self._extract_params(node, source, node_type)
|
|
1241
|
+
calls = self._extract_calls(node, source)
|
|
1242
|
+
doc = self._extract_docstring(node, source)
|
|
1243
|
+
|
|
1244
|
+
sym_type = "function"
|
|
1245
|
+
if current_class:
|
|
1246
|
+
sym_type = "method"
|
|
1247
|
+
|
|
1248
|
+
sym = {
|
|
1249
|
+
"name": name,
|
|
1250
|
+
"type": sym_type,
|
|
1251
|
+
"line": node.start_point.row + 1,
|
|
1252
|
+
"params": params,
|
|
1253
|
+
"calls": calls,
|
|
1254
|
+
"class": current_class,
|
|
1255
|
+
}
|
|
1256
|
+
if doc:
|
|
1257
|
+
sym["doc"] = doc
|
|
1258
|
+
symbols.append(sym)
|
|
1259
|
+
|
|
1260
|
+
elif node_type in class_type_set:
|
|
1261
|
+
name = self._get_node_name(node, source)
|
|
1262
|
+
if name:
|
|
1263
|
+
methods = []
|
|
1264
|
+
class_calls = []
|
|
1265
|
+
doc = self._extract_docstring(node, source)
|
|
1266
|
+
|
|
1267
|
+
for child in node.children:
|
|
1268
|
+
if child.type in func_type_set or (extra_types and child.type in extra_types):
|
|
1269
|
+
method_name = self._get_node_name(child, source)
|
|
1270
|
+
if method_name:
|
|
1271
|
+
params = self._extract_params(child, source, child.type)
|
|
1272
|
+
method_calls = self._extract_calls(child, source)
|
|
1273
|
+
method_doc = self._extract_docstring(child, source)
|
|
1274
|
+
m = {
|
|
1275
|
+
"name": method_name,
|
|
1276
|
+
"type": "method",
|
|
1277
|
+
"line": child.start_point.row + 1,
|
|
1278
|
+
"params": params,
|
|
1279
|
+
"calls": method_calls,
|
|
1280
|
+
}
|
|
1281
|
+
if method_doc:
|
|
1282
|
+
m["doc"] = method_doc
|
|
1283
|
+
methods.append(m)
|
|
1284
|
+
class_calls.extend(method_calls)
|
|
1285
|
+
|
|
1286
|
+
sym = {
|
|
1287
|
+
"name": name,
|
|
1288
|
+
"type": "class",
|
|
1289
|
+
"line": node.start_point.row + 1,
|
|
1290
|
+
"methods": methods,
|
|
1291
|
+
"calls": list(set(class_calls)),
|
|
1292
|
+
}
|
|
1293
|
+
if doc:
|
|
1294
|
+
sym["doc"] = doc
|
|
1295
|
+
symbols.append(sym)
|
|
1296
|
+
|
|
1297
|
+
current_class = name
|
|
1298
|
+
|
|
1299
|
+
for child in node.children:
|
|
1300
|
+
self._extract_generic(source, child, symbols, current_class, func_types, class_types, *extra_types)
|
|
1301
|
+
|
|
1302
|
+
def _get_node_name(self, node, source: str) -> str | None:
|
|
1303
|
+
"""Get name from definition node."""
|
|
1304
|
+
for child in node.children:
|
|
1305
|
+
if child.type == "identifier":
|
|
1306
|
+
return child.text.decode("utf-8")
|
|
1307
|
+
return None
|
|
1308
|
+
|
|
1309
|
+
def _extract_params(self, func_node, source: str, node_type: str) -> list[str]:
|
|
1310
|
+
"""Extract function parameters."""
|
|
1311
|
+
params = []
|
|
1312
|
+
|
|
1313
|
+
for child in func_node.children:
|
|
1314
|
+
if child.type == "parameters":
|
|
1315
|
+
for param in child.children:
|
|
1316
|
+
if param.type == "identifier":
|
|
1317
|
+
params.append(param.text.decode("utf-8"))
|
|
1318
|
+
elif param.type in ("optional_parameter", "rest_parameter", "spread_element",
|
|
1319
|
+
"typed_parameter", "default_parameter", "keyword_argument",
|
|
1320
|
+
"parameter", "receiver"):
|
|
1321
|
+
for p in param.children:
|
|
1322
|
+
if p.type == "identifier":
|
|
1323
|
+
params.append(p.text.decode("utf-8"))
|
|
1324
|
+
break
|
|
1325
|
+
|
|
1326
|
+
return params
|
|
1327
|
+
|
|
1328
|
+
def _extract_calls(self, func_node, source: str) -> list[str]:
|
|
1329
|
+
"""Extract function calls within a function body."""
|
|
1330
|
+
calls = []
|
|
1331
|
+
self._find_calls_recursive(func_node, calls)
|
|
1332
|
+
return list(set(calls))
|
|
1333
|
+
|
|
1334
|
+
def _find_calls_recursive(self, node, calls: list) -> None:
|
|
1335
|
+
"""Recursively find function calls."""
|
|
1336
|
+
if node.type in ("call", "call_expression"):
|
|
1337
|
+
# Get the function being called
|
|
1338
|
+
func = node.child_by_field_name("function") or (node.children[0] if node.children else None)
|
|
1339
|
+
if func:
|
|
1340
|
+
if func.type == "identifier":
|
|
1341
|
+
calls.append(func.text.decode("utf-8"))
|
|
1342
|
+
elif func.type in ("member_expression", "attribute"):
|
|
1343
|
+
# obj.method() — extract method name
|
|
1344
|
+
prop = func.child_by_field_name("property") or func.child_by_field_name("attribute")
|
|
1345
|
+
if prop:
|
|
1346
|
+
calls.append(prop.text.decode("utf-8"))
|
|
1347
|
+
else:
|
|
1348
|
+
# Fallback: last identifier child
|
|
1349
|
+
for child in reversed(func.children):
|
|
1350
|
+
if child.type == "identifier" or child.type == "property_identifier":
|
|
1351
|
+
calls.append(child.text.decode("utf-8"))
|
|
1352
|
+
break
|
|
1353
|
+
elif func.type == "attribute_expression":
|
|
1354
|
+
attr = func.child_by_field_name("attribute")
|
|
1355
|
+
if attr:
|
|
1356
|
+
calls.append(attr.text.decode("utf-8"))
|
|
1357
|
+
|
|
1358
|
+
for child in node.children:
|
|
1359
|
+
self._find_calls_recursive(child, calls)
|
|
1360
|
+
|
|
1361
|
+
def _extract_imports(self, source: str, node, ext: str) -> list[dict[str, Any]]:
|
|
1362
|
+
"""Extract import statements."""
|
|
1363
|
+
imports = []
|
|
1364
|
+
|
|
1365
|
+
if ext in (".js", ".jsx", ".ts", ".tsx"):
|
|
1366
|
+
self._find_js_imports(node, imports)
|
|
1367
|
+
elif ext == ".py":
|
|
1368
|
+
self._find_python_imports(node, imports)
|
|
1369
|
+
|
|
1370
|
+
return imports
|
|
1371
|
+
|
|
1372
|
+
def _find_js_imports(self, node, imports: list) -> None:
|
|
1373
|
+
"""Find JavaScript/TypeScript imports."""
|
|
1374
|
+
if node.type == "import_statement":
|
|
1375
|
+
module_name = None
|
|
1376
|
+
imported = []
|
|
1377
|
+
|
|
1378
|
+
for child in node.children:
|
|
1379
|
+
if child.type == "string":
|
|
1380
|
+
module_name = child.text.decode("utf-8").strip('"\'')
|
|
1381
|
+
elif child.type == "import_clause":
|
|
1382
|
+
for c in child.children:
|
|
1383
|
+
if c.type == "identifier":
|
|
1384
|
+
imported.append(c.text.decode("utf-8"))
|
|
1385
|
+
elif c.type == "named_imports":
|
|
1386
|
+
for ic in c.children:
|
|
1387
|
+
if ic.type == "import_specifier":
|
|
1388
|
+
name = ic.child_by_field_name("name")
|
|
1389
|
+
if name:
|
|
1390
|
+
imported.append(name.text.decode("utf-8"))
|
|
1391
|
+
|
|
1392
|
+
if module_name:
|
|
1393
|
+
imports.append({
|
|
1394
|
+
"module": module_name,
|
|
1395
|
+
"imported": imported,
|
|
1396
|
+
"default": imported[0] if imported else None,
|
|
1397
|
+
})
|
|
1398
|
+
|
|
1399
|
+
for child in node.children:
|
|
1400
|
+
self._find_js_imports(child, imports)
|
|
1401
|
+
|
|
1402
|
+
def _find_python_imports(self, node, imports: list) -> None:
|
|
1403
|
+
"""Find Python imports."""
|
|
1404
|
+
if node.type in ("import_statement", "import_from_statement"):
|
|
1405
|
+
module_name = None
|
|
1406
|
+
imported = []
|
|
1407
|
+
|
|
1408
|
+
for child in node.children:
|
|
1409
|
+
if child.type == "dotted_name":
|
|
1410
|
+
module_name = child.text.decode("utf-8")
|
|
1411
|
+
elif child.type == "aliased_import":
|
|
1412
|
+
for c in child.children:
|
|
1413
|
+
if c.type == "identifier":
|
|
1414
|
+
imported.append(c.text.decode("utf-8"))
|
|
1415
|
+
elif child.type == "wildcard_import":
|
|
1416
|
+
imported.append("*")
|
|
1417
|
+
elif child.type == "dotted_name" and node.type == "import_from_statement":
|
|
1418
|
+
for c in child.children:
|
|
1419
|
+
if c.type == "identifier":
|
|
1420
|
+
imported.append(c.text.decode("utf-8"))
|
|
1421
|
+
|
|
1422
|
+
if module_name:
|
|
1423
|
+
imports.append({
|
|
1424
|
+
"module": module_name,
|
|
1425
|
+
"imported": imported,
|
|
1426
|
+
})
|
|
1427
|
+
|
|
1428
|
+
for child in node.children:
|
|
1429
|
+
self._find_python_imports(child, imports)
|
|
1430
|
+
|
|
1431
|
+
def _extract_exports(self, source: str, node, ext: str) -> list[dict[str, Any]]:
|
|
1432
|
+
"""Extract export statements."""
|
|
1433
|
+
exports = []
|
|
1434
|
+
|
|
1435
|
+
if ext in (".js", ".jsx", ".ts", ".tsx"):
|
|
1436
|
+
self._find_js_exports(node, exports)
|
|
1437
|
+
elif ext == ".py":
|
|
1438
|
+
self._find_python_exports(node, exports)
|
|
1439
|
+
|
|
1440
|
+
return exports
|
|
1441
|
+
|
|
1442
|
+
def _find_js_exports(self, node, exports: list) -> None:
|
|
1443
|
+
"""Find JavaScript/TypeScript exports."""
|
|
1444
|
+
if node.type == "export_statement":
|
|
1445
|
+
for child in node.children:
|
|
1446
|
+
if child.type == "named_export":
|
|
1447
|
+
for c in child.children:
|
|
1448
|
+
if c.type == "export_clause":
|
|
1449
|
+
for ec in c.children:
|
|
1450
|
+
if ec.type == "export_specifier":
|
|
1451
|
+
name = ec.child_by_field_name("name")
|
|
1452
|
+
if name:
|
|
1453
|
+
exports.append({"name": name.text.decode("utf-8"), "type": "named"})
|
|
1454
|
+
elif child.type == "variable_declaration":
|
|
1455
|
+
for c in child.children:
|
|
1456
|
+
if c.type == "variable_declarator":
|
|
1457
|
+
name_node = c.child_by_field_name("name")
|
|
1458
|
+
if name_node:
|
|
1459
|
+
exports.append({"name": name_node.text.decode("utf-8"), "type": "variable"})
|
|
1460
|
+
elif child.type == "class_declaration":
|
|
1461
|
+
name_node = self._get_node_name(child, "")
|
|
1462
|
+
if name_node:
|
|
1463
|
+
exports.append({"name": name_node, "type": "class"})
|
|
1464
|
+
elif child.type == "function_declaration":
|
|
1465
|
+
name_node = self._get_node_name(child, "")
|
|
1466
|
+
if name_node:
|
|
1467
|
+
exports.append({"name": name_node, "type": "function"})
|
|
1468
|
+
|
|
1469
|
+
for child in node.children:
|
|
1470
|
+
self._find_js_exports(child, exports)
|
|
1471
|
+
|
|
1472
|
+
def _find_python_exports(self, node, exports: list) -> None:
|
|
1473
|
+
"""Find Python __all__ exports."""
|
|
1474
|
+
if node.type == "assignment_statement":
|
|
1475
|
+
for child in node.children:
|
|
1476
|
+
if child.type == "attribute" and child.text.decode("utf-8") == "__all__":
|
|
1477
|
+
for c in child.children:
|
|
1478
|
+
if c.type == "list":
|
|
1479
|
+
for lc in c.children:
|
|
1480
|
+
if lc.type == "string":
|
|
1481
|
+
exports.append({"name": lc.text.decode("utf-8").strip('"\''), "type": "explicit"})
|
|
1482
|
+
|
|
1483
|
+
for child in node.children:
|
|
1484
|
+
self._find_python_exports(child, exports)
|
|
1485
|
+
|
|
1486
|
+
def _extract_api_routes(self, source: str, node, ext: str) -> list[dict[str, Any]]:
|
|
1487
|
+
"""Extract API routes/endpoints."""
|
|
1488
|
+
routes = []
|
|
1489
|
+
|
|
1490
|
+
if ext in (".js", ".jsx", ".ts", ".tsx"):
|
|
1491
|
+
self._find_js_routes(node, routes)
|
|
1492
|
+
|
|
1493
|
+
return routes
|
|
1494
|
+
|
|
1495
|
+
def _find_js_routes(self, node, routes: list) -> None:
|
|
1496
|
+
"""Find JavaScript/TypeScript API routes."""
|
|
1497
|
+
source_bytes = node.text if hasattr(node, 'text') else b''
|
|
1498
|
+
source_str = source_bytes.decode("utf-8", errors="ignore")
|
|
1499
|
+
|
|
1500
|
+
import re
|
|
1501
|
+
|
|
1502
|
+
patterns = [
|
|
1503
|
+
(r'["\'](GET|POST|PUT|DELETE|PATCH)\s+["\']([^"\']+)["\']', 'express'),
|
|
1504
|
+
(r'@Get\(["\']([^"\']+)["\']\)', 'nestjs'),
|
|
1505
|
+
(r'@Post\(["\']([^"\']+)["\']\)', 'nestjs'),
|
|
1506
|
+
(r'@Put\(["\']([^"\']+)["\']\)', 'nestjs'),
|
|
1507
|
+
(r'@Delete\(["\']([^"\']+)["\']\)', 'nestjs'),
|
|
1508
|
+
(r'@RequestMapping\(["\']([^"\']+)["\']', 'spring'),
|
|
1509
|
+
(r'router\.(get|post|put|delete|patch)\(["\']([^"\']+)["\']', 'express'),
|
|
1510
|
+
(r'app\.(get|post|put|delete|patch)\(["\']([^"\']+)["\']', 'express'),
|
|
1511
|
+
]
|
|
1512
|
+
|
|
1513
|
+
for pattern, framework in patterns:
|
|
1514
|
+
matches = re.findall(pattern, source_str)
|
|
1515
|
+
for match in matches:
|
|
1516
|
+
if len(match) == 2:
|
|
1517
|
+
method = match[0] if match[0] in ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'] else framework
|
|
1518
|
+
path = match[1] if match[0] in ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'] else match[1]
|
|
1519
|
+
routes.append({"method": method.upper(), "path": path, "framework": framework})
|
|
1520
|
+
|
|
1521
|
+
for child in node.children:
|
|
1522
|
+
self._find_js_routes(child, routes)
|
|
1523
|
+
|
|
1524
|
+
def _extract_entities(self, source: str, node, ext: str) -> list[dict[str, Any]]:
|
|
1525
|
+
"""Extract database entities/models."""
|
|
1526
|
+
entities = []
|
|
1527
|
+
|
|
1528
|
+
if ext in (".js", ".jsx", ".ts", ".tsx"):
|
|
1529
|
+
self._find_js_entities(node, entities, source)
|
|
1530
|
+
elif ext == ".py":
|
|
1531
|
+
self._find_python_entities(node, entities, source)
|
|
1532
|
+
|
|
1533
|
+
return entities
|
|
1534
|
+
|
|
1535
|
+
def _find_js_entities(self, node, entities: list, source: str) -> None:
|
|
1536
|
+
"""Find JavaScript/TypeScript entities/models."""
|
|
1537
|
+
source_bytes = node.text if hasattr(node, 'text') else b''
|
|
1538
|
+
source_str = source_bytes.decode("utf-8", errors="ignore")
|
|
1539
|
+
|
|
1540
|
+
if node.type == "class_declaration":
|
|
1541
|
+
name = self._get_node_name(node, source_str)
|
|
1542
|
+
if name:
|
|
1543
|
+
entity_type = "unknown"
|
|
1544
|
+
if "@Entity" in source_str:
|
|
1545
|
+
entity_type = "typeorm"
|
|
1546
|
+
elif "sequelize" in source_str.lower() or "Model" in name:
|
|
1547
|
+
entity_type = "sequelize"
|
|
1548
|
+
elif "prisma" in source_str.lower() or "@Model" in source_str:
|
|
1549
|
+
entity_type = "prisma"
|
|
1550
|
+
|
|
1551
|
+
fields = []
|
|
1552
|
+
for child in node.children:
|
|
1553
|
+
if child.type == "class_body":
|
|
1554
|
+
for cb in child.children:
|
|
1555
|
+
if cb.type == "field_definition":
|
|
1556
|
+
field_name = self._get_node_name(cb, source_str)
|
|
1557
|
+
if field_name:
|
|
1558
|
+
fields.append(field_name)
|
|
1559
|
+
|
|
1560
|
+
entities.append({
|
|
1561
|
+
"name": name,
|
|
1562
|
+
"type": entity_type,
|
|
1563
|
+
"fields": fields,
|
|
1564
|
+
})
|
|
1565
|
+
|
|
1566
|
+
for child in node.children:
|
|
1567
|
+
self._find_js_entities(child, entities, source_str)
|
|
1568
|
+
|
|
1569
|
+
def _find_python_entities(self, node, entities: list, source: str) -> None:
|
|
1570
|
+
"""Find Python entities/models."""
|
|
1571
|
+
source_bytes = node.text if hasattr(node, 'text') else b''
|
|
1572
|
+
source_str = source_bytes.decode("utf-8", errors="ignore")
|
|
1573
|
+
|
|
1574
|
+
if node.type == "class_definition":
|
|
1575
|
+
name = self._get_node_name(node, source_str)
|
|
1576
|
+
if name:
|
|
1577
|
+
entity_type = "unknown"
|
|
1578
|
+
if "SQLModel" in source_str or "Base" in source_str:
|
|
1579
|
+
entity_type = "sqlmodel"
|
|
1580
|
+
elif "Flask" in source_str or "SQLAlchemy" in source_str:
|
|
1581
|
+
entity_type = "sqlalchemy"
|
|
1582
|
+
elif "Django" in source_str:
|
|
1583
|
+
entity_type = "django"
|
|
1584
|
+
elif "Pydantic" in source_str:
|
|
1585
|
+
entity_type = "pydantic"
|
|
1586
|
+
|
|
1587
|
+
fields = []
|
|
1588
|
+
for child in node.children:
|
|
1589
|
+
if child.type == "block":
|
|
1590
|
+
for bc in child.children:
|
|
1591
|
+
if bc.type == "expression_statement":
|
|
1592
|
+
for bcc in bc.children:
|
|
1593
|
+
if bcc.type == "assignment":
|
|
1594
|
+
for bcca in bcc.children:
|
|
1595
|
+
if bcca.type == "identifier":
|
|
1596
|
+
fields.append(bcca.text.decode("utf-8"))
|
|
1597
|
+
|
|
1598
|
+
entities.append({
|
|
1599
|
+
"name": name,
|
|
1600
|
+
"type": entity_type,
|
|
1601
|
+
"fields": fields,
|
|
1602
|
+
})
|
|
1603
|
+
|
|
1604
|
+
for child in node.children:
|
|
1605
|
+
self._find_python_entities(child, entities, source_str)
|
|
1606
|
+
|
|
1607
|
+
def _extract_docstring(self, node, source: str) -> str | None:
|
|
1608
|
+
"""Extract Python docstring from a function/class body."""
|
|
1609
|
+
# Look for the first expression_statement in the body that is a string
|
|
1610
|
+
for child in node.children:
|
|
1611
|
+
if child.type == "block":
|
|
1612
|
+
for bc in child.children:
|
|
1613
|
+
if bc.type == "expression_statement":
|
|
1614
|
+
for bcc in bc.children:
|
|
1615
|
+
if bcc.type == "string":
|
|
1616
|
+
doc = bcc.text.decode("utf-8", errors="ignore")
|
|
1617
|
+
# Strip triple quotes
|
|
1618
|
+
doc = doc.strip('"').strip("'").strip()
|
|
1619
|
+
if doc:
|
|
1620
|
+
# Truncate long docstrings
|
|
1621
|
+
return doc[:200] + "..." if len(doc) > 200 else doc
|
|
1622
|
+
break # Only check first statement
|
|
1623
|
+
break
|
|
1624
|
+
return None
|
|
1625
|
+
|
|
1626
|
+
def _extract_jsdoc(self, node, source: str) -> str | None:
|
|
1627
|
+
"""Extract JSDoc comment preceding a node."""
|
|
1628
|
+
# Check for comment node preceding this node
|
|
1629
|
+
start_line = node.start_point.row
|
|
1630
|
+
source_lines = source.split("\n")
|
|
1631
|
+
|
|
1632
|
+
# Walk backwards from the node to find a JSDoc comment
|
|
1633
|
+
doc_lines = []
|
|
1634
|
+
in_jsdoc = False
|
|
1635
|
+
for i in range(start_line - 1, max(start_line - 15, -1), -1):
|
|
1636
|
+
if i < 0 or i >= len(source_lines):
|
|
1637
|
+
break
|
|
1638
|
+
line = source_lines[i].strip()
|
|
1639
|
+
|
|
1640
|
+
if line.endswith("*/"):
|
|
1641
|
+
in_jsdoc = True
|
|
1642
|
+
line = line[:-2].strip()
|
|
1643
|
+
if line:
|
|
1644
|
+
doc_lines.insert(0, line.lstrip("* "))
|
|
1645
|
+
elif in_jsdoc:
|
|
1646
|
+
if line.startswith("/**"):
|
|
1647
|
+
line = line[3:].strip()
|
|
1648
|
+
if line:
|
|
1649
|
+
doc_lines.insert(0, line.lstrip("* "))
|
|
1650
|
+
break
|
|
1651
|
+
elif line.startswith("/*"):
|
|
1652
|
+
line = line[2:].strip()
|
|
1653
|
+
if line:
|
|
1654
|
+
doc_lines.insert(0, line.lstrip("* "))
|
|
1655
|
+
break
|
|
1656
|
+
elif line.startswith("*"):
|
|
1657
|
+
line = line[1:].strip()
|
|
1658
|
+
if line and not line.startswith("@"):
|
|
1659
|
+
doc_lines.insert(0, line)
|
|
1660
|
+
else:
|
|
1661
|
+
break
|
|
1662
|
+
elif line.startswith("//"):
|
|
1663
|
+
# Single-line comment
|
|
1664
|
+
doc_lines.insert(0, line[2:].strip())
|
|
1665
|
+
elif line == "":
|
|
1666
|
+
if doc_lines:
|
|
1667
|
+
break
|
|
1668
|
+
continue
|
|
1669
|
+
else:
|
|
1670
|
+
break
|
|
1671
|
+
|
|
1672
|
+
if doc_lines:
|
|
1673
|
+
doc = " ".join(doc_lines).strip()
|
|
1674
|
+
return doc[:200] + "..." if len(doc) > 200 else doc
|
|
1675
|
+
return None
|
|
1676
|
+
|
|
1677
|
+
def _build_index(self, root: Path) -> dict[str, Any]:
|
|
1678
|
+
"""Build the final index structure."""
|
|
1679
|
+
languages = set()
|
|
1680
|
+
for file_path in self.file_symbols.keys():
|
|
1681
|
+
ext = Path(file_path).suffix.lower()
|
|
1682
|
+
lang_info = LANGUAGE_MAP.get(ext)
|
|
1683
|
+
if lang_info:
|
|
1684
|
+
languages.add(lang_info[0])
|
|
1685
|
+
elif ext in REGEX_LANGUAGES:
|
|
1686
|
+
languages.add(REGEX_LANGUAGES[ext])
|
|
1687
|
+
else:
|
|
1688
|
+
languages.add(ext.lstrip("."))
|
|
1689
|
+
|
|
1690
|
+
# Build file dependency graph from imports
|
|
1691
|
+
file_deps = self._build_file_dependencies()
|
|
1692
|
+
|
|
1693
|
+
# Build cross-file type map
|
|
1694
|
+
type_map = self._build_type_map()
|
|
1695
|
+
|
|
1696
|
+
result = {
|
|
1697
|
+
"project_root": str(root),
|
|
1698
|
+
"last_indexed": self._timestamp(),
|
|
1699
|
+
"files": self.file_symbols,
|
|
1700
|
+
"call_graph": self.call_graph,
|
|
1701
|
+
"file_dependencies": file_deps,
|
|
1702
|
+
"file_hashes": self._compute_hashes(root),
|
|
1703
|
+
"languages": list(languages),
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
if type_map:
|
|
1707
|
+
result["type_map"] = type_map
|
|
1708
|
+
|
|
1709
|
+
# Run plugin post-processors
|
|
1710
|
+
result = plugin_registry.run_post_processors(result)
|
|
1711
|
+
|
|
1712
|
+
return result
|
|
1713
|
+
|
|
1714
|
+
def _build_type_map(self) -> dict[str, dict[str, Any]]:
|
|
1715
|
+
"""Build a cross-file type map: symbol name -> definition location + type info.
|
|
1716
|
+
|
|
1717
|
+
This resolves imported symbols to their source definitions, so an AI agent
|
|
1718
|
+
can look up where a type/function is actually defined even when it's imported.
|
|
1719
|
+
"""
|
|
1720
|
+
# Step 1: Build export registry — what each file exports
|
|
1721
|
+
export_registry: dict[str, dict[str, str]] = {} # symbol_name -> {file, type, line}
|
|
1722
|
+
for rel_path, file_data in self.file_symbols.items():
|
|
1723
|
+
if not isinstance(file_data, dict):
|
|
1724
|
+
continue
|
|
1725
|
+
|
|
1726
|
+
# Register all top-level symbols as potential exports
|
|
1727
|
+
for sym in file_data.get("symbols", []):
|
|
1728
|
+
name = sym.get("name")
|
|
1729
|
+
if name and not sym.get("class"): # Only top-level
|
|
1730
|
+
export_registry[name] = {
|
|
1731
|
+
"defined_in": rel_path,
|
|
1732
|
+
"type": sym.get("type"),
|
|
1733
|
+
"line": sym.get("line"),
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
# Register explicit exports
|
|
1737
|
+
for exp in file_data.get("exports", []):
|
|
1738
|
+
name = exp.get("name")
|
|
1739
|
+
if name and name not in export_registry:
|
|
1740
|
+
export_registry[name] = {
|
|
1741
|
+
"defined_in": rel_path,
|
|
1742
|
+
"type": exp.get("type", "export"),
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
# Step 2: Resolve imports to definitions
|
|
1746
|
+
type_map: dict[str, dict[str, Any]] = {}
|
|
1747
|
+
for rel_path, file_data in self.file_symbols.items():
|
|
1748
|
+
if not isinstance(file_data, dict):
|
|
1749
|
+
continue
|
|
1750
|
+
|
|
1751
|
+
for imp in file_data.get("imports", []):
|
|
1752
|
+
imported_names = imp.get("imported", [])
|
|
1753
|
+
for imported_name in imported_names:
|
|
1754
|
+
if imported_name in export_registry:
|
|
1755
|
+
defn = export_registry[imported_name]
|
|
1756
|
+
if defn["defined_in"] != rel_path:
|
|
1757
|
+
key = f"{rel_path}:{imported_name}"
|
|
1758
|
+
type_map[key] = {
|
|
1759
|
+
"imported_in": rel_path,
|
|
1760
|
+
"name": imported_name,
|
|
1761
|
+
"defined_in": defn["defined_in"],
|
|
1762
|
+
"type": defn.get("type"),
|
|
1763
|
+
"line": defn.get("line"),
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
return type_map
|
|
1767
|
+
|
|
1768
|
+
def _build_file_dependencies(self) -> dict[str, list[str]]:
|
|
1769
|
+
"""Build a graph of which files import from which other files."""
|
|
1770
|
+
deps = {}
|
|
1771
|
+
|
|
1772
|
+
# Map module names to files for resolution
|
|
1773
|
+
module_to_file = {}
|
|
1774
|
+
for rel_path in self.file_symbols:
|
|
1775
|
+
# Register by filename stem and path variants
|
|
1776
|
+
stem = Path(rel_path).stem
|
|
1777
|
+
module_to_file[stem] = rel_path
|
|
1778
|
+
# Also register without extension
|
|
1779
|
+
no_ext = str(Path(rel_path).with_suffix(""))
|
|
1780
|
+
module_to_file[no_ext] = rel_path
|
|
1781
|
+
module_to_file[no_ext.replace("\\", "/")] = rel_path
|
|
1782
|
+
|
|
1783
|
+
for rel_path, file_data in self.file_symbols.items():
|
|
1784
|
+
if not isinstance(file_data, dict):
|
|
1785
|
+
continue
|
|
1786
|
+
imports = file_data.get("imports", [])
|
|
1787
|
+
if not imports:
|
|
1788
|
+
continue
|
|
1789
|
+
|
|
1790
|
+
dep_files = []
|
|
1791
|
+
for imp in imports:
|
|
1792
|
+
module = imp.get("module", "")
|
|
1793
|
+
if not module:
|
|
1794
|
+
continue
|
|
1795
|
+
|
|
1796
|
+
# Skip external packages (no relative path prefix, no /)
|
|
1797
|
+
if module.startswith("."):
|
|
1798
|
+
# Resolve relative import
|
|
1799
|
+
base_dir = str(Path(rel_path).parent)
|
|
1800
|
+
resolved = module.lstrip("./")
|
|
1801
|
+
candidate = f"{base_dir}/{resolved}" if base_dir != "." else resolved
|
|
1802
|
+
|
|
1803
|
+
# Try to find matching file
|
|
1804
|
+
for ext in (".ts", ".tsx", ".js", ".jsx", ".py"):
|
|
1805
|
+
key = candidate + ext
|
|
1806
|
+
if key.replace("\\", "/") in {k.replace("\\", "/") for k in self.file_symbols}:
|
|
1807
|
+
dep_files.append(key.replace("\\", "/"))
|
|
1808
|
+
break
|
|
1809
|
+
else:
|
|
1810
|
+
# Try index file
|
|
1811
|
+
for ext in (".ts", ".tsx", ".js", ".jsx"):
|
|
1812
|
+
key = f"{candidate}/index{ext}"
|
|
1813
|
+
if key.replace("\\", "/") in {k.replace("\\", "/") for k in self.file_symbols}:
|
|
1814
|
+
dep_files.append(key.replace("\\", "/"))
|
|
1815
|
+
break
|
|
1816
|
+
else:
|
|
1817
|
+
# Absolute import — try to match against known files
|
|
1818
|
+
clean = module.replace("@/", "src/").replace("~/", "")
|
|
1819
|
+
if clean in module_to_file:
|
|
1820
|
+
dep_files.append(module_to_file[clean])
|
|
1821
|
+
|
|
1822
|
+
if dep_files:
|
|
1823
|
+
deps[rel_path] = list(set(dep_files))
|
|
1824
|
+
|
|
1825
|
+
return deps
|
|
1826
|
+
|
|
1827
|
+
def _timestamp(self) -> str:
|
|
1828
|
+
"""Get current ISO timestamp."""
|
|
1829
|
+
from datetime import datetime, timezone
|
|
1830
|
+
return datetime.now(timezone.utc).isoformat()
|
|
1831
|
+
|
|
1832
|
+
def _compute_hashes(self, root: Path) -> dict[str, str]:
|
|
1833
|
+
"""Compute SHA-256 hashes for all indexed files."""
|
|
1834
|
+
hashes = {}
|
|
1835
|
+
for rel_path in self.file_symbols:
|
|
1836
|
+
file_path = root / rel_path
|
|
1837
|
+
try:
|
|
1838
|
+
content = file_path.read_bytes()
|
|
1839
|
+
hashes[rel_path] = hashlib.sha256(content).hexdigest()
|
|
1840
|
+
except OSError:
|
|
1841
|
+
pass
|
|
1842
|
+
return hashes
|
|
1843
|
+
|
|
1844
|
+
|
|
1845
|
+
def index_directory(path: str | Path, incremental: bool = False) -> dict[str, Any]:
|
|
1846
|
+
"""Convenience function to index a directory."""
|
|
1847
|
+
indexer = CodeIndexer()
|
|
1848
|
+
return indexer.index_directory(Path(path), incremental=incremental)
|
|
1849
|
+
|
|
1850
|
+
|
|
1851
|
+
def save_index(index: dict[str, Any], output_path: Path) -> None:
|
|
1852
|
+
"""Save index to JSON file."""
|
|
1853
|
+
output_path = Path(output_path)
|
|
1854
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1855
|
+
output_path.write_text(json.dumps(index, indent=2), encoding="utf-8")
|
|
1856
|
+
|
|
1857
|
+
|
|
1858
|
+
def load_index(index_path: Path) -> dict[str, Any]:
|
|
1859
|
+
"""Load index from JSON file."""
|
|
1860
|
+
return json.loads(index_path.read_text(encoding="utf-8"))
|