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.
Files changed (44) hide show
  1. codegraph/__init__.py +12 -0
  2. codegraph/__main__.py +6 -0
  3. codegraph/cli.py +911 -0
  4. codegraph/codegraph.py +618 -0
  5. codegraph/context/__init__.py +216 -0
  6. codegraph/db/__init__.py +11 -0
  7. codegraph/db/connection.py +163 -0
  8. codegraph/db/queries.py +752 -0
  9. codegraph/db/schema.sql +151 -0
  10. codegraph/directory.py +160 -0
  11. codegraph/errors.py +101 -0
  12. codegraph/extraction/__init__.py +956 -0
  13. codegraph/extraction/languages/__init__.py +64 -0
  14. codegraph/extraction/languages/base.py +132 -0
  15. codegraph/extraction/languages/c_cfg.py +53 -0
  16. codegraph/extraction/languages/cpp_cfg.py +60 -0
  17. codegraph/extraction/languages/dart_cfg.py +53 -0
  18. codegraph/extraction/languages/go_cfg.py +70 -0
  19. codegraph/extraction/languages/java_cfg.py +62 -0
  20. codegraph/extraction/languages/javascript_cfg.py +84 -0
  21. codegraph/extraction/languages/kotlin_cfg.py +52 -0
  22. codegraph/extraction/languages/lua_cfg.py +65 -0
  23. codegraph/extraction/languages/python_cfg.py +75 -0
  24. codegraph/extraction/languages/ruby_cfg.py +48 -0
  25. codegraph/extraction/languages/rust_cfg.py +68 -0
  26. codegraph/extraction/languages/scala_cfg.py +59 -0
  27. codegraph/extraction/languages/swift_cfg.py +64 -0
  28. codegraph/extraction/languages/typescript_cfg.py +89 -0
  29. codegraph/extraction/tree_sitter_extractor.py +689 -0
  30. codegraph/graph/__init__.py +685 -0
  31. codegraph/installer/__init__.py +6 -0
  32. codegraph/mcp/__init__.py +668 -0
  33. codegraph/project_config.py +191 -0
  34. codegraph/resolution/__init__.py +337 -0
  35. codegraph/search/__init__.py +653 -0
  36. codegraph/sync/__init__.py +204 -0
  37. codegraph/types.py +334 -0
  38. codegraph/ui/__init__.py +11 -0
  39. codegraph/utils.py +218 -0
  40. codegraph_py-1.0.0.dist-info/METADATA +238 -0
  41. codegraph_py-1.0.0.dist-info/RECORD +44 -0
  42. codegraph_py-1.0.0.dist-info/WHEEL +5 -0
  43. codegraph_py-1.0.0.dist-info/entry_points.txt +2 -0
  44. codegraph_py-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,685 @@
1
+ """
2
+ CodeGraph Graph Traversal Module
3
+
4
+ Provides graph traversal, path finding, and dependency analysis capabilities.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import sqlite3
10
+ from collections import deque
11
+ from dataclasses import dataclass, field
12
+ from typing import Any, Dict, List, Optional, Set, Tuple
13
+
14
+ from codegraph.db.queries import QueryBuilder
15
+ from codegraph.types import Node, Edge, EdgeKind, NodeKind
16
+
17
+
18
+ @dataclass
19
+ class TraversalResult:
20
+ """Result of a graph traversal."""
21
+ nodes: List[Node] = field(default_factory=list)
22
+ edges: List[Edge] = field(default_factory=list)
23
+ depths: Dict[str, int] = field(default_factory=dict)
24
+ paths: Dict[str, List[str]] = field(default_factory=dict)
25
+
26
+
27
+ @dataclass
28
+ class PathResult:
29
+ """Result of a path finding operation."""
30
+ found: bool
31
+ path: List[str] = field(default_factory=list)
32
+ nodes: List[Node] = field(default_factory=list)
33
+ edges: List[Edge] = field(default_factory=list)
34
+ length: int = 0
35
+
36
+
37
+ @dataclass
38
+ class DependencyInfo:
39
+ """Information about a dependency relationship."""
40
+ source: str
41
+ target: str
42
+ kind: str
43
+ file_path: Optional[str] = None
44
+ line: Optional[int] = None
45
+
46
+
47
+ @dataclass
48
+ class NodeMetrics:
49
+ """Metrics for a node."""
50
+ node_id: str
51
+ incoming_edges: int = 0
52
+ outgoing_edges: int = 0
53
+ call_depth: int = 0
54
+ fan_out: int = 0
55
+ fan_in: int = 0
56
+
57
+
58
+ class GraphTraverser:
59
+ """Graph traversal and path finding capabilities."""
60
+
61
+ def __init__(self, db: sqlite3.Connection, db_path: str = ''):
62
+ self._db = db
63
+ self._qb = QueryBuilder(db, db_path)
64
+
65
+ def bfs_traverse(
66
+ self,
67
+ start_id: str,
68
+ max_depth: Optional[int] = None,
69
+ edge_kinds: Optional[List[str]] = None,
70
+ direction: str = 'outgoing',
71
+ ) -> TraversalResult:
72
+ """Breadth-first traversal from a starting node.
73
+
74
+ Args:
75
+ start_id: Starting node ID
76
+ max_depth: Maximum traversal depth (None for unlimited)
77
+ edge_kinds: Filter by edge kinds
78
+ direction: 'outgoing', 'incoming', or 'both'
79
+
80
+ Returns:
81
+ TraversalResult with visited nodes and edges
82
+ """
83
+ result = TraversalResult()
84
+ visited: Set[str] = set()
85
+ queue: deque[Tuple[str, int]] = deque([(start_id, 0)])
86
+ visited.add(start_id)
87
+ result.depths[start_id] = 0
88
+
89
+ # Get starting node
90
+ start_node = self._qb.get_node_by_id(start_id)
91
+ if start_node:
92
+ result.nodes.append(start_node)
93
+
94
+ while queue:
95
+ current_id, depth = queue.popleft()
96
+
97
+ if max_depth is not None and depth >= max_depth:
98
+ continue
99
+
100
+ # Get edges based on direction
101
+ if direction in ('outgoing', 'both'):
102
+ outgoing = self._qb.get_outgoing_edges(current_id, edge_kinds)
103
+ for edge in outgoing:
104
+ result.edges.append(edge)
105
+ if edge.target not in visited:
106
+ visited.add(edge.target)
107
+ result.depths[edge.target] = depth + 1
108
+ queue.append((edge.target, depth + 1))
109
+ target_node = self._qb.get_node_by_id(edge.target)
110
+ if target_node:
111
+ result.nodes.append(target_node)
112
+
113
+ if direction in ('incoming', 'both'):
114
+ incoming = self._qb.get_incoming_edges(current_id, edge_kinds)
115
+ for edge in incoming:
116
+ result.edges.append(edge)
117
+ if edge.source not in visited:
118
+ visited.add(edge.source)
119
+ result.depths[edge.source] = depth + 1
120
+ queue.append((edge.source, depth + 1))
121
+ source_node = self._qb.get_node_by_id(edge.source)
122
+ if source_node:
123
+ result.nodes.append(source_node)
124
+
125
+ return result
126
+
127
+ def dfs_traverse(
128
+ self,
129
+ start_id: str,
130
+ max_depth: Optional[int] = None,
131
+ edge_kinds: Optional[List[str]] = None,
132
+ direction: str = 'outgoing',
133
+ ) -> TraversalResult:
134
+ """Depth-first traversal from a starting node.
135
+
136
+ Args:
137
+ start_id: Starting node ID
138
+ max_depth: Maximum traversal depth (None for unlimited)
139
+ edge_kinds: Filter by edge kinds
140
+ direction: 'outgoing', 'incoming', or 'both'
141
+
142
+ Returns:
143
+ TraversalResult with visited nodes and edges
144
+ """
145
+ result = TraversalResult()
146
+ visited: Set[str] = set()
147
+
148
+ def dfs(current_id: str, depth: int) -> None:
149
+ if max_depth is not None and depth >= max_depth:
150
+ return
151
+ if current_id in visited:
152
+ return
153
+
154
+ visited.add(current_id)
155
+ result.depths[current_id] = depth
156
+
157
+ node = self._qb.get_node_by_id(current_id)
158
+ if node:
159
+ result.nodes.append(node)
160
+
161
+ # Get edges based on direction
162
+ edges = []
163
+ if direction in ('outgoing', 'both'):
164
+ edges.extend(self._qb.get_outgoing_edges(current_id, edge_kinds))
165
+ if direction in ('incoming', 'both'):
166
+ edges.extend(self._qb.get_incoming_edges(current_id, edge_kinds))
167
+
168
+ for edge in edges:
169
+ result.edges.append(edge)
170
+ neighbor_id = edge.target if direction in ('outgoing', 'both') and edge.target != current_id else edge.source
171
+ if neighbor_id != current_id:
172
+ dfs(neighbor_id, depth + 1)
173
+
174
+ dfs(start_id, 0)
175
+ return result
176
+
177
+ def getCallers(self, node_id: str, include_indirect: bool = False) -> List[Node]:
178
+ """Get nodes that call/ reference the given node.
179
+
180
+ Args:
181
+ node_id: Target node ID
182
+ include_indirect: If True, include indirect callers through the call chain
183
+
184
+ Returns:
185
+ List of caller nodes
186
+ """
187
+ callers: List[Node] = []
188
+ seen: Set[str] = set()
189
+
190
+ def collect_callers(nid: str) -> None:
191
+ if nid in seen:
192
+ return
193
+ seen.add(nid)
194
+
195
+ edges = self._qb.get_incoming_edges(nid, [EdgeKind.CALLS, EdgeKind.REFERENCES])
196
+ for edge in edges:
197
+ caller_node = self._qb.get_node_by_id(edge.source)
198
+ if caller_node and edge.source not in seen:
199
+ callers.append(caller_node)
200
+ if include_indirect:
201
+ collect_callers(edge.source)
202
+
203
+ collect_callers(node_id)
204
+ return callers
205
+
206
+ def getCallees(self, node_id: str, include_indirect: bool = False) -> List[Node]:
207
+ """Get nodes that the given node calls/ references.
208
+
209
+ Args:
210
+ node_id: Source node ID
211
+ include_indirect: If True, include indirect callees through the call chain
212
+
213
+ Returns:
214
+ List of callee nodes
215
+ """
216
+ callees: List[Node] = []
217
+ seen: Set[str] = set()
218
+
219
+ def collect_callees(nid: str) -> None:
220
+ if nid in seen:
221
+ return
222
+ seen.add(nid)
223
+
224
+ edges = self._qb.get_outgoing_edges(nid, [EdgeKind.CALLS, EdgeKind.REFERENCES])
225
+ for edge in edges:
226
+ callee_node = self._qb.get_node_by_id(edge.target)
227
+ if callee_node and edge.target not in seen:
228
+ callees.append(callee_node)
229
+ if include_indirect:
230
+ collect_callees(edge.target)
231
+
232
+ collect_callees(node_id)
233
+ return callees
234
+
235
+ def getImpactRadius(
236
+ self,
237
+ node_id: str,
238
+ radius: int = 3,
239
+ edge_kinds: Optional[List[str]] = None,
240
+ ) -> TraversalResult:
241
+ """Get all nodes within N hops of the given node.
242
+
243
+ Args:
244
+ node_id: Starting node ID
245
+ radius: Number of hops to traverse
246
+ edge_kinds: Filter by edge kinds
247
+
248
+ Returns:
249
+ TraversalResult with all nodes within radius
250
+ """
251
+ return self.bfs_traverse(
252
+ start_id=node_id,
253
+ max_depth=radius,
254
+ edge_kinds=edge_kinds,
255
+ direction='both',
256
+ )
257
+
258
+ def findPath(
259
+ self,
260
+ start_id: str,
261
+ end_id: str,
262
+ edge_kinds: Optional[List[str]] = None,
263
+ max_length: int = 20,
264
+ ) -> PathResult:
265
+ """Find a path between two nodes using BFS.
266
+
267
+ Args:
268
+ start_id: Starting node ID
269
+ end_id: Target node ID
270
+ edge_kinds: Filter by edge kinds
271
+ max_length: Maximum path length
272
+
273
+ Returns:
274
+ PathResult with found path or empty result
275
+ """
276
+ if start_id == end_id:
277
+ node = self._qb.get_node_by_id(start_id)
278
+ return PathResult(
279
+ found=True,
280
+ path=[start_id],
281
+ nodes=[node] if node else [],
282
+ length=0,
283
+ )
284
+
285
+ queue: deque[Tuple[str, List[str]]] = deque([(start_id, [start_id])])
286
+ visited: Set[str] = {start_id}
287
+
288
+ while queue:
289
+ current_id, path = queue.popleft()
290
+
291
+ if len(path) >= max_length:
292
+ continue
293
+
294
+ # Check outgoing edges
295
+ edges = self._qb.get_outgoing_edges(current_id, edge_kinds)
296
+ for edge in edges:
297
+ if edge.target == end_id:
298
+ full_path = path + [edge.target]
299
+ nodes = self._qb.get_nodes_by_ids(full_path)
300
+ return PathResult(
301
+ found=True,
302
+ path=full_path,
303
+ nodes=nodes,
304
+ edges=edges,
305
+ length=len(full_path) - 1,
306
+ )
307
+
308
+ if edge.target not in visited:
309
+ visited.add(edge.target)
310
+ queue.append((edge.target, path + [edge.target]))
311
+
312
+ return PathResult(found=False)
313
+
314
+ def getAncestors(
315
+ self,
316
+ node_id: str,
317
+ edge_kinds: Optional[List[str]] = None,
318
+ max_depth: Optional[int] = None,
319
+ ) -> List[Node]:
320
+ """Get ancestor nodes (parent, grandparent, etc.) via incoming edges.
321
+
322
+ Args:
323
+ node_id: Starting node ID
324
+ edge_kinds: Filter by edge kinds (e.g., 'extends', 'imports')
325
+ max_depth: Maximum depth to traverse
326
+
327
+ Returns:
328
+ List of ancestor nodes
329
+ """
330
+ result = self.bfs_traverse(
331
+ start_id=node_id,
332
+ max_depth=max_depth,
333
+ edge_kinds=edge_kinds,
334
+ direction='incoming',
335
+ )
336
+ return result.nodes
337
+
338
+ def getChildren(
339
+ self,
340
+ node_id: str,
341
+ edge_kinds: Optional[List[str]] = None,
342
+ max_depth: Optional[int] = None,
343
+ ) -> List[Node]:
344
+ """Get child nodes via outgoing edges.
345
+
346
+ Args:
347
+ node_id: Starting node ID
348
+ edge_kinds: Filter by edge kinds (e.g., 'contains', 'calls')
349
+ max_depth: Maximum depth to traverse
350
+
351
+ Returns:
352
+ List of child nodes
353
+ """
354
+ result = self.bfs_traverse(
355
+ start_id=node_id,
356
+ max_depth=max_depth,
357
+ edge_kinds=edge_kinds,
358
+ direction='outgoing',
359
+ )
360
+ return result.nodes
361
+
362
+ def getTypeHierarchy(
363
+ self,
364
+ node_id: str,
365
+ ) -> Dict[str, List[Node]]:
366
+ """Get type hierarchy for a node (super types and sub types).
367
+
368
+ Args:
369
+ node_id: Node ID to get hierarchy for
370
+
371
+ Returns:
372
+ Dict with 'super' and 'sub' keys containing type lists
373
+ """
374
+ hierarchy: Dict[str, List[Node]] = {
375
+ 'super': [], # Parent types
376
+ 'sub': [], # Child types
377
+ }
378
+
379
+ # Get super types (extends, implements)
380
+ super_edges = self._qb.get_outgoing_edges(
381
+ node_id,
382
+ [EdgeKind.EXTENDS, EdgeKind.IMPLEMENTS]
383
+ )
384
+ for edge in super_edges:
385
+ super_node = self._qb.get_node_by_id(edge.target)
386
+ if super_node:
387
+ hierarchy['super'].append(super_node)
388
+
389
+ # Get sub types
390
+ sub_edges = self._qb.get_incoming_edges(
391
+ node_id,
392
+ [EdgeKind.EXTENDS, EdgeKind.IMPLEMENTS]
393
+ )
394
+ for edge in sub_edges:
395
+ sub_node = self._qb.get_node_by_id(edge.source)
396
+ if sub_node:
397
+ hierarchy['sub'].append(sub_node)
398
+
399
+ return hierarchy
400
+
401
+
402
+ class GraphQueryManager:
403
+ """High-level graph queries and analysis."""
404
+
405
+ def __init__(self, db: sqlite3.Connection, db_path: str = ''):
406
+ self._db = db
407
+ self._qb = QueryBuilder(db, db_path)
408
+ self._traverser = GraphTraverser(db, db_path)
409
+
410
+ def getContext(
411
+ self,
412
+ node_id: str,
413
+ depth: int = 2,
414
+ include_code: bool = False,
415
+ ) -> Dict[str, Any]:
416
+ """Get comprehensive context around a node.
417
+
418
+ Args:
419
+ node_id: Focal node ID
420
+ depth: Traversal depth
421
+ include_code: Include source code snippets
422
+
423
+ Returns:
424
+ Dict with context information
425
+ """
426
+ focal = self._qb.get_node_by_id(node_id)
427
+ if not focal:
428
+ return {}
429
+
430
+ # Get traversal results
431
+ outgoing = self._traverser.bfs_traverse(node_id, max_depth=depth, direction='outgoing')
432
+ incoming = self._traverser.bfs_traverse(node_id, max_depth=depth, direction='incoming')
433
+
434
+ # Get direct callers and callees
435
+ callers = self._traverser.getCallers(node_id)
436
+ callees = self._traverser.getCallees(node_id)
437
+
438
+ # Get type hierarchy
439
+ type_hierarchy = self._traverser.getTypeHierarchy(node_id)
440
+
441
+ return {
442
+ 'focal': focal,
443
+ 'ancestors': incoming.nodes,
444
+ 'children': outgoing.nodes,
445
+ 'callers': callers,
446
+ 'callees': callees,
447
+ 'super_types': type_hierarchy.get('super', []),
448
+ 'sub_types': type_hierarchy.get('sub', []),
449
+ 'outgoing_edges': outgoing.edges,
450
+ 'incoming_edges': incoming.edges,
451
+ }
452
+
453
+ def getFileDependencies(
454
+ self,
455
+ file_path: str,
456
+ transitive: bool = False,
457
+ ) -> Dict[str, List[DependencyInfo]]:
458
+ """Get dependencies for a file.
459
+
460
+ Args:
461
+ file_path: Path to the file
462
+ transitive: If True, include transitive dependencies
463
+
464
+ Returns:
465
+ Dict with 'imports' and 'exports' keys
466
+ """
467
+ nodes = self._qb.get_nodes_by_file(file_path)
468
+
469
+ dependencies: Dict[str, List[DependencyInfo]] = {
470
+ 'imports': [],
471
+ 'exports': [],
472
+ }
473
+
474
+ # Get import edges from nodes in this file
475
+ for node in nodes:
476
+ outgoing = self._qb.get_outgoing_edges(
477
+ node.id,
478
+ [EdgeKind.IMPORTS, EdgeKind.REFERENCES]
479
+ )
480
+ for edge in outgoing:
481
+ target_node = self._qb.get_node_by_id(edge.target)
482
+ if target_node:
483
+ dependencies['imports'].append(DependencyInfo(
484
+ source=node.id,
485
+ target=edge.target,
486
+ kind=edge.kind,
487
+ file_path=target_node.file_path,
488
+ line=edge.line,
489
+ ))
490
+
491
+ # Get export edges
492
+ incoming = self._qb.get_incoming_edges(node.id, [EdgeKind.EXPORTS])
493
+ for edge in incoming:
494
+ source_node = self._qb.get_node_by_id(edge.source)
495
+ if source_node:
496
+ dependencies['exports'].append(DependencyInfo(
497
+ source=edge.source,
498
+ target=node.id,
499
+ kind=edge.kind,
500
+ file_path=source_node.file_path,
501
+ line=edge.line,
502
+ ))
503
+
504
+ # Add transitive dependencies if requested
505
+ if transitive:
506
+ visited_files: Set[str] = {file_path}
507
+ queue = list(dependencies['imports'])
508
+
509
+ while queue:
510
+ dep = queue.pop(0)
511
+ if dep.file_path and dep.file_path in visited_files:
512
+ continue
513
+ if dep.file_path:
514
+ visited_files.add(dep.file_path)
515
+ sub_deps = self.getFileDependencies(dep.file_path, transitive=False)
516
+ dependencies['imports'].extend(sub_deps.get('imports', []))
517
+
518
+ return dependencies
519
+
520
+ def getExportedSymbols(self, file_path: str) -> List[Node]:
521
+ """Get exported symbols from a file.
522
+
523
+ Args:
524
+ file_path: Path to the file
525
+
526
+ Returns:
527
+ List of exported nodes
528
+ """
529
+ nodes = self._qb.get_nodes_by_file(file_path)
530
+ exported = []
531
+
532
+ for node in nodes:
533
+ # Check if node is exported
534
+ if node.is_exported:
535
+ exported.append(node)
536
+ continue
537
+
538
+ # Check for export edges
539
+ edges = self._qb.get_incoming_edges(node.id, [EdgeKind.EXPORTS])
540
+ if edges:
541
+ exported.append(node)
542
+
543
+ return exported
544
+
545
+ def findCircularDependencies(
546
+ self,
547
+ start_id: Optional[str] = None,
548
+ ) -> List[List[str]]:
549
+ """Find circular dependencies in the graph.
550
+
551
+ Args:
552
+ start_id: Optional starting node ID
553
+
554
+ Returns:
555
+ List of cycles, each as a list of node IDs
556
+ """
557
+ cycles: List[List[str]] = []
558
+
559
+ if start_id:
560
+ nodes_to_check = [start_id]
561
+ else:
562
+ # Check all nodes
563
+ all_files = self._qb.get_all_files()
564
+ nodes_to_check = []
565
+ for f in all_files:
566
+ file_nodes = self._qb.get_nodes_by_file(f.path)
567
+ for n in file_nodes:
568
+ if n.kind in (NodeKind.MODULE, NodeKind.CLASS, NodeKind.FUNCTION):
569
+ nodes_to_check.append(n.id)
570
+
571
+ for node_id in nodes_to_check:
572
+ visited: Set[str] = set()
573
+ path: List[str] = []
574
+
575
+ def dfs(current: str) -> bool:
576
+ if current in visited:
577
+ # Found a cycle
578
+ if current in path:
579
+ idx = path.index(current)
580
+ cycle = path[idx:] + [current]
581
+ if cycle not in cycles:
582
+ cycles.append(cycle)
583
+ return True
584
+ return False
585
+
586
+ visited.add(current)
587
+ path.append(current)
588
+
589
+ edges = self._qb.get_outgoing_edges(current, [EdgeKind.IMPORTS, EdgeKind.CALLS])
590
+ for edge in edges:
591
+ if dfs(edge.target):
592
+ return True
593
+
594
+ path.pop()
595
+ return False
596
+
597
+ dfs(node_id)
598
+
599
+ return cycles
600
+
601
+ def getNodeMetrics(self, node_id: str) -> NodeMetrics:
602
+ """Get metrics for a node.
603
+
604
+ Args:
605
+ node_id: Node ID
606
+
607
+ Returns:
608
+ NodeMetrics with various measurements
609
+ """
610
+ metrics = NodeMetrics(node_id=node_id)
611
+
612
+ # Get outgoing edges
613
+ outgoing = self._qb.get_outgoing_edges(node_id)
614
+ metrics.outgoing_edges = len(outgoing)
615
+ metrics.fan_out = len([e for e in outgoing if e.kind in (EdgeKind.CALLS, EdgeKind.REFERENCES)])
616
+
617
+ # Get incoming edges
618
+ incoming = self._qb.get_incoming_edges(node_id)
619
+ metrics.incoming_edges = len(incoming)
620
+ metrics.fan_in = len([e for e in incoming if e.kind in (EdgeKind.CALLS, EdgeKind.REFERENCES)])
621
+
622
+ # Calculate call depth (longest path from any root)
623
+ result = self._traverser.bfs_traverse(node_id, direction='incoming', edge_kinds=[EdgeKind.CALLS])
624
+ metrics.call_depth = max(result.depths.values()) if result.depths else 0
625
+
626
+ return metrics
627
+
628
+ def findDeadCode(
629
+ self,
630
+ exclude_exports: bool = True,
631
+ min_fan_in: int = 0,
632
+ ) -> Dict[str, List[Node]]:
633
+ """Find potentially dead/unused code.
634
+
635
+ Args:
636
+ exclude_exports: Exclude exported symbols from results
637
+ min_fan_in: Minimum fan-in (incoming references) to consider alive
638
+
639
+ Returns:
640
+ Dict with categories of dead code
641
+ """
642
+ all_nodes = []
643
+ files = self._qb.get_all_files()
644
+
645
+ for f in files:
646
+ nodes = self._qb.get_nodes_by_file(f.path)
647
+ all_nodes.extend(nodes)
648
+
649
+ dead_code: Dict[str, List[Node]] = {
650
+ 'unexported_functions': [],
651
+ 'unused_imports': [],
652
+ 'unreferenced': [],
653
+ }
654
+
655
+ for node in all_nodes:
656
+ # Skip certain kinds
657
+ if node.kind not in (NodeKind.FUNCTION, NodeKind.METHOD, NodeKind.CLASS, NodeKind.VARIABLE):
658
+ continue
659
+
660
+ # Get incoming references
661
+ incoming = self._qb.get_incoming_edges(node.id, [EdgeKind.CALLS, EdgeKind.REFERENCES])
662
+
663
+ # Check if exported
664
+ is_exported = node.is_exported
665
+ export_edges = self._qb.get_incoming_edges(node.id, [EdgeKind.EXPORTS])
666
+ if export_edges:
667
+ is_exported = True
668
+
669
+ if not is_exported and exclude_exports:
670
+ if node.kind in (NodeKind.FUNCTION, NodeKind.METHOD):
671
+ dead_code['unexported_functions'].append(node)
672
+
673
+ # Check for unreferenced
674
+ if len(incoming) <= min_fan_in and not is_exported:
675
+ if node.kind in (NodeKind.FUNCTION, NodeKind.METHOD, NodeKind.CLASS):
676
+ dead_code['unreferenced'].append(node)
677
+
678
+ # Find unused imports
679
+ all_import_nodes = self._qb.get_nodes_by_kind(NodeKind.IMPORT)
680
+ for imp in all_import_nodes:
681
+ incoming = self._qb.get_incoming_edges(imp.id, [EdgeKind.REFERENCES])
682
+ if not incoming:
683
+ dead_code['unused_imports'].append(imp)
684
+
685
+ return dead_code
@@ -0,0 +1,6 @@
1
+ """CodeGraph Installer - Plug-in for AI agent auto-configuration."""
2
+
3
+
4
+ def run_installer():
5
+ """Run the interactive installer (not yet implemented)."""
6
+ raise NotImplementedError("Installer not yet available in Python version")