codegraph-py 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.
- codegraph/__init__.py +12 -0
- codegraph/__main__.py +6 -0
- codegraph/cli.py +911 -0
- codegraph/codegraph.py +618 -0
- codegraph/context/__init__.py +216 -0
- codegraph/db/__init__.py +11 -0
- codegraph/db/connection.py +163 -0
- codegraph/db/queries.py +752 -0
- codegraph/db/schema.sql +151 -0
- codegraph/directory.py +160 -0
- codegraph/errors.py +101 -0
- codegraph/extraction/__init__.py +956 -0
- codegraph/extraction/languages/__init__.py +64 -0
- codegraph/extraction/languages/base.py +132 -0
- codegraph/extraction/languages/c_cfg.py +53 -0
- codegraph/extraction/languages/cpp_cfg.py +60 -0
- codegraph/extraction/languages/dart_cfg.py +53 -0
- codegraph/extraction/languages/go_cfg.py +70 -0
- codegraph/extraction/languages/java_cfg.py +62 -0
- codegraph/extraction/languages/javascript_cfg.py +84 -0
- codegraph/extraction/languages/kotlin_cfg.py +52 -0
- codegraph/extraction/languages/lua_cfg.py +65 -0
- codegraph/extraction/languages/python_cfg.py +75 -0
- codegraph/extraction/languages/ruby_cfg.py +48 -0
- codegraph/extraction/languages/rust_cfg.py +68 -0
- codegraph/extraction/languages/scala_cfg.py +59 -0
- codegraph/extraction/languages/swift_cfg.py +64 -0
- codegraph/extraction/languages/typescript_cfg.py +89 -0
- codegraph/extraction/tree_sitter_extractor.py +689 -0
- codegraph/graph/__init__.py +685 -0
- codegraph/installer/__init__.py +6 -0
- codegraph/mcp/__init__.py +668 -0
- codegraph/project_config.py +191 -0
- codegraph/resolution/__init__.py +337 -0
- codegraph/search/__init__.py +653 -0
- codegraph/sync/__init__.py +204 -0
- codegraph/types.py +334 -0
- codegraph/ui/__init__.py +11 -0
- codegraph/utils.py +218 -0
- codegraph_py-1.0.0.dist-info/METADATA +238 -0
- codegraph_py-1.0.0.dist-info/RECORD +44 -0
- codegraph_py-1.0.0.dist-info/WHEEL +5 -0
- codegraph_py-1.0.0.dist-info/entry_points.txt +2 -0
- codegraph_py-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,689 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tree-sitter-based code extraction engine.
|
|
3
|
+
|
|
4
|
+
Parses source files using tree-sitter and extracts structural information:
|
|
5
|
+
functions, classes, methods, imports, calls, and their relationships.
|
|
6
|
+
Replaces/enhances the regex-based parsers with full AST parsing.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import time
|
|
11
|
+
import logging
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
from tree_sitter import Language, Parser, Node as TSNode, Tree
|
|
16
|
+
|
|
17
|
+
from ..types import (
|
|
18
|
+
Language as LangEnum,
|
|
19
|
+
Node, Edge, NodeKind, EdgeKind,
|
|
20
|
+
ExtractionResult, ExtractionError, UnresolvedReference,
|
|
21
|
+
FileRecord,
|
|
22
|
+
)
|
|
23
|
+
from .languages.base import LanguageConfig, ImportInfo
|
|
24
|
+
from .languages import get_config
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _normalize_path(file_path: str) -> str:
|
|
30
|
+
"""Normalize file path to use forward slashes."""
|
|
31
|
+
return file_path.replace('\\', '/')
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _make_id(kind: str, file_path: str, name: str) -> str:
|
|
35
|
+
"""Create a deterministic node ID matching existing convention."""
|
|
36
|
+
clean = name.replace(' ', '_')
|
|
37
|
+
return f'{kind}:{_normalize_path(file_path)}::{clean}'
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _node_text(node: TSNode) -> str:
|
|
41
|
+
"""Get text from a node safely."""
|
|
42
|
+
try:
|
|
43
|
+
t = node.text
|
|
44
|
+
if isinstance(t, bytes):
|
|
45
|
+
return t.decode('utf-8', errors='replace')
|
|
46
|
+
return str(t)
|
|
47
|
+
except Exception:
|
|
48
|
+
return ''
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class TreeSitterExtractor:
|
|
52
|
+
"""
|
|
53
|
+
Extracts code structure using tree-sitter AST parsing.
|
|
54
|
+
|
|
55
|
+
For each file, it:
|
|
56
|
+
1. Detects the language config
|
|
57
|
+
2. Parses with tree-sitter
|
|
58
|
+
3. Walks the AST to extract symbols and relationships
|
|
59
|
+
4. Returns ExtractionResult matching the existing type system
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def __init__(self):
|
|
63
|
+
self._parsers: dict[str, Parser] = {}
|
|
64
|
+
self._languages: dict[str, Language] = {}
|
|
65
|
+
|
|
66
|
+
def _get_language(self, lang_id: str) -> Optional[Language]:
|
|
67
|
+
"""Get or create a tree-sitter Language for the given language id."""
|
|
68
|
+
if lang_id not in self._languages:
|
|
69
|
+
try:
|
|
70
|
+
import importlib
|
|
71
|
+
mod_name = f'tree_sitter_{lang_id.replace("-", "_")}'
|
|
72
|
+
try:
|
|
73
|
+
mod = importlib.import_module(mod_name)
|
|
74
|
+
except ModuleNotFoundError:
|
|
75
|
+
logger.debug(f"Tree-sitter grammar not available: '{lang_id}'")
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
# Try standard 'language()' function first
|
|
79
|
+
lang_func = getattr(mod, 'language', None)
|
|
80
|
+
if lang_func is not None:
|
|
81
|
+
self._languages[lang_id] = Language(lang_func())
|
|
82
|
+
else:
|
|
83
|
+
# Some packages use language_<grammar_name> (e.g. typescript)
|
|
84
|
+
specific_func = getattr(mod, f'language_{lang_id}', None)
|
|
85
|
+
if specific_func is not None:
|
|
86
|
+
self._languages[lang_id] = Language(specific_func())
|
|
87
|
+
else:
|
|
88
|
+
logger.debug(f"No language function found in '{mod_name}'")
|
|
89
|
+
return None
|
|
90
|
+
except Exception as e:
|
|
91
|
+
logger.debug(f"Failed to load tree-sitter language '{lang_id}': {e}")
|
|
92
|
+
return None
|
|
93
|
+
return self._languages.get(lang_id)
|
|
94
|
+
|
|
95
|
+
def _get_parser(self, lang_id: str) -> Optional[Parser]:
|
|
96
|
+
"""Get or create a parser for the given language."""
|
|
97
|
+
if lang_id not in self._parsers:
|
|
98
|
+
ts_lang = self._get_language(lang_id)
|
|
99
|
+
if ts_lang is None:
|
|
100
|
+
return None
|
|
101
|
+
parser = Parser()
|
|
102
|
+
parser.language = ts_lang
|
|
103
|
+
self._parsers[lang_id] = parser
|
|
104
|
+
return self._parsers.get(lang_id)
|
|
105
|
+
|
|
106
|
+
def extract(
|
|
107
|
+
self, file_path: str, source: bytes, lang_enum: LangEnum
|
|
108
|
+
) -> ExtractionResult:
|
|
109
|
+
"""
|
|
110
|
+
Extract code structure from a source file using tree-sitter.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
file_path: Normalized path (relative to project root)
|
|
114
|
+
source: Raw file content as bytes
|
|
115
|
+
lang_enum: Language enum value
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
ExtractionResult with nodes, edges, etc.
|
|
119
|
+
"""
|
|
120
|
+
start_time = time.time()
|
|
121
|
+
config = get_config(lang_enum)
|
|
122
|
+
if config is None:
|
|
123
|
+
return ExtractionResult(
|
|
124
|
+
nodes=[_make_file_node(file_path, source, lang_enum.value)],
|
|
125
|
+
duration_ms=(time.time() - start_time) * 1000,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
parser = self._get_parser(config.language_id)
|
|
129
|
+
if parser is None:
|
|
130
|
+
return ExtractionResult(
|
|
131
|
+
nodes=[_make_file_node(file_path, source, lang_enum.value)],
|
|
132
|
+
errors=[ExtractionError(
|
|
133
|
+
message=f"Tree-sitter parser not available for {lang_enum.value}",
|
|
134
|
+
severity='warning',
|
|
135
|
+
)],
|
|
136
|
+
duration_ms=(time.time() - start_time) * 1000,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# Parse
|
|
140
|
+
try:
|
|
141
|
+
tree = parser.parse(source)
|
|
142
|
+
except Exception as e:
|
|
143
|
+
return ExtractionResult(
|
|
144
|
+
nodes=[_make_file_node(file_path, source, lang_enum.value)],
|
|
145
|
+
errors=[ExtractionError(
|
|
146
|
+
message=f"Parse error: {e}",
|
|
147
|
+
file_path=file_path,
|
|
148
|
+
severity='error',
|
|
149
|
+
)],
|
|
150
|
+
duration_ms=(time.time() - start_time) * 1000,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Extract
|
|
154
|
+
extractor = _FileExtractor(file_path, source, config, tree, lang_enum)
|
|
155
|
+
result = extractor.extract()
|
|
156
|
+
result.duration_ms = (time.time() - start_time) * 1000
|
|
157
|
+
return result
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _make_file_node(file_path: str, source: bytes, language: str) -> Node:
|
|
161
|
+
"""Create a basic file node."""
|
|
162
|
+
lines = source.splitlines() if source else []
|
|
163
|
+
nlines = max(len(lines), 1)
|
|
164
|
+
norm = _normalize_path(file_path)
|
|
165
|
+
return Node(
|
|
166
|
+
id=f'file:{norm}',
|
|
167
|
+
kind='file',
|
|
168
|
+
name=Path(file_path).name,
|
|
169
|
+
qualified_name=norm,
|
|
170
|
+
file_path=norm,
|
|
171
|
+
language=language,
|
|
172
|
+
start_line=1,
|
|
173
|
+
end_line=nlines,
|
|
174
|
+
start_column=0,
|
|
175
|
+
end_column=0,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class _FileExtractor:
|
|
180
|
+
"""Extracts structure from a single file's AST."""
|
|
181
|
+
|
|
182
|
+
def __init__(
|
|
183
|
+
self,
|
|
184
|
+
file_path: str,
|
|
185
|
+
source: bytes,
|
|
186
|
+
config: LanguageConfig,
|
|
187
|
+
tree: Tree,
|
|
188
|
+
lang_enum: LangEnum,
|
|
189
|
+
):
|
|
190
|
+
self.file_path = file_path
|
|
191
|
+
self.norm_path = _normalize_path(file_path)
|
|
192
|
+
self.source = source
|
|
193
|
+
self.config = config
|
|
194
|
+
self.tree = tree
|
|
195
|
+
self.lang = lang_enum.value
|
|
196
|
+
|
|
197
|
+
self.nodes: list[Node] = []
|
|
198
|
+
self.edges: list[Edge] = []
|
|
199
|
+
self.errors: list[ExtractionError] = []
|
|
200
|
+
self.unresolved_refs: list[UnresolvedReference] = []
|
|
201
|
+
|
|
202
|
+
# Node kind sets for fast dispatch
|
|
203
|
+
t = config
|
|
204
|
+
self._func_set = set(t.function_types)
|
|
205
|
+
self._class_set = set(t.class_types)
|
|
206
|
+
self._method_set = set(t.method_types)
|
|
207
|
+
self._interface_set = set(t.interface_types)
|
|
208
|
+
self._struct_set = set(t.struct_types)
|
|
209
|
+
self._enum_set = set(t.enum_types)
|
|
210
|
+
self._type_alias_set = set(t.type_alias_types)
|
|
211
|
+
self._call_set = set(t.call_types)
|
|
212
|
+
self._var_set = set(t.variable_types)
|
|
213
|
+
|
|
214
|
+
# Track scope for method detection
|
|
215
|
+
self._in_class_body = False
|
|
216
|
+
self._current_class_id: Optional[str] = None
|
|
217
|
+
|
|
218
|
+
# ------------------------------------------------------------------
|
|
219
|
+
# Helpers
|
|
220
|
+
# ------------------------------------------------------------------
|
|
221
|
+
|
|
222
|
+
def _pos(self, node: TSNode) -> tuple[int, int, int, int]:
|
|
223
|
+
"""Return (start_line, end_line, start_col, end_col)."""
|
|
224
|
+
return (
|
|
225
|
+
node.start_point[0] + 1,
|
|
226
|
+
node.end_point[0] + 1,
|
|
227
|
+
node.start_point[1],
|
|
228
|
+
node.end_point[1],
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
def _node_name(self, node: TSNode) -> Optional[str]:
|
|
232
|
+
"""Extract name via the configured name_field."""
|
|
233
|
+
name_node = node.child_by_field_name(self.config.name_field)
|
|
234
|
+
if name_node is not None:
|
|
235
|
+
return _node_text(name_node)
|
|
236
|
+
for child in node.named_children:
|
|
237
|
+
if child.type in ('identifier', 'property_identifier', 'type_identifier'):
|
|
238
|
+
return _node_text(child)
|
|
239
|
+
return None
|
|
240
|
+
|
|
241
|
+
def _get_lines(self) -> int:
|
|
242
|
+
return max(len(self.source.splitlines()), 1) if self.source else 1
|
|
243
|
+
|
|
244
|
+
# ------------------------------------------------------------------
|
|
245
|
+
# Main extraction
|
|
246
|
+
# ------------------------------------------------------------------
|
|
247
|
+
|
|
248
|
+
def extract(self) -> ExtractionResult:
|
|
249
|
+
"""Run extraction and return result."""
|
|
250
|
+
root = self.tree.root_node
|
|
251
|
+
nlines = self._get_lines()
|
|
252
|
+
|
|
253
|
+
file_node = Node(
|
|
254
|
+
id=f'file:{self.norm_path}',
|
|
255
|
+
kind='file',
|
|
256
|
+
name=Path(self.file_path).name,
|
|
257
|
+
qualified_name=self.norm_path,
|
|
258
|
+
file_path=self.norm_path,
|
|
259
|
+
language=self.lang,
|
|
260
|
+
start_line=1,
|
|
261
|
+
end_line=nlines,
|
|
262
|
+
start_column=0,
|
|
263
|
+
end_column=0,
|
|
264
|
+
)
|
|
265
|
+
self.nodes.append(file_node)
|
|
266
|
+
|
|
267
|
+
# Visit top-level children
|
|
268
|
+
self._visit_children(root, file_node.id)
|
|
269
|
+
|
|
270
|
+
return ExtractionResult(
|
|
271
|
+
nodes=self.nodes,
|
|
272
|
+
edges=self.edges,
|
|
273
|
+
unresolved_references=self.unresolved_refs,
|
|
274
|
+
errors=self.errors,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
# ------------------------------------------------------------------
|
|
278
|
+
# AST traversal
|
|
279
|
+
# ------------------------------------------------------------------
|
|
280
|
+
|
|
281
|
+
def _visit_children(self, parent: TSNode, parent_node_id: str):
|
|
282
|
+
"""Visit all named children of a node."""
|
|
283
|
+
for child in parent.named_children:
|
|
284
|
+
self._visit_node(child, parent_node_id)
|
|
285
|
+
|
|
286
|
+
def _visit_node(self, node: TSNode, parent_node_id: str):
|
|
287
|
+
"""Dispatch a single node based on type."""
|
|
288
|
+
if not node.is_named or self.config.should_skip_type(node.type):
|
|
289
|
+
return
|
|
290
|
+
|
|
291
|
+
t = node.type
|
|
292
|
+
|
|
293
|
+
if t in self._func_set and not self._in_class_body:
|
|
294
|
+
self._extract_function(node, parent_node_id)
|
|
295
|
+
elif t in self._method_set and self._in_class_body:
|
|
296
|
+
self._extract_method(node, parent_node_id)
|
|
297
|
+
elif t in self._class_set:
|
|
298
|
+
self._extract_class(node, parent_node_id)
|
|
299
|
+
elif t in self._interface_set:
|
|
300
|
+
self._extract_interface(node, parent_node_id)
|
|
301
|
+
elif t in self._struct_set:
|
|
302
|
+
self._extract_struct(node, parent_node_id)
|
|
303
|
+
elif t in self._enum_set:
|
|
304
|
+
self._extract_enum(node, parent_node_id)
|
|
305
|
+
elif t in self._type_alias_set:
|
|
306
|
+
self._extract_type_alias(node, parent_node_id)
|
|
307
|
+
elif t in self._call_set:
|
|
308
|
+
self._extract_call(node, parent_node_id)
|
|
309
|
+
elif self.config.extract_import(node, self.source) is not None:
|
|
310
|
+
self._extract_import(node, parent_node_id)
|
|
311
|
+
else:
|
|
312
|
+
# Continue traversing
|
|
313
|
+
self._visit_children(node, parent_node_id)
|
|
314
|
+
|
|
315
|
+
# ------------------------------------------------------------------
|
|
316
|
+
# Symbol extractors
|
|
317
|
+
# ------------------------------------------------------------------
|
|
318
|
+
|
|
319
|
+
def _extract_function(self, node: TSNode, parent_node_id: str):
|
|
320
|
+
"""Extract a top-level function."""
|
|
321
|
+
name = self._node_name(node)
|
|
322
|
+
if not name:
|
|
323
|
+
self._visit_children(node, parent_node_id)
|
|
324
|
+
return
|
|
325
|
+
|
|
326
|
+
sl, el, sc, ec = self._pos(node)
|
|
327
|
+
nid = _make_id(self.lang, self.norm_path, name)
|
|
328
|
+
|
|
329
|
+
func_node = Node(
|
|
330
|
+
id=nid,
|
|
331
|
+
kind='function',
|
|
332
|
+
name=name,
|
|
333
|
+
qualified_name=f'{self.norm_path}::{name}',
|
|
334
|
+
file_path=self.norm_path,
|
|
335
|
+
language=self.lang,
|
|
336
|
+
start_line=sl, end_line=el,
|
|
337
|
+
start_column=sc, end_column=ec,
|
|
338
|
+
signature=self.config.get_signature(node, self.source),
|
|
339
|
+
is_async=self.config.is_async(node),
|
|
340
|
+
is_static=self.config.is_static(node),
|
|
341
|
+
is_exported=self.config.is_exported(node, self.source),
|
|
342
|
+
)
|
|
343
|
+
self.nodes.append(func_node)
|
|
344
|
+
self.edges.append(Edge(source=parent_node_id, target=nid, kind='contains'))
|
|
345
|
+
|
|
346
|
+
self._visit_children(node, nid)
|
|
347
|
+
|
|
348
|
+
def _extract_method(self, node: TSNode, parent_node_id: str):
|
|
349
|
+
"""Extract a method inside a class."""
|
|
350
|
+
name = self._node_name(node)
|
|
351
|
+
if not name:
|
|
352
|
+
self._visit_children(node, parent_node_id)
|
|
353
|
+
return
|
|
354
|
+
|
|
355
|
+
sl, el, sc, ec = self._pos(node)
|
|
356
|
+
|
|
357
|
+
# Build qualified name using class name if available
|
|
358
|
+
qname = name
|
|
359
|
+
if self._current_class_id:
|
|
360
|
+
qname = f'{self._current_class_id.split("::")[-1]}.{name}'
|
|
361
|
+
nid = _make_id('method', self.norm_path, qname)
|
|
362
|
+
|
|
363
|
+
method_node = Node(
|
|
364
|
+
id=nid,
|
|
365
|
+
kind='method',
|
|
366
|
+
name=name,
|
|
367
|
+
qualified_name=f'{self.norm_path}::{qname}',
|
|
368
|
+
file_path=self.norm_path,
|
|
369
|
+
language=self.lang,
|
|
370
|
+
start_line=sl, end_line=el,
|
|
371
|
+
start_column=sc, end_column=ec,
|
|
372
|
+
signature=self.config.get_signature(node, self.source),
|
|
373
|
+
is_async=self.config.is_async(node),
|
|
374
|
+
is_static=self.config.is_static(node),
|
|
375
|
+
is_exported=self.config.is_exported(node, self.source),
|
|
376
|
+
)
|
|
377
|
+
self.nodes.append(method_node)
|
|
378
|
+
self.edges.append(Edge(source=parent_node_id, target=nid, kind='contains'))
|
|
379
|
+
|
|
380
|
+
self._visit_children(node, nid)
|
|
381
|
+
|
|
382
|
+
def _extract_class(self, node: TSNode, parent_node_id: str):
|
|
383
|
+
"""Extract a class definition and its body."""
|
|
384
|
+
name = self._node_name(node)
|
|
385
|
+
if not name:
|
|
386
|
+
self._visit_children(node, parent_node_id)
|
|
387
|
+
return
|
|
388
|
+
|
|
389
|
+
sl, el, sc, ec = self._pos(node)
|
|
390
|
+
nid = _make_id('class', self.norm_path, name)
|
|
391
|
+
|
|
392
|
+
class_node = Node(
|
|
393
|
+
id=nid,
|
|
394
|
+
kind='class',
|
|
395
|
+
name=name,
|
|
396
|
+
qualified_name=f'{self.norm_path}::{name}',
|
|
397
|
+
file_path=self.norm_path,
|
|
398
|
+
language=self.lang,
|
|
399
|
+
start_line=sl, end_line=el,
|
|
400
|
+
start_column=sc, end_column=ec,
|
|
401
|
+
)
|
|
402
|
+
self.nodes.append(class_node)
|
|
403
|
+
self.edges.append(Edge(source=parent_node_id, target=nid, kind='contains'))
|
|
404
|
+
|
|
405
|
+
# Visit body with class scope flag
|
|
406
|
+
old_class_id = self._current_class_id
|
|
407
|
+
old_in_class = self._in_class_body
|
|
408
|
+
self._current_class_id = nid
|
|
409
|
+
self._in_class_body = True
|
|
410
|
+
|
|
411
|
+
body = node.child_by_field_name(self.config.body_field)
|
|
412
|
+
if body:
|
|
413
|
+
self._visit_children(body, nid)
|
|
414
|
+
else:
|
|
415
|
+
self._visit_children(node, nid)
|
|
416
|
+
|
|
417
|
+
self._in_class_body = old_in_class
|
|
418
|
+
self._current_class_id = old_class_id
|
|
419
|
+
|
|
420
|
+
def _extract_interface(self, node: TSNode, parent_node_id: str):
|
|
421
|
+
"""Extract an interface definition."""
|
|
422
|
+
name = self._node_name(node)
|
|
423
|
+
if not name:
|
|
424
|
+
self._visit_children(node, parent_node_id)
|
|
425
|
+
return
|
|
426
|
+
|
|
427
|
+
sl, el, sc, ec = self._pos(node)
|
|
428
|
+
nid = _make_id('interface', self.norm_path, name)
|
|
429
|
+
|
|
430
|
+
i_node = Node(
|
|
431
|
+
id=nid, kind='interface', name=name,
|
|
432
|
+
qualified_name=f'{self.norm_path}::{name}',
|
|
433
|
+
file_path=self.norm_path, language=self.lang,
|
|
434
|
+
start_line=sl, end_line=el, start_column=sc, end_column=ec,
|
|
435
|
+
)
|
|
436
|
+
self.nodes.append(i_node)
|
|
437
|
+
self.edges.append(Edge(source=parent_node_id, target=nid, kind='contains'))
|
|
438
|
+
|
|
439
|
+
body = node.child_by_field_name(self.config.body_field)
|
|
440
|
+
if body:
|
|
441
|
+
self._visit_children(body, nid)
|
|
442
|
+
else:
|
|
443
|
+
self._visit_children(node, nid)
|
|
444
|
+
|
|
445
|
+
def _extract_struct(self, node: TSNode, parent_node_id: str):
|
|
446
|
+
"""Extract a struct definition."""
|
|
447
|
+
name = self._node_name(node)
|
|
448
|
+
if not name:
|
|
449
|
+
self._visit_children(node, parent_node_id)
|
|
450
|
+
return
|
|
451
|
+
|
|
452
|
+
sl, el, sc, ec = self._pos(node)
|
|
453
|
+
nid = _make_id('struct', self.norm_path, name)
|
|
454
|
+
|
|
455
|
+
s_node = Node(
|
|
456
|
+
id=nid, kind='struct', name=name,
|
|
457
|
+
qualified_name=f'{self.norm_path}::{name}',
|
|
458
|
+
file_path=self.norm_path, language=self.lang,
|
|
459
|
+
start_line=sl, end_line=el, start_column=sc, end_column=ec,
|
|
460
|
+
)
|
|
461
|
+
self.nodes.append(s_node)
|
|
462
|
+
self.edges.append(Edge(source=parent_node_id, target=nid, kind='contains'))
|
|
463
|
+
|
|
464
|
+
self._visit_children(node, nid)
|
|
465
|
+
|
|
466
|
+
def _extract_enum(self, node: TSNode, parent_node_id: str):
|
|
467
|
+
"""Extract an enum definition."""
|
|
468
|
+
name = self._node_name(node)
|
|
469
|
+
if not name:
|
|
470
|
+
self._visit_children(node, parent_node_id)
|
|
471
|
+
return
|
|
472
|
+
|
|
473
|
+
sl, el, sc, ec = self._pos(node)
|
|
474
|
+
nid = _make_id('enum', self.norm_path, name)
|
|
475
|
+
|
|
476
|
+
e_node = Node(
|
|
477
|
+
id=nid, kind='enum', name=name,
|
|
478
|
+
qualified_name=f'{self.norm_path}::{name}',
|
|
479
|
+
file_path=self.norm_path, language=self.lang,
|
|
480
|
+
start_line=sl, end_line=el, start_column=sc, end_column=ec,
|
|
481
|
+
)
|
|
482
|
+
self.nodes.append(e_node)
|
|
483
|
+
self.edges.append(Edge(source=parent_node_id, target=nid, kind='contains'))
|
|
484
|
+
|
|
485
|
+
self._visit_children(node, nid)
|
|
486
|
+
|
|
487
|
+
def _extract_type_alias(self, node: TSNode, parent_node_id: str):
|
|
488
|
+
"""Extract a type alias."""
|
|
489
|
+
name = self._node_name(node)
|
|
490
|
+
if not name:
|
|
491
|
+
return
|
|
492
|
+
|
|
493
|
+
sl, el, sc, ec = self._pos(node)
|
|
494
|
+
nid = _make_id('type_alias', self.norm_path, name)
|
|
495
|
+
|
|
496
|
+
t_node = Node(
|
|
497
|
+
id=nid, kind='type_alias', name=name,
|
|
498
|
+
qualified_name=f'{self.norm_path}::{name}',
|
|
499
|
+
file_path=self.norm_path, language=self.lang,
|
|
500
|
+
start_line=sl, end_line=el, start_column=sc, end_column=ec,
|
|
501
|
+
)
|
|
502
|
+
self.nodes.append(t_node)
|
|
503
|
+
self.edges.append(Edge(source=parent_node_id, target=nid, kind='contains'))
|
|
504
|
+
|
|
505
|
+
def _extract_import(self, node: TSNode, parent_node_id: str):
|
|
506
|
+
"""Extract an import statement and create import nodes/edges."""
|
|
507
|
+
info = self.config.extract_import(node, self.source)
|
|
508
|
+
if info is None:
|
|
509
|
+
return
|
|
510
|
+
|
|
511
|
+
sl, el, sc, ec = self._pos(node)
|
|
512
|
+
|
|
513
|
+
# Parse import info to create import nodes
|
|
514
|
+
# info.signature contains full import text
|
|
515
|
+
sig = info.signature
|
|
516
|
+
module_name = info.module_name
|
|
517
|
+
|
|
518
|
+
if node.type == 'import_from_statement':
|
|
519
|
+
# e.g. from os.path import join, exists
|
|
520
|
+
# Find the import names via the names field in tree-sitter
|
|
521
|
+
names = []
|
|
522
|
+
names_node = node.child_by_field_name('names')
|
|
523
|
+
if names_node:
|
|
524
|
+
for child in names_node.named_children:
|
|
525
|
+
name_text = _node_text(child)
|
|
526
|
+
if name_text:
|
|
527
|
+
names.append(name_text)
|
|
528
|
+
if not names:
|
|
529
|
+
# Fallback: parse from signature
|
|
530
|
+
if ' import ' in sig:
|
|
531
|
+
parts = sig.split(' import ', 1)
|
|
532
|
+
if len(parts) == 2:
|
|
533
|
+
names = [n.strip() for n in parts[1].split(',')]
|
|
534
|
+
|
|
535
|
+
for imp_name in names:
|
|
536
|
+
imp_node = self._make_import_node(
|
|
537
|
+
imp_name,
|
|
538
|
+
f'{module_name}.{imp_name}',
|
|
539
|
+
sl, el, sc, ec
|
|
540
|
+
)
|
|
541
|
+
self.nodes.append(imp_node)
|
|
542
|
+
self.edges.append(Edge(
|
|
543
|
+
source=parent_node_id, target=imp_node.id, kind='contains'
|
|
544
|
+
))
|
|
545
|
+
# Create imports edge to module
|
|
546
|
+
self.unresolved_refs.append(UnresolvedReference(
|
|
547
|
+
from_node_id=imp_node.id,
|
|
548
|
+
reference_name=module_name,
|
|
549
|
+
reference_kind='imports',
|
|
550
|
+
line=sl,
|
|
551
|
+
column=sc,
|
|
552
|
+
file_path=self.norm_path,
|
|
553
|
+
language=self.lang,
|
|
554
|
+
candidates=None,
|
|
555
|
+
))
|
|
556
|
+
elif node.type == 'import_statement':
|
|
557
|
+
# e.g. import os, sys
|
|
558
|
+
names = node.child_by_field_name('names')
|
|
559
|
+
if names:
|
|
560
|
+
for child in names.named_children:
|
|
561
|
+
name_text = _node_text(child)
|
|
562
|
+
if name_text:
|
|
563
|
+
imp_node = self._make_import_node(
|
|
564
|
+
name_text, name_text,
|
|
565
|
+
sl, el, sc, ec
|
|
566
|
+
)
|
|
567
|
+
self.nodes.append(imp_node)
|
|
568
|
+
self.edges.append(Edge(
|
|
569
|
+
source=parent_node_id, target=imp_node.id, kind='contains'
|
|
570
|
+
))
|
|
571
|
+
self.unresolved_refs.append(UnresolvedReference(
|
|
572
|
+
from_node_id=imp_node.id,
|
|
573
|
+
reference_name=name_text,
|
|
574
|
+
reference_kind='imports',
|
|
575
|
+
line=sl,
|
|
576
|
+
column=sc,
|
|
577
|
+
file_path=self.norm_path,
|
|
578
|
+
language=self.lang,
|
|
579
|
+
candidates=None,
|
|
580
|
+
))
|
|
581
|
+
|
|
582
|
+
def _make_import_node(self, name: str, qualified_name: str,
|
|
583
|
+
start_line: int, end_line: int,
|
|
584
|
+
start_col: int, end_col: int) -> Node:
|
|
585
|
+
"""Create an import node."""
|
|
586
|
+
nid = _make_id('import', self.norm_path, qualified_name)
|
|
587
|
+
return Node(
|
|
588
|
+
id=nid,
|
|
589
|
+
kind='import',
|
|
590
|
+
name=name,
|
|
591
|
+
qualified_name=qualified_name,
|
|
592
|
+
file_path=self.norm_path,
|
|
593
|
+
language=self.lang,
|
|
594
|
+
start_line=start_line,
|
|
595
|
+
end_line=end_line,
|
|
596
|
+
start_column=start_col,
|
|
597
|
+
end_column=end_col,
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
def _extract_call(self, node: TSNode, parent_node_id: str):
|
|
601
|
+
"""Extract a function call (as unresolved reference)."""
|
|
602
|
+
func = node.child_by_field_name('function')
|
|
603
|
+
if func is None:
|
|
604
|
+
return
|
|
605
|
+
|
|
606
|
+
name = _node_text(func)
|
|
607
|
+
if not name:
|
|
608
|
+
return
|
|
609
|
+
|
|
610
|
+
sl, el, sc, ec = self._pos(node)
|
|
611
|
+
self.unresolved_refs.append(UnresolvedReference(
|
|
612
|
+
from_node_id=parent_node_id,
|
|
613
|
+
reference_name=name,
|
|
614
|
+
reference_kind='call',
|
|
615
|
+
line=sl,
|
|
616
|
+
column=sc,
|
|
617
|
+
file_path=self.norm_path,
|
|
618
|
+
language=self.lang,
|
|
619
|
+
))
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
# =============================================================================
|
|
623
|
+
# Convenience: single-file parse function (drop-in for parse_with_treesitter)
|
|
624
|
+
# =============================================================================
|
|
625
|
+
|
|
626
|
+
_singleton_extractor: Optional[TreeSitterExtractor] = None
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def get_ts_extractor() -> TreeSitterExtractor:
|
|
630
|
+
"""Get or create the singleton TreeSitterExtractor."""
|
|
631
|
+
global _singleton_extractor
|
|
632
|
+
if _singleton_extractor is None:
|
|
633
|
+
_singleton_extractor = TreeSitterExtractor()
|
|
634
|
+
return _singleton_extractor
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def parse_file(file_path: str, content: str, language: str) -> ExtractionResult:
|
|
638
|
+
"""
|
|
639
|
+
Parse a single file using tree-sitter.
|
|
640
|
+
|
|
641
|
+
Args:
|
|
642
|
+
file_path: Path (relative to project root, normalized)
|
|
643
|
+
content: File content as string
|
|
644
|
+
language: Language identifier string
|
|
645
|
+
|
|
646
|
+
Returns:
|
|
647
|
+
ExtractionResult
|
|
648
|
+
"""
|
|
649
|
+
from . import detect_language as detect_lang
|
|
650
|
+
|
|
651
|
+
# Map string language to enum
|
|
652
|
+
lang_map = {
|
|
653
|
+
'python': LangEnum.PYTHON,
|
|
654
|
+
'javascript': LangEnum.JAVASCRIPT,
|
|
655
|
+
'jsx': LangEnum.JSX,
|
|
656
|
+
'typescript': LangEnum.TYPESCRIPT,
|
|
657
|
+
'tsx': LangEnum.TSX,
|
|
658
|
+
'arkts': LangEnum.ARKTS,
|
|
659
|
+
'go': LangEnum.GO,
|
|
660
|
+
'rust': LangEnum.RUST,
|
|
661
|
+
'java': LangEnum.JAVA,
|
|
662
|
+
'c': LangEnum.C,
|
|
663
|
+
'cpp': LangEnum.CPP,
|
|
664
|
+
'csharp': LangEnum.CSHARP,
|
|
665
|
+
'php': LangEnum.PHP,
|
|
666
|
+
'ruby': LangEnum.RUBY,
|
|
667
|
+
'swift': LangEnum.SWIFT,
|
|
668
|
+
'kotlin': LangEnum.KOTLIN,
|
|
669
|
+
'dart': LangEnum.DART,
|
|
670
|
+
'scala': LangEnum.SCALA,
|
|
671
|
+
'lua': LangEnum.LUA,
|
|
672
|
+
'luau': LangEnum.LUAU,
|
|
673
|
+
'solidity': LangEnum.SOLIDITY,
|
|
674
|
+
'nix': LangEnum.NIX,
|
|
675
|
+
'terraform': LangEnum.TERRAFORM,
|
|
676
|
+
'pascal': LangEnum.PASCAL,
|
|
677
|
+
'objc': LangEnum.OBJC,
|
|
678
|
+
'r': LangEnum.R,
|
|
679
|
+
'svelte': LangEnum.SVELTE,
|
|
680
|
+
'vue': LangEnum.VUE,
|
|
681
|
+
'astro': LangEnum.ASTRO,
|
|
682
|
+
'liquid': LangEnum.LIQUID,
|
|
683
|
+
'razor': LangEnum.RAZOR,
|
|
684
|
+
'unknown': LangEnum.UNKNOWN,
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
lang_enum = lang_map.get(language, LangEnum.UNKNOWN)
|
|
688
|
+
extractor = get_ts_extractor()
|
|
689
|
+
return extractor.extract(file_path, content.encode('utf-8'), lang_enum)
|