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.
Files changed (41) hide show
  1. codegraph_nav/__init__.py +194 -0
  2. codegraph_nav/ast_grep_analyzer.py +448 -0
  3. codegraph_nav/cli.py +223 -0
  4. codegraph_nav/code_navigator.py +1328 -0
  5. codegraph_nav/code_search.py +1009 -0
  6. codegraph_nav/colors.py +209 -0
  7. codegraph_nav/completions.py +354 -0
  8. codegraph_nav/dart_analyzer.py +301 -0
  9. codegraph_nav/dependency_graph.py +814 -0
  10. codegraph_nav/domain/__init__.py +20 -0
  11. codegraph_nav/domain/routes.py +337 -0
  12. codegraph_nav/domain/schemas.py +229 -0
  13. codegraph_nav/domain/tags.py +87 -0
  14. codegraph_nav/exporters.py +563 -0
  15. codegraph_nav/go_analyzer.py +273 -0
  16. codegraph_nav/graph/__init__.py +72 -0
  17. codegraph_nav/graph/builder.py +409 -0
  18. codegraph_nav/graph/communities.py +402 -0
  19. codegraph_nav/graph/flows.py +311 -0
  20. codegraph_nav/graph/query.py +380 -0
  21. codegraph_nav/graph/schema.py +266 -0
  22. codegraph_nav/graph/search.py +257 -0
  23. codegraph_nav/graph/store.py +517 -0
  24. codegraph_nav/hints.py +195 -0
  25. codegraph_nav/import_resolver.py +891 -0
  26. codegraph_nav/js_ts_analyzer.py +564 -0
  27. codegraph_nav/line_reader.py +664 -0
  28. codegraph_nav/mcp/__init__.py +39 -0
  29. codegraph_nav/mcp/__main__.py +5 -0
  30. codegraph_nav/mcp/server.py +2228 -0
  31. codegraph_nav/py.typed +2 -0
  32. codegraph_nav/ruby_analyzer.py +259 -0
  33. codegraph_nav/rust_analyzer.py +379 -0
  34. codegraph_nav/token_efficient_renderer.py +743 -0
  35. codegraph_nav/watcher.py +382 -0
  36. codegraph_nav-0.1.0.dist-info/METADATA +487 -0
  37. codegraph_nav-0.1.0.dist-info/RECORD +41 -0
  38. codegraph_nav-0.1.0.dist-info/WHEEL +5 -0
  39. codegraph_nav-0.1.0.dist-info/entry_points.txt +4 -0
  40. codegraph_nav-0.1.0.dist-info/licenses/LICENSE +21 -0
  41. 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
+ )