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,564 @@
1
+ """JavaScript and TypeScript analyzers using tree-sitter for AST-based symbol extraction.
2
+
3
+ This module provides accurate symbol detection for JavaScript and TypeScript files
4
+ using tree-sitter for parsing. Falls back to regex-based GenericAnalyzer when
5
+ tree-sitter is not installed.
6
+
7
+ Example:
8
+ >>> from codegraph_nav.js_ts_analyzer import JavaScriptAnalyzer
9
+ >>> source = '''
10
+ ... function greet(name) {
11
+ ... return `Hello, ${name}!`;
12
+ ... }
13
+ ... '''
14
+ >>> analyzer = JavaScriptAnalyzer('example.js', source)
15
+ >>> symbols = analyzer.analyze()
16
+ >>> print(symbols[0].name)
17
+ 'greet'
18
+
19
+ Installation:
20
+ To enable AST support, install with the 'ast' extra:
21
+ pip install codegraph-nav[ast]
22
+ """
23
+
24
+ import sys
25
+ from typing import TYPE_CHECKING
26
+
27
+ if TYPE_CHECKING:
28
+ from tree_sitter import Node
29
+
30
+ # Try to import tree-sitter
31
+ try:
32
+ import tree_sitter_javascript as ts_javascript
33
+ import tree_sitter_typescript as ts_typescript
34
+ from tree_sitter import Language, Parser
35
+
36
+ TREE_SITTER_AVAILABLE = True
37
+ except ImportError:
38
+ TREE_SITTER_AVAILABLE = False
39
+
40
+ from .code_navigator import GenericAnalyzer, Symbol
41
+
42
+
43
+ class JavaScriptAnalyzer:
44
+ """Analyzes JavaScript/JSX files using tree-sitter for accurate symbol extraction.
45
+
46
+ When tree-sitter is not available, automatically falls back to regex-based
47
+ GenericAnalyzer.
48
+
49
+ Attributes:
50
+ file_path: Path to the file being analyzed.
51
+ source: Source code content.
52
+ is_jsx: Whether to parse as JSX.
53
+ symbols: Extracted symbols.
54
+
55
+ Example:
56
+ >>> source = '''
57
+ ... const add = (a, b) => a + b;
58
+ ...
59
+ ... class Calculator {
60
+ ... multiply(x, y) {
61
+ ... return x * y;
62
+ ... }
63
+ ... }
64
+ ... '''
65
+ >>> analyzer = JavaScriptAnalyzer('calc.js', source)
66
+ >>> symbols = analyzer.analyze()
67
+ >>> print([s.name for s in symbols])
68
+ ['add', 'Calculator', 'multiply']
69
+ """
70
+
71
+ def __init__(self, file_path: str, source: str, is_jsx: bool = False):
72
+ """Initialize the JavaScript analyzer.
73
+
74
+ Args:
75
+ file_path: Relative path to the file.
76
+ source: Source code content.
77
+ is_jsx: Whether to parse as JSX (default: False).
78
+ """
79
+ self.file_path = file_path
80
+ self.source = source
81
+ self.is_jsx = is_jsx
82
+ self.lines = source.split("\n")
83
+ self.symbols: list[Symbol] = []
84
+ self._current_class: str | None = None
85
+
86
+ def analyze(self) -> list[Symbol]:
87
+ """Parse and analyze the file.
88
+
89
+ Returns:
90
+ List of Symbol objects found in the file.
91
+ """
92
+ if not TREE_SITTER_AVAILABLE:
93
+ # Fallback to regex-based analyzer
94
+ fallback = GenericAnalyzer(self.file_path, self.source, "javascript")
95
+ return fallback.analyze()
96
+
97
+ try:
98
+ parser = Parser(Language(ts_javascript.language()))
99
+ tree = parser.parse(bytes(self.source, "utf-8"))
100
+ self._visit_node(tree.root_node)
101
+ except Exception as e:
102
+ print(f"tree-sitter error in {self.file_path}: {e}", file=sys.stderr)
103
+ # Fallback to regex on error
104
+ fallback = GenericAnalyzer(self.file_path, self.source, "javascript")
105
+ return fallback.analyze()
106
+
107
+ return self.symbols
108
+
109
+ def _visit_node(self, node: "Node") -> None:
110
+ """Recursively visit AST nodes and extract symbols.
111
+
112
+ Args:
113
+ node: A tree-sitter AST node.
114
+ """
115
+ node_type = node.type
116
+
117
+ if node_type == "function_declaration":
118
+ self._extract_function(node)
119
+ elif node_type in ("class_declaration", "abstract_class_declaration"):
120
+ self._extract_class(node)
121
+ elif node_type == "method_definition":
122
+ self._extract_method(node)
123
+ elif node_type == "variable_declaration":
124
+ self._extract_variable_declaration(node)
125
+ elif node_type == "lexical_declaration":
126
+ self._extract_variable_declaration(node)
127
+ elif node_type == "export_statement":
128
+ # Process the exported item
129
+ for child in node.children:
130
+ self._visit_node(child)
131
+ return # Don't visit children again
132
+
133
+ # Recursively visit children
134
+ for child in node.children:
135
+ self._visit_node(child)
136
+
137
+ def _get_node_text(self, node: "Node") -> str:
138
+ """Get the text content of a node.
139
+
140
+ Args:
141
+ node: A tree-sitter AST node.
142
+
143
+ Returns:
144
+ The text content of the node.
145
+ """
146
+ return self.source[node.start_byte : node.end_byte]
147
+
148
+ def _get_identifier_name(self, node: "Node") -> str | None:
149
+ """Get the identifier name from a node.
150
+
151
+ Args:
152
+ node: A tree-sitter AST node.
153
+
154
+ Returns:
155
+ The identifier name, or None if not found.
156
+ """
157
+ # JavaScript names are `identifier`; TypeScript class/interface names
158
+ # are `type_identifier`. Accept both so TS class detection works.
159
+ for child in node.children:
160
+ if child.type in ("identifier", "type_identifier"):
161
+ return self._get_node_text(child)
162
+ return None
163
+
164
+ def _extract_function(self, node: "Node") -> None:
165
+ """Extract a function declaration.
166
+
167
+ Args:
168
+ node: A function_declaration node.
169
+ """
170
+ name = self._get_identifier_name(node)
171
+ if not name:
172
+ return
173
+
174
+ # Check if async
175
+ is_async = any(child.type == "async" for child in node.children)
176
+
177
+ # Get parameters
178
+ params = ""
179
+ for child in node.children:
180
+ if child.type == "formal_parameters":
181
+ params = self._get_node_text(child)
182
+ break
183
+
184
+ prefix = "async " if is_async else ""
185
+ signature = f"{prefix}function {name}{params}"
186
+
187
+ self.symbols.append(
188
+ Symbol(
189
+ name=name,
190
+ type="function",
191
+ file_path=self.file_path,
192
+ line_start=node.start_point[0] + 1,
193
+ line_end=node.end_point[0] + 1,
194
+ signature=signature[:100],
195
+ )
196
+ )
197
+
198
+ def _extract_class(self, node: "Node") -> None:
199
+ """Extract a class declaration and its methods.
200
+
201
+ Args:
202
+ node: A class_declaration node.
203
+ """
204
+ name = self._get_identifier_name(node)
205
+ if not name:
206
+ return
207
+
208
+ # Get heritage (extends)
209
+ heritage = ""
210
+ for child in node.children:
211
+ if child.type == "class_heritage":
212
+ heritage_text = self._get_node_text(child)
213
+ heritage = f" {heritage_text}"
214
+ break
215
+
216
+ signature = f"class {name}{heritage}"
217
+
218
+ self.symbols.append(
219
+ Symbol(
220
+ name=name,
221
+ type="class",
222
+ file_path=self.file_path,
223
+ line_start=node.start_point[0] + 1,
224
+ line_end=node.end_point[0] + 1,
225
+ signature=signature[:100],
226
+ )
227
+ )
228
+
229
+ # Visit class body for methods
230
+ old_class = self._current_class
231
+ self._current_class = name
232
+ for child in node.children:
233
+ if child.type == "class_body":
234
+ for member in child.children:
235
+ self._visit_node(member)
236
+ self._current_class = old_class
237
+
238
+ def _extract_method(self, node: "Node") -> None:
239
+ """Extract a method definition.
240
+
241
+ Args:
242
+ node: A method_definition node.
243
+ """
244
+ # Get method name
245
+ name = None
246
+ for child in node.children:
247
+ if child.type == "property_identifier":
248
+ name = self._get_node_text(child)
249
+ break
250
+
251
+ if not name:
252
+ return
253
+
254
+ # Check for async/static
255
+ is_async = any(child.type == "async" for child in node.children)
256
+ is_static = any(child.type == "static" for child in node.children)
257
+
258
+ # Get parameters
259
+ params = ""
260
+ for child in node.children:
261
+ if child.type == "formal_parameters":
262
+ params = self._get_node_text(child)
263
+ break
264
+
265
+ prefix = ""
266
+ if is_static:
267
+ prefix += "static "
268
+ if is_async:
269
+ prefix += "async "
270
+
271
+ signature = f"{prefix}{name}{params}"
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=signature[:100],
281
+ parent=self._current_class,
282
+ )
283
+ )
284
+
285
+ def _extract_variable_declaration(self, node: "Node") -> None:
286
+ """Extract arrow functions and function expressions from variable declarations.
287
+
288
+ Args:
289
+ node: A variable_declaration or lexical_declaration node.
290
+ """
291
+ for child in node.children:
292
+ if child.type == "variable_declarator":
293
+ self._extract_variable_declarator(child)
294
+
295
+ def _extract_variable_declarator(self, node: "Node") -> None:
296
+ """Extract a variable declarator that may contain an arrow function.
297
+
298
+ Args:
299
+ node: A variable_declarator node.
300
+ """
301
+ name = None
302
+ value = None
303
+
304
+ for child in node.children:
305
+ if child.type == "identifier":
306
+ name = self._get_node_text(child)
307
+ elif child.type in ("arrow_function", "function_expression"):
308
+ value = child
309
+
310
+ if not name or not value:
311
+ return
312
+
313
+ # Check if it's an arrow function or function expression
314
+ is_async = any(c.type == "async" for c in value.children)
315
+
316
+ # Get parameters
317
+ params = ""
318
+ for child in value.children:
319
+ if child.type == "formal_parameters":
320
+ params = self._get_node_text(child)
321
+ break
322
+ elif child.type == "identifier":
323
+ # Single param arrow function: x => x + 1
324
+ params = f"({self._get_node_text(child)})"
325
+ break
326
+
327
+ prefix = "async " if is_async else ""
328
+ signature = f"const {name} = {prefix}{params} =>"
329
+
330
+ self.symbols.append(
331
+ Symbol(
332
+ name=name,
333
+ type="function",
334
+ file_path=self.file_path,
335
+ line_start=node.start_point[0] + 1,
336
+ line_end=node.end_point[0] + 1,
337
+ signature=signature[:100],
338
+ )
339
+ )
340
+
341
+
342
+ class TypeScriptAnalyzer(JavaScriptAnalyzer):
343
+ """Analyzes TypeScript/TSX files using tree-sitter for accurate symbol extraction.
344
+
345
+ Extends JavaScriptAnalyzer with TypeScript-specific constructs like interfaces,
346
+ type aliases, and enums.
347
+
348
+ Attributes:
349
+ file_path: Path to the file being analyzed.
350
+ source: Source code content.
351
+ is_tsx: Whether to parse as TSX.
352
+ symbols: Extracted symbols.
353
+
354
+ Example:
355
+ >>> source = '''
356
+ ... interface User {
357
+ ... name: string;
358
+ ... age: number;
359
+ ... }
360
+ ...
361
+ ... type Status = 'active' | 'inactive';
362
+ ...
363
+ ... enum Color {
364
+ ... Red,
365
+ ... Green,
366
+ ... Blue
367
+ ... }
368
+ ... '''
369
+ >>> analyzer = TypeScriptAnalyzer('types.ts', source)
370
+ >>> symbols = analyzer.analyze()
371
+ >>> print([s.name for s in symbols])
372
+ ['User', 'Status', 'Color']
373
+ """
374
+
375
+ def __init__(self, file_path: str, source: str, is_tsx: bool = False):
376
+ """Initialize the TypeScript analyzer.
377
+
378
+ Args:
379
+ file_path: Relative path to the file.
380
+ source: Source code content.
381
+ is_tsx: Whether to parse as TSX (default: False).
382
+ """
383
+ super().__init__(file_path, source, is_jsx=is_tsx)
384
+ self.is_tsx = is_tsx
385
+
386
+ def analyze(self) -> list[Symbol]:
387
+ """Parse and analyze the file.
388
+
389
+ Returns:
390
+ List of Symbol objects found in the file.
391
+ """
392
+ if not TREE_SITTER_AVAILABLE:
393
+ # Fallback to regex-based analyzer
394
+ fallback = GenericAnalyzer(self.file_path, self.source, "typescript")
395
+ return fallback.analyze()
396
+
397
+ try:
398
+ # Use TSX parser for .tsx files, otherwise use regular TypeScript
399
+ if self.is_tsx:
400
+ language = Language(ts_typescript.language_tsx())
401
+ else:
402
+ language = Language(ts_typescript.language_typescript())
403
+
404
+ parser = Parser(language)
405
+ tree = parser.parse(bytes(self.source, "utf-8"))
406
+ self._visit_node(tree.root_node)
407
+ except Exception as e:
408
+ print(f"tree-sitter error in {self.file_path}: {e}", file=sys.stderr)
409
+ # Fallback to regex on error
410
+ fallback = GenericAnalyzer(self.file_path, self.source, "typescript")
411
+ return fallback.analyze()
412
+
413
+ return self.symbols
414
+
415
+ def _visit_node(self, node: "Node") -> None:
416
+ """Recursively visit AST nodes and extract symbols.
417
+
418
+ Extends parent class to handle TypeScript-specific nodes.
419
+
420
+ Args:
421
+ node: A tree-sitter AST node.
422
+ """
423
+ node_type = node.type
424
+
425
+ # TypeScript-specific node types
426
+ if node_type == "interface_declaration":
427
+ self._extract_interface(node)
428
+ elif node_type == "type_alias_declaration":
429
+ self._extract_type_alias(node)
430
+ elif node_type == "enum_declaration":
431
+ self._extract_enum(node)
432
+ elif node_type == "ambient_declaration":
433
+ # Process declare statements
434
+ for child in node.children:
435
+ self._visit_node(child)
436
+ return
437
+ else:
438
+ # Let parent class handle common JS constructs
439
+ super()._visit_node(node)
440
+ return
441
+
442
+ # Recursively visit children for TS-specific nodes
443
+ for child in node.children:
444
+ self._visit_node(child)
445
+
446
+ def _extract_interface(self, node: "Node") -> None:
447
+ """Extract an interface declaration.
448
+
449
+ Args:
450
+ node: An interface_declaration node.
451
+ """
452
+ name = None
453
+ for child in node.children:
454
+ if child.type == "type_identifier":
455
+ name = self._get_node_text(child)
456
+ break
457
+
458
+ if not name:
459
+ return
460
+
461
+ # Get extends clause if any
462
+ extends = ""
463
+ for child in node.children:
464
+ if child.type == "extends_type_clause":
465
+ extends_text = self._get_node_text(child)
466
+ extends = f" {extends_text}"
467
+ break
468
+
469
+ # Get type parameters if any
470
+ type_params = ""
471
+ for child in node.children:
472
+ if child.type == "type_parameters":
473
+ type_params = self._get_node_text(child)
474
+ break
475
+
476
+ signature = f"interface {name}{type_params}{extends}"
477
+
478
+ self.symbols.append(
479
+ Symbol(
480
+ name=name,
481
+ type="interface",
482
+ file_path=self.file_path,
483
+ line_start=node.start_point[0] + 1,
484
+ line_end=node.end_point[0] + 1,
485
+ signature=signature[:100],
486
+ )
487
+ )
488
+
489
+ def _extract_type_alias(self, node: "Node") -> None:
490
+ """Extract a type alias declaration.
491
+
492
+ Args:
493
+ node: A type_alias_declaration node.
494
+ """
495
+ name = None
496
+ for child in node.children:
497
+ if child.type == "type_identifier":
498
+ name = self._get_node_text(child)
499
+ break
500
+
501
+ if not name:
502
+ return
503
+
504
+ # Get type parameters if any
505
+ type_params = ""
506
+ for child in node.children:
507
+ if child.type == "type_parameters":
508
+ type_params = self._get_node_text(child)
509
+ break
510
+
511
+ # Get the type value (simplified, first 50 chars)
512
+ type_value = ""
513
+ for i, child in enumerate(node.children):
514
+ if child.type == "=":
515
+ # Next child should be the type
516
+ remaining = node.children[i + 1 :]
517
+ if remaining:
518
+ type_value = self._get_node_text(remaining[0])[:50]
519
+ break
520
+
521
+ signature = f"type {name}{type_params} = {type_value}"
522
+
523
+ self.symbols.append(
524
+ Symbol(
525
+ name=name,
526
+ type="type",
527
+ file_path=self.file_path,
528
+ line_start=node.start_point[0] + 1,
529
+ line_end=node.end_point[0] + 1,
530
+ signature=signature[:100],
531
+ )
532
+ )
533
+
534
+ def _extract_enum(self, node: "Node") -> None:
535
+ """Extract an enum declaration.
536
+
537
+ Args:
538
+ node: An enum_declaration node.
539
+ """
540
+ name = None
541
+ for child in node.children:
542
+ if child.type == "identifier":
543
+ name = self._get_node_text(child)
544
+ break
545
+
546
+ if not name:
547
+ return
548
+
549
+ # Check for const enum
550
+ is_const = any(child.type == "const" for child in node.children)
551
+
552
+ prefix = "const " if is_const else ""
553
+ signature = f"{prefix}enum {name}"
554
+
555
+ self.symbols.append(
556
+ Symbol(
557
+ name=name,
558
+ type="enum",
559
+ file_path=self.file_path,
560
+ line_start=node.start_point[0] + 1,
561
+ line_end=node.end_point[0] + 1,
562
+ signature=signature[:100],
563
+ )
564
+ )