codegraph-nav 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.
- codegraph_nav/__init__.py +194 -0
- codegraph_nav/ast_grep_analyzer.py +448 -0
- codegraph_nav/cli.py +223 -0
- codegraph_nav/code_navigator.py +1328 -0
- codegraph_nav/code_search.py +1009 -0
- codegraph_nav/colors.py +209 -0
- codegraph_nav/completions.py +354 -0
- codegraph_nav/dart_analyzer.py +301 -0
- codegraph_nav/dependency_graph.py +814 -0
- codegraph_nav/domain/__init__.py +20 -0
- codegraph_nav/domain/routes.py +337 -0
- codegraph_nav/domain/schemas.py +229 -0
- codegraph_nav/domain/tags.py +87 -0
- codegraph_nav/exporters.py +563 -0
- codegraph_nav/go_analyzer.py +273 -0
- codegraph_nav/graph/__init__.py +72 -0
- codegraph_nav/graph/builder.py +409 -0
- codegraph_nav/graph/communities.py +402 -0
- codegraph_nav/graph/flows.py +311 -0
- codegraph_nav/graph/query.py +380 -0
- codegraph_nav/graph/schema.py +266 -0
- codegraph_nav/graph/search.py +257 -0
- codegraph_nav/graph/store.py +517 -0
- codegraph_nav/hints.py +195 -0
- codegraph_nav/import_resolver.py +891 -0
- codegraph_nav/js_ts_analyzer.py +564 -0
- codegraph_nav/line_reader.py +664 -0
- codegraph_nav/mcp/__init__.py +39 -0
- codegraph_nav/mcp/__main__.py +5 -0
- codegraph_nav/mcp/server.py +2228 -0
- codegraph_nav/py.typed +2 -0
- codegraph_nav/ruby_analyzer.py +259 -0
- codegraph_nav/rust_analyzer.py +379 -0
- codegraph_nav/token_efficient_renderer.py +743 -0
- codegraph_nav/watcher.py +382 -0
- codegraph_nav-0.1.0.dist-info/METADATA +487 -0
- codegraph_nav-0.1.0.dist-info/RECORD +41 -0
- codegraph_nav-0.1.0.dist-info/WHEEL +5 -0
- codegraph_nav-0.1.0.dist-info/entry_points.txt +4 -0
- codegraph_nav-0.1.0.dist-info/licenses/LICENSE +21 -0
- codegraph_nav-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
"""Dart/Flutter analyzer using tree-sitter for AST-based symbol extraction.
|
|
2
|
+
|
|
3
|
+
Falls back to regex-based GenericAnalyzer when the tree-sitter Dart grammar is
|
|
4
|
+
not installed.
|
|
5
|
+
|
|
6
|
+
The Dart grammar ships via ``tree-sitter-dart``, a pip-installable package that
|
|
7
|
+
provides pre-compiled grammar wheels for Linux, macOS and Windows — no C
|
|
8
|
+
compiler and no manual build step required. It exposes the standard
|
|
9
|
+
``py-tree-sitter`` interface (``Language(tree_sitter_dart.language())``), so this
|
|
10
|
+
analyzer loads and traverses the AST exactly like the Go/JS/TS/Ruby/Rust
|
|
11
|
+
analyzers (``.type``, ``.children``, ``.start_point``). Flutter needs no separate
|
|
12
|
+
grammar: Flutter widgets are ordinary Dart classes covered by this same grammar.
|
|
13
|
+
|
|
14
|
+
Installation:
|
|
15
|
+
To enable AST support, install with the 'dart' extra:
|
|
16
|
+
pip install codegraph-nav[dart]
|
|
17
|
+
|
|
18
|
+
Example:
|
|
19
|
+
>>> from codegraph_nav.dart_analyzer import DartAnalyzer
|
|
20
|
+
>>> source = '''
|
|
21
|
+
... class MyWidget extends StatelessWidget {
|
|
22
|
+
... Widget build(BuildContext context) {
|
|
23
|
+
... return Container();
|
|
24
|
+
... }
|
|
25
|
+
... }
|
|
26
|
+
... '''
|
|
27
|
+
>>> analyzer = DartAnalyzer('my_widget.dart', source)
|
|
28
|
+
>>> symbols = analyzer.analyze()
|
|
29
|
+
>>> print(symbols[0].name)
|
|
30
|
+
MyWidget
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
import sys
|
|
34
|
+
from typing import TYPE_CHECKING
|
|
35
|
+
|
|
36
|
+
from .code_navigator import GenericAnalyzer, Symbol
|
|
37
|
+
|
|
38
|
+
if TYPE_CHECKING:
|
|
39
|
+
from tree_sitter import Node
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
import tree_sitter_dart
|
|
43
|
+
from tree_sitter import Language, Parser
|
|
44
|
+
|
|
45
|
+
_DART_LANGUAGE = Language(tree_sitter_dart.language())
|
|
46
|
+
TREE_SITTER_AVAILABLE = True
|
|
47
|
+
except ImportError:
|
|
48
|
+
TREE_SITTER_AVAILABLE = False
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class DartAnalyzer:
|
|
52
|
+
"""Analyzes Dart/Flutter files using tree-sitter for accurate symbol extraction.
|
|
53
|
+
|
|
54
|
+
When the tree-sitter Dart grammar is unavailable, automatically falls back
|
|
55
|
+
to regex-based GenericAnalyzer.
|
|
56
|
+
|
|
57
|
+
Attributes:
|
|
58
|
+
file_path: Path to the file being analyzed.
|
|
59
|
+
source: Source code content.
|
|
60
|
+
symbols: Extracted symbols.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(self, file_path: str, source: str):
|
|
64
|
+
self.file_path = file_path
|
|
65
|
+
self.source = source
|
|
66
|
+
# tree-sitter offsets are UTF-8 byte offsets — slice an encoded view.
|
|
67
|
+
self.source_bytes = source.encode("utf-8")
|
|
68
|
+
self.lines = source.split("\n")
|
|
69
|
+
self.symbols: list[Symbol] = []
|
|
70
|
+
self._current_class: str | None = None
|
|
71
|
+
|
|
72
|
+
def analyze(self) -> list[Symbol]:
|
|
73
|
+
"""Parse and analyze the file.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
List of Symbol objects found in the file.
|
|
77
|
+
"""
|
|
78
|
+
if not TREE_SITTER_AVAILABLE:
|
|
79
|
+
return GenericAnalyzer(self.file_path, self.source, "dart").analyze()
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
parser = Parser(_DART_LANGUAGE)
|
|
83
|
+
tree = parser.parse(bytes(self.source, "utf-8"))
|
|
84
|
+
self._visit_node(tree.root_node)
|
|
85
|
+
except Exception as e:
|
|
86
|
+
print(f"tree-sitter error in {self.file_path}: {e}", file=sys.stderr)
|
|
87
|
+
return GenericAnalyzer(self.file_path, self.source, "dart").analyze()
|
|
88
|
+
|
|
89
|
+
return self.symbols
|
|
90
|
+
|
|
91
|
+
def _visit_node(self, node: "Node") -> None:
|
|
92
|
+
"""Recursively visit AST nodes and extract symbols."""
|
|
93
|
+
nt = node.type
|
|
94
|
+
|
|
95
|
+
if nt == "class_definition":
|
|
96
|
+
self._extract_class(node)
|
|
97
|
+
return
|
|
98
|
+
elif nt == "mixin_declaration":
|
|
99
|
+
self._extract_mixin(node)
|
|
100
|
+
return
|
|
101
|
+
elif nt == "enum_declaration":
|
|
102
|
+
self._extract_enum(node)
|
|
103
|
+
elif nt == "extension_declaration":
|
|
104
|
+
self._extract_extension(node)
|
|
105
|
+
return
|
|
106
|
+
elif nt == "function_signature":
|
|
107
|
+
if self._current_class is None:
|
|
108
|
+
self._extract_function(node)
|
|
109
|
+
elif nt == "method_signature":
|
|
110
|
+
self._extract_method(node)
|
|
111
|
+
elif nt in ("constant_constructor_signature", "constructor_signature"):
|
|
112
|
+
self._extract_constructor(node)
|
|
113
|
+
else:
|
|
114
|
+
for child in node.children:
|
|
115
|
+
self._visit_node(child)
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
for child in node.children:
|
|
119
|
+
self._visit_node(child)
|
|
120
|
+
|
|
121
|
+
def _get_text(self, node: "Node") -> str:
|
|
122
|
+
return self.source_bytes[node.start_byte : node.end_byte].decode("utf-8", "replace")
|
|
123
|
+
|
|
124
|
+
def _child_by_type(self, node: "Node", *types: str) -> "Node | None":
|
|
125
|
+
for child in node.children:
|
|
126
|
+
if child.type in types:
|
|
127
|
+
return child
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
def _extract_class(self, node: "Node") -> None:
|
|
131
|
+
name_node = self._child_by_type(node, "identifier")
|
|
132
|
+
if not name_node:
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
name = self._get_text(name_node)
|
|
136
|
+
is_abstract = any(c.type == "abstract" for c in node.children)
|
|
137
|
+
prefix = "abstract " if is_abstract else ""
|
|
138
|
+
|
|
139
|
+
sig = f"{prefix}class {name}"
|
|
140
|
+
supertype = self._child_by_type(node, "superclass", "supertype")
|
|
141
|
+
if supertype:
|
|
142
|
+
sig += f" {self._get_text(supertype)}"
|
|
143
|
+
|
|
144
|
+
self.symbols.append(
|
|
145
|
+
Symbol(
|
|
146
|
+
name=name,
|
|
147
|
+
type="class",
|
|
148
|
+
file_path=self.file_path,
|
|
149
|
+
line_start=node.start_point[0] + 1,
|
|
150
|
+
line_end=node.end_point[0] + 1,
|
|
151
|
+
signature=sig[:100],
|
|
152
|
+
)
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
old = self._current_class
|
|
156
|
+
self._current_class = name
|
|
157
|
+
body = self._child_by_type(node, "class_body")
|
|
158
|
+
if body:
|
|
159
|
+
for child in body.children:
|
|
160
|
+
self._visit_node(child)
|
|
161
|
+
self._current_class = old
|
|
162
|
+
|
|
163
|
+
def _extract_mixin(self, node: "Node") -> None:
|
|
164
|
+
name_node = self._child_by_type(node, "identifier")
|
|
165
|
+
if not name_node:
|
|
166
|
+
return
|
|
167
|
+
|
|
168
|
+
name = self._get_text(name_node)
|
|
169
|
+
self.symbols.append(
|
|
170
|
+
Symbol(
|
|
171
|
+
name=name,
|
|
172
|
+
type="mixin",
|
|
173
|
+
file_path=self.file_path,
|
|
174
|
+
line_start=node.start_point[0] + 1,
|
|
175
|
+
line_end=node.end_point[0] + 1,
|
|
176
|
+
signature=f"mixin {name}",
|
|
177
|
+
)
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
old = self._current_class
|
|
181
|
+
self._current_class = name
|
|
182
|
+
body = self._child_by_type(node, "class_body")
|
|
183
|
+
if body:
|
|
184
|
+
for child in body.children:
|
|
185
|
+
self._visit_node(child)
|
|
186
|
+
self._current_class = old
|
|
187
|
+
|
|
188
|
+
def _extract_enum(self, node: "Node") -> None:
|
|
189
|
+
name_node = self._child_by_type(node, "identifier")
|
|
190
|
+
if not name_node:
|
|
191
|
+
return
|
|
192
|
+
|
|
193
|
+
name = self._get_text(name_node)
|
|
194
|
+
self.symbols.append(
|
|
195
|
+
Symbol(
|
|
196
|
+
name=name,
|
|
197
|
+
type="enum",
|
|
198
|
+
file_path=self.file_path,
|
|
199
|
+
line_start=node.start_point[0] + 1,
|
|
200
|
+
line_end=node.end_point[0] + 1,
|
|
201
|
+
signature=f"enum {name}",
|
|
202
|
+
)
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
def _extract_extension(self, node: "Node") -> None:
|
|
206
|
+
name_node = self._child_by_type(node, "identifier")
|
|
207
|
+
name = self._get_text(name_node) if name_node else "<anonymous>"
|
|
208
|
+
|
|
209
|
+
on_type = ""
|
|
210
|
+
for i, child in enumerate(node.children):
|
|
211
|
+
if child.type == "on":
|
|
212
|
+
rest = node.children[i + 1 :]
|
|
213
|
+
if rest:
|
|
214
|
+
on_type = f" on {self._get_text(rest[0])}"
|
|
215
|
+
break
|
|
216
|
+
|
|
217
|
+
self.symbols.append(
|
|
218
|
+
Symbol(
|
|
219
|
+
name=name,
|
|
220
|
+
type="extension",
|
|
221
|
+
file_path=self.file_path,
|
|
222
|
+
line_start=node.start_point[0] + 1,
|
|
223
|
+
line_end=node.end_point[0] + 1,
|
|
224
|
+
signature=f"extension {name}{on_type}"[:100],
|
|
225
|
+
)
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
old = self._current_class
|
|
229
|
+
self._current_class = name
|
|
230
|
+
body = self._child_by_type(node, "class_body")
|
|
231
|
+
if body:
|
|
232
|
+
for child in body.children:
|
|
233
|
+
self._visit_node(child)
|
|
234
|
+
self._current_class = old
|
|
235
|
+
|
|
236
|
+
def _extract_function(self, node: "Node") -> None:
|
|
237
|
+
name_node = self._child_by_type(node, "identifier")
|
|
238
|
+
if not name_node:
|
|
239
|
+
return
|
|
240
|
+
|
|
241
|
+
name = self._get_text(name_node)
|
|
242
|
+
ret_type = ""
|
|
243
|
+
for child in node.children:
|
|
244
|
+
if child.type in ("type_identifier", "void_type", "nullable_type"):
|
|
245
|
+
ret_type = self._get_text(child) + " "
|
|
246
|
+
break
|
|
247
|
+
|
|
248
|
+
self.symbols.append(
|
|
249
|
+
Symbol(
|
|
250
|
+
name=name,
|
|
251
|
+
type="function",
|
|
252
|
+
file_path=self.file_path,
|
|
253
|
+
line_start=node.start_point[0] + 1,
|
|
254
|
+
line_end=node.end_point[0] + 1,
|
|
255
|
+
signature=f"{ret_type}{name}()"[:100],
|
|
256
|
+
)
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
def _extract_method(self, node: "Node") -> None:
|
|
260
|
+
func_sig = self._child_by_type(node, "function_signature")
|
|
261
|
+
target = func_sig if func_sig else node
|
|
262
|
+
name_node = self._child_by_type(target, "identifier")
|
|
263
|
+
if not name_node:
|
|
264
|
+
return
|
|
265
|
+
|
|
266
|
+
name = self._get_text(name_node)
|
|
267
|
+
ret_type = ""
|
|
268
|
+
for child in target.children:
|
|
269
|
+
if child.type in ("type_identifier", "void_type", "nullable_type"):
|
|
270
|
+
ret_type = self._get_text(child) + " "
|
|
271
|
+
break
|
|
272
|
+
|
|
273
|
+
self.symbols.append(
|
|
274
|
+
Symbol(
|
|
275
|
+
name=name,
|
|
276
|
+
type="method",
|
|
277
|
+
file_path=self.file_path,
|
|
278
|
+
line_start=node.start_point[0] + 1,
|
|
279
|
+
line_end=node.end_point[0] + 1,
|
|
280
|
+
signature=f"{ret_type}{name}()"[:100],
|
|
281
|
+
parent=self._current_class,
|
|
282
|
+
)
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
def _extract_constructor(self, node: "Node") -> None:
|
|
286
|
+
identifiers = [c for c in node.children if c.type == "identifier"]
|
|
287
|
+
if not identifiers:
|
|
288
|
+
return
|
|
289
|
+
|
|
290
|
+
name = self._get_text(identifiers[0])
|
|
291
|
+
self.symbols.append(
|
|
292
|
+
Symbol(
|
|
293
|
+
name=name,
|
|
294
|
+
type="constructor",
|
|
295
|
+
file_path=self.file_path,
|
|
296
|
+
line_start=node.start_point[0] + 1,
|
|
297
|
+
line_end=node.end_point[0] + 1,
|
|
298
|
+
signature=f"{name}()",
|
|
299
|
+
parent=self._current_class,
|
|
300
|
+
)
|
|
301
|
+
)
|