codegraphcontext-rust 0.5.5__cp311-cp311-win_amd64.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 (101) hide show
  1. codegraphcontext/__init__.py +2 -0
  2. codegraphcontext/__main__.py +5 -0
  3. codegraphcontext/_cgc_rust.cp311-win_amd64.pyd +0 -0
  4. codegraphcontext/cli/__init__.py +1 -0
  5. codegraphcontext/cli/cli_helpers.py +774 -0
  6. codegraphcontext/cli/config_manager.py +1051 -0
  7. codegraphcontext/cli/main.py +2386 -0
  8. codegraphcontext/cli/registry_commands.py +404 -0
  9. codegraphcontext/cli/setup_macos.py +93 -0
  10. codegraphcontext/cli/setup_wizard.py +991 -0
  11. codegraphcontext/cli/visualizer.py +50 -0
  12. codegraphcontext/core/__init__.py +165 -0
  13. codegraphcontext/core/bundle_registry.py +181 -0
  14. codegraphcontext/core/cgc_bundle.py +858 -0
  15. codegraphcontext/core/cgcignore.py +118 -0
  16. codegraphcontext/core/database.py +273 -0
  17. codegraphcontext/core/database_falkordb.py +480 -0
  18. codegraphcontext/core/database_falkordb_remote.py +199 -0
  19. codegraphcontext/core/database_kuzu.py +636 -0
  20. codegraphcontext/core/falkor_worker.py +132 -0
  21. codegraphcontext/core/jobs.py +132 -0
  22. codegraphcontext/core/watcher.py +260 -0
  23. codegraphcontext/prompts.py +125 -0
  24. codegraphcontext/server.py +471 -0
  25. codegraphcontext/tool_definitions.py +220 -0
  26. codegraphcontext/tools/__init__.py +0 -0
  27. codegraphcontext/tools/advanced_language_query_tool.py +103 -0
  28. codegraphcontext/tools/code_finder.py +1160 -0
  29. codegraphcontext/tools/graph_builder.py +337 -0
  30. codegraphcontext/tools/handlers/analysis_handlers.py +114 -0
  31. codegraphcontext/tools/handlers/indexing_handlers.py +116 -0
  32. codegraphcontext/tools/handlers/management_handlers.py +339 -0
  33. codegraphcontext/tools/handlers/query_handlers.py +83 -0
  34. codegraphcontext/tools/handlers/watcher_handlers.py +83 -0
  35. codegraphcontext/tools/indexing/__init__.py +1 -0
  36. codegraphcontext/tools/indexing/constants.py +25 -0
  37. codegraphcontext/tools/indexing/discovery.py +64 -0
  38. codegraphcontext/tools/indexing/engine.py +109 -0
  39. codegraphcontext/tools/indexing/persistence/__init__.py +3 -0
  40. codegraphcontext/tools/indexing/persistence/parquet_loader.py +643 -0
  41. codegraphcontext/tools/indexing/persistence/writer.py +1114 -0
  42. codegraphcontext/tools/indexing/pipeline.py +171 -0
  43. codegraphcontext/tools/indexing/pre_scan.py +105 -0
  44. codegraphcontext/tools/indexing/resolution/__init__.py +8 -0
  45. codegraphcontext/tools/indexing/resolution/calls.py +253 -0
  46. codegraphcontext/tools/indexing/resolution/inheritance.py +91 -0
  47. codegraphcontext/tools/indexing/sanitize.py +41 -0
  48. codegraphcontext/tools/indexing/schema.py +79 -0
  49. codegraphcontext/tools/indexing/schema_contract.py +44 -0
  50. codegraphcontext/tools/indexing/scip_pipeline.py +140 -0
  51. codegraphcontext/tools/languages/c.py +563 -0
  52. codegraphcontext/tools/languages/cpp.py +616 -0
  53. codegraphcontext/tools/languages/csharp.py +551 -0
  54. codegraphcontext/tools/languages/dart.py +378 -0
  55. codegraphcontext/tools/languages/elixir.py +460 -0
  56. codegraphcontext/tools/languages/go.py +499 -0
  57. codegraphcontext/tools/languages/haskell.py +426 -0
  58. codegraphcontext/tools/languages/java.py +500 -0
  59. codegraphcontext/tools/languages/javascript.py +589 -0
  60. codegraphcontext/tools/languages/kotlin.py +640 -0
  61. codegraphcontext/tools/languages/perl.py +260 -0
  62. codegraphcontext/tools/languages/php.py +519 -0
  63. codegraphcontext/tools/languages/python.py +576 -0
  64. codegraphcontext/tools/languages/ruby.py +537 -0
  65. codegraphcontext/tools/languages/rust.py +295 -0
  66. codegraphcontext/tools/languages/scala.py +519 -0
  67. codegraphcontext/tools/languages/swift.py +491 -0
  68. codegraphcontext/tools/languages/typescript.py +575 -0
  69. codegraphcontext/tools/languages/typescriptjsx.py +151 -0
  70. codegraphcontext/tools/package_resolver.py +472 -0
  71. codegraphcontext/tools/query_tool_languages/c_toolkit.py +5 -0
  72. codegraphcontext/tools/query_tool_languages/cpp_toolkit.py +87 -0
  73. codegraphcontext/tools/query_tool_languages/csharp_toolkit.py +5 -0
  74. codegraphcontext/tools/query_tool_languages/dart_toolkit.py +5 -0
  75. codegraphcontext/tools/query_tool_languages/go_toolkit.py +5 -0
  76. codegraphcontext/tools/query_tool_languages/haskell_toolkit.py +5 -0
  77. codegraphcontext/tools/query_tool_languages/java_toolkit.py +5 -0
  78. codegraphcontext/tools/query_tool_languages/javascript_toolkit.py +5 -0
  79. codegraphcontext/tools/query_tool_languages/perl_toolkit.py +5 -0
  80. codegraphcontext/tools/query_tool_languages/python_toolkit.py +5 -0
  81. codegraphcontext/tools/query_tool_languages/ruby_toolkit.py +5 -0
  82. codegraphcontext/tools/query_tool_languages/rust_toolkit.py +5 -0
  83. codegraphcontext/tools/query_tool_languages/scala_toolkit.py +5 -0
  84. codegraphcontext/tools/query_tool_languages/swift_toolkit.py +5 -0
  85. codegraphcontext/tools/query_tool_languages/typescript_toolkit.py +5 -0
  86. codegraphcontext/tools/scip_indexer.py +467 -0
  87. codegraphcontext/tools/scip_pb2.py +2455 -0
  88. codegraphcontext/tools/system.py +133 -0
  89. codegraphcontext/tools/tree_sitter_parser.py +103 -0
  90. codegraphcontext/utils/debug_log.py +90 -0
  91. codegraphcontext/utils/path_ignore.py +55 -0
  92. codegraphcontext/utils/repo_path.py +27 -0
  93. codegraphcontext/utils/tree_sitter_manager.py +285 -0
  94. codegraphcontext/utils/visualize_graph.py +0 -0
  95. codegraphcontext/viz/server.py +282 -0
  96. codegraphcontext_rust-0.5.5.dist-info/METADATA +537 -0
  97. codegraphcontext_rust-0.5.5.dist-info/RECORD +101 -0
  98. codegraphcontext_rust-0.5.5.dist-info/WHEEL +4 -0
  99. codegraphcontext_rust-0.5.5.dist-info/entry_points.txt +3 -0
  100. codegraphcontext_rust-0.5.5.dist-info/licenses/LICENSE +21 -0
  101. codegraphcontext_rust-0.5.5.dist-info/sboms/cgc-python.cyclonedx.json +2333 -0
@@ -0,0 +1,1160 @@
1
+ # src/codegraphcontext/tools/code_finder.py
2
+ import logging
3
+
4
+ from typing import Any, Dict, List, Literal, Optional
5
+ from pathlib import Path
6
+
7
+ from ..core.database import DatabaseManager
8
+ from ..utils.path_ignore import cypher_path_not_under_ignore_dirs
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ def _levenshtein_distance(a: str, b: str) -> int:
14
+ """Levenshtein distance for short identifiers (typo-tolerant name search)."""
15
+ if len(a) < len(b):
16
+ return _levenshtein_distance(b, a)
17
+ if not b:
18
+ return len(a)
19
+ prev = list(range(len(b) + 1))
20
+ for i, c1 in enumerate(a):
21
+ curr = [i + 1]
22
+ for j, c2 in enumerate(b):
23
+ curr.append(min(prev[j + 1] + 1, curr[j] + 1, prev[j] + (c1 != c2)))
24
+ prev = curr
25
+ return prev[-1]
26
+
27
+ class CodeFinder:
28
+ """Module for finding relevant code snippets and analyzing relationships."""
29
+
30
+ def __init__(self, db_manager: DatabaseManager):
31
+ self.db_manager = db_manager
32
+ self.driver = self.db_manager.get_driver()
33
+ self._lacks_native_fulltext = getattr(db_manager, 'get_backend_type', lambda: 'neo4j')() != 'neo4j'
34
+
35
+ def format_query(self, find_by: Literal["Class", "Function"], fuzzy_search:bool, repo_path: Optional[str] = None) -> str:
36
+ """Format the search query based on the search type and fuzzy search settings."""
37
+ repo_filter = "AND node.path STARTS WITH $repo_path" if repo_path else ""
38
+ if self._lacks_native_fulltext:
39
+ # FalkorDB does not support CALL db.idx.fulltext.queryNodes.
40
+ # Fall back to a pure Cypher CONTAINS/toLower match on node name.
41
+ name_filter = "toLower(node.name) CONTAINS toLower($search_term)"
42
+ return f"""
43
+ MATCH (node:{find_by})
44
+ WHERE {name_filter} {repo_filter}
45
+ RETURN node.name as name, node.path as path, node.line_number as line_number,
46
+ node.source as source, node.docstring as docstring, node.is_dependency as is_dependency
47
+ ORDER BY node.is_dependency ASC, node.name
48
+ LIMIT 20
49
+ """
50
+ return f"""
51
+ CALL db.index.fulltext.queryNodes("code_search_index", $search_term) YIELD node, score
52
+ WITH node, score
53
+ WHERE node:{find_by} {'AND node.name CONTAINS $search_term' if not fuzzy_search else ''} {repo_filter}
54
+ RETURN node.name as name, node.path as path, node.line_number as line_number,
55
+ node.source as source, node.docstring as docstring, node.is_dependency as is_dependency
56
+ ORDER BY score DESC
57
+ LIMIT 20
58
+ """
59
+
60
+ def _find_by_name_fuzzy_portable(
61
+ self,
62
+ label: Literal["Function", "Class"],
63
+ search_term: str,
64
+ edit_distance: int,
65
+ repo_path: Optional[str],
66
+ ) -> List[Dict]:
67
+ """Fuzzy name match for backends without Lucene fuzzy syntax (Kùzu, FalkorDB, …)."""
68
+ if not search_term.strip():
69
+ return []
70
+ where_clause = "WHERE node.path STARTS WITH $repo_path" if repo_path else ""
71
+ limit_tail = "" if repo_path else " LIMIT 8000"
72
+ params: Dict[str, Any] = {}
73
+ if repo_path:
74
+ params["repo_path"] = repo_path
75
+ query = f"""
76
+ MATCH (node:{label})
77
+ {where_clause}
78
+ RETURN node.name as name, node.path as path, node.line_number as line_number,
79
+ node.source as source, node.docstring as docstring, node.is_dependency as is_dependency
80
+ {limit_tail}
81
+ """
82
+ with self.driver.session() as session:
83
+ rows = session.run(query, **params).data()
84
+ q = search_term.lower()
85
+ scored: List[tuple[int, Dict]] = []
86
+ for row in rows:
87
+ nm = row.get("name")
88
+ if not isinstance(nm, str):
89
+ continue
90
+ d = _levenshtein_distance(q, nm.lower())
91
+ if d <= edit_distance:
92
+ scored.append((d, row))
93
+ scored.sort(key=lambda x: x[0])
94
+ return [r for _, r in scored[:20]]
95
+
96
+ def find_by_function_name(
97
+ self,
98
+ search_term: str,
99
+ fuzzy_search: bool,
100
+ repo_path: Optional[str] = None,
101
+ edit_distance: int = 2,
102
+ ) -> List[Dict]:
103
+ """Find functions by name matching."""
104
+ if not fuzzy_search:
105
+ with self.driver.session() as session:
106
+ result = session.run(f"""
107
+ MATCH (node:Function {{name: $name}})
108
+ {"WHERE node.path STARTS WITH $repo_path" if repo_path else ""}
109
+ RETURN node.name as name, node.path as path, node.line_number as line_number,
110
+ node.source as source, node.docstring as docstring, node.is_dependency as is_dependency
111
+ LIMIT 20
112
+ """, name=search_term, repo_path=repo_path)
113
+ return result.data()
114
+
115
+ if self._lacks_native_fulltext:
116
+ return self._find_by_name_fuzzy_portable(
117
+ "Function", search_term, edit_distance, repo_path
118
+ )
119
+
120
+ formatted_search_term = f"name:{search_term}"
121
+ with self.driver.session() as session:
122
+ result = session.run(
123
+ self.format_query("Function", fuzzy_search, repo_path),
124
+ search_term=formatted_search_term,
125
+ repo_path=repo_path,
126
+ )
127
+ return result.data()
128
+
129
+ def find_by_class_name(
130
+ self,
131
+ search_term: str,
132
+ fuzzy_search: bool,
133
+ repo_path: Optional[str] = None,
134
+ edit_distance: int = 2,
135
+ ) -> List[Dict]:
136
+ """Find classes by name matching."""
137
+ if not fuzzy_search:
138
+ with self.driver.session() as session:
139
+ result = session.run(f"""
140
+ MATCH (node:Class {{name: $name}})
141
+ {"WHERE node.path STARTS WITH $repo_path" if repo_path else ""}
142
+ RETURN node.name as name, node.path as path, node.line_number as line_number,
143
+ node.source as source, node.docstring as docstring, node.is_dependency as is_dependency
144
+ LIMIT 20
145
+ """, name=search_term, repo_path=repo_path)
146
+ return result.data()
147
+
148
+ if self._lacks_native_fulltext:
149
+ return self._find_by_name_fuzzy_portable(
150
+ "Class", search_term, edit_distance, repo_path
151
+ )
152
+
153
+ formatted_search_term = f"name:{search_term}"
154
+ with self.driver.session() as session:
155
+ result = session.run(
156
+ self.format_query("Class", fuzzy_search, repo_path),
157
+ search_term=formatted_search_term,
158
+ repo_path=repo_path,
159
+ )
160
+ return result.data()
161
+
162
+ def find_by_variable_name(self, search_term: str, repo_path: Optional[str] = None) -> List[Dict]:
163
+ """Find variables by name matching"""
164
+ with self.driver.session() as session:
165
+ result = session.run(f"""
166
+ MATCH (v:Variable)
167
+ WHERE v.name CONTAINS $search_term {"AND v.path STARTS WITH $repo_path" if repo_path else ""}
168
+ RETURN v.name as name, v.path as path, v.line_number as line_number,
169
+ v.value as value, v.context as context, v.is_dependency as is_dependency
170
+ ORDER BY v.is_dependency ASC, v.name
171
+ LIMIT 20
172
+ """, search_term=search_term, repo_path=repo_path)
173
+
174
+ return result.data()
175
+
176
+ def find_by_content(self, search_term: str, repo_path: Optional[str] = None) -> List[Dict]:
177
+ """Find code by content matching in source or docstrings using the full-text index."""
178
+ if self._lacks_native_fulltext:
179
+ return self._find_by_content_falkordb(search_term, repo_path)
180
+ with self.driver.session() as session:
181
+ result = session.run(f"""
182
+ CALL db.index.fulltext.queryNodes("code_search_index", $search_term) YIELD node, score
183
+ WITH node, score
184
+ WHERE (node:Function OR node:Class OR node:Variable) {"AND node.path STARTS WITH $repo_path" if repo_path else ""}
185
+ MATCH (node)<-[:CONTAINS]-(f:File)
186
+ RETURN
187
+ CASE
188
+ WHEN node:Function THEN 'function'
189
+ WHEN node:Class THEN 'class'
190
+ ELSE 'variable'
191
+ END as type,
192
+ node.name as name, f.path as path,
193
+ node.line_number as line_number, node.source as source,
194
+ node.docstring as docstring, node.is_dependency as is_dependency
195
+ ORDER BY score DESC
196
+ LIMIT 20
197
+ """, search_term=search_term, repo_path=repo_path)
198
+ return result.data()
199
+
200
+ def _find_by_content_falkordb(self, search_term: str, repo_path: Optional[str] = None) -> List[Dict]:
201
+ """FalkorDB-compatible content search using pure Cypher CONTAINS matching.
202
+ FalkorDB does not support CALL db.idx.fulltext.queryNodes, so we fall back
203
+ to substring matching on name, source, and docstring fields."""
204
+ all_results = []
205
+ with self.driver.session() as session:
206
+ repo_filter = "AND node.path STARTS WITH $repo_path" if repo_path else ""
207
+ for label, type_name in [('Function', 'function'), ('Class', 'class')]:
208
+ try:
209
+ result = session.run(f"""
210
+ MATCH (node:{label})
211
+ WHERE (toLower(node.name) CONTAINS toLower($search_term)
212
+ OR (node.source IS NOT NULL AND toLower(node.source) CONTAINS toLower($search_term))
213
+ OR (node.docstring IS NOT NULL AND toLower(node.docstring) CONTAINS toLower($search_term)))
214
+ {repo_filter}
215
+ RETURN
216
+ '{type_name}' as type,
217
+ node.name as name, node.path as path,
218
+ node.line_number as line_number, node.source as source,
219
+ node.docstring as docstring, node.is_dependency as is_dependency
220
+ ORDER BY node.is_dependency ASC, node.name
221
+ LIMIT 20
222
+ """, search_term=search_term, repo_path=repo_path)
223
+ all_results.extend(result.data())
224
+ except Exception:
225
+ logger.debug(f"FalkorDB content query failed for label {label}", exc_info=True)
226
+ return all_results[:20]
227
+
228
+ def find_by_module_name(self, search_term: str) -> List[Dict]:
229
+ """Find modules by name matching"""
230
+ with self.driver.session() as session:
231
+ result = session.run("""
232
+ MATCH (m:Module)
233
+ WHERE m.name CONTAINS $search_term
234
+ RETURN m.name as name, m.lang as lang
235
+ ORDER BY m.name
236
+ LIMIT 20
237
+ """, search_term=search_term)
238
+ return result.data()
239
+
240
+ def find_imports(self, search_term: str) -> List[Dict]:
241
+ """Find imported symbols (aliases or original names)."""
242
+ with self.driver.session() as session:
243
+ result = session.run("""
244
+ MATCH (f:File)-[r:IMPORTS]->(m:Module)
245
+ WHERE r.alias = $search_term OR r.imported_name = $search_term
246
+ RETURN
247
+ r.alias as alias,
248
+ r.imported_name as imported_name,
249
+ m.name as module_name,
250
+ f.path as path,
251
+ r.line_number as line_number
252
+ ORDER BY f.path
253
+ LIMIT 20
254
+ """, search_term=search_term)
255
+ return result.data()
256
+
257
+ def find_related_code(self, user_query: str, fuzzy_search: bool, edit_distance: int, repo_path: Optional[str] = None) -> Dict[str, Any]:
258
+ """Find code related to a query using multiple search strategies"""
259
+ # Neo4j full-text uses Lucene fuzzy tokens (e.g. name:foo~2). Kùzu/FalkorDB use
260
+ # portable Levenshtein over candidate names instead.
261
+ lucene_fuzzy_query = (
262
+ " ".join(f"{t}~{edit_distance}" for t in user_query.split())
263
+ if fuzzy_search and not self._lacks_native_fulltext
264
+ else user_query
265
+ )
266
+ name_lookup_q = lucene_fuzzy_query if (fuzzy_search and not self._lacks_native_fulltext) else user_query
267
+ content_lookup_q = lucene_fuzzy_query if (fuzzy_search and not self._lacks_native_fulltext) else user_query
268
+
269
+ results: Dict[str, Any] = {
270
+ "query": lucene_fuzzy_query if fuzzy_search else user_query,
271
+ "functions_by_name": self.find_by_function_name(
272
+ name_lookup_q, fuzzy_search, repo_path, edit_distance
273
+ ),
274
+ "classes_by_name": self.find_by_class_name(
275
+ name_lookup_q, fuzzy_search, repo_path, edit_distance
276
+ ),
277
+ "variables_by_name": self.find_by_variable_name(user_query, repo_path), # no fuzzy for variables as they are not using full-text index
278
+ "content_matches": self.find_by_content(content_lookup_q, repo_path),
279
+ }
280
+
281
+ all_results: List[Dict[str, Any]] = []
282
+
283
+ for func in results["functions_by_name"]:
284
+ func["search_type"] = "function_name"
285
+ func["relevance_score"] = 0.9 if not func["is_dependency"] else 0.7
286
+ all_results.append(func)
287
+
288
+ for cls in results["classes_by_name"]:
289
+ cls["search_type"] = "class_name"
290
+ cls["relevance_score"] = 0.8 if not cls["is_dependency"] else 0.6
291
+ all_results.append(cls)
292
+
293
+ for var in results["variables_by_name"]:
294
+ var["search_type"] = "variable_name"
295
+ var["relevance_score"] = 0.7 if not var["is_dependency"] else 0.5
296
+ all_results.append(var)
297
+
298
+ for content in results["content_matches"]:
299
+ content["search_type"] = "content"
300
+ content["relevance_score"] = 0.6 if not content["is_dependency"] else 0.4
301
+ all_results.append(content)
302
+
303
+ all_results.sort(key=lambda x: x["relevance_score"], reverse=True)
304
+
305
+ results["ranked_results"] = all_results[:15]
306
+ results["total_matches"] = len(all_results)
307
+
308
+ return results
309
+
310
+ def find_functions_by_argument(self, argument_name: str, path: Optional[str] = None, repo_path: Optional[str] = None) -> List[Dict]:
311
+ """Find functions that take a specific argument name."""
312
+ with self.driver.session() as session:
313
+ repo_filter = "AND f.path STARTS WITH $repo_path" if repo_path else ""
314
+ if path:
315
+ query = f"""
316
+ MATCH (f:Function)-[:HAS_PARAMETER]->(p:Parameter)
317
+ WHERE p.name = $argument_name AND f.path = $path {repo_filter}
318
+ RETURN f.name AS function_name, f.path AS path, f.line_number AS line_number,
319
+ f.docstring AS docstring, f.is_dependency AS is_dependency
320
+ ORDER BY f.is_dependency ASC, f.path, f.line_number
321
+ LIMIT 20
322
+ """
323
+ result = session.run(query, argument_name=argument_name, path=path, repo_path=repo_path)
324
+ else:
325
+ query = f"""
326
+ MATCH (f:Function)-[:HAS_PARAMETER]->(p:Parameter)
327
+ WHERE p.name = $argument_name {repo_filter}
328
+ RETURN f.name AS function_name, f.path AS path, f.line_number AS line_number,
329
+ f.docstring AS docstring, f.is_dependency AS is_dependency
330
+ ORDER BY f.is_dependency ASC, f.path, f.line_number
331
+ LIMIT 20
332
+ """
333
+ result = session.run(query, argument_name=argument_name, repo_path=repo_path)
334
+ return result.data()
335
+
336
+ def find_functions_by_decorator(self, decorator_name: str, path: Optional[str] = None, repo_path: Optional[str] = None) -> List[Dict]:
337
+ """Find functions that have a specific decorator applied to them."""
338
+ with self.driver.session() as session:
339
+ repo_filter = "AND f.path STARTS WITH $repo_path" if repo_path else ""
340
+ if path:
341
+ query = f"""
342
+ MATCH (f:Function)
343
+ WHERE f.path = $path AND $decorator_name IN f.decorators {repo_filter}
344
+ RETURN f.name AS function_name, f.path AS path, f.line_number AS line_number,
345
+ f.docstring AS docstring, f.is_dependency AS is_dependency, f.decorators AS decorators
346
+ ORDER BY f.is_dependency ASC, f.path, f.line_number
347
+ LIMIT 20
348
+ """
349
+ result = session.run(query, decorator_name=decorator_name, path=path, repo_path=repo_path)
350
+ else:
351
+ query = f"""
352
+ MATCH (f:Function)
353
+ WHERE $decorator_name IN f.decorators {repo_filter}
354
+ RETURN f.name AS function_name, f.path AS path, f.line_number AS line_number,
355
+ f.docstring AS docstring, f.is_dependency AS is_dependency, f.decorators AS decorators
356
+ ORDER BY f.is_dependency ASC, f.path, f.line_number
357
+ LIMIT 20
358
+ """
359
+ result = session.run(query, decorator_name=decorator_name, repo_path=repo_path)
360
+ return result.data()
361
+
362
+ def who_calls_function(self, function_name: str, path: Optional[str] = None, repo_path: Optional[str] = None) -> List[Dict]:
363
+ """Find what functions call a specific function using CALLS relationships with improved matching"""
364
+ with self.driver.session() as session:
365
+ repo_filter = "AND caller.path STARTS WITH $repo_path" if repo_path else ""
366
+ if path:
367
+ result = session.run(f"""
368
+ MATCH (caller)-[call:CALLS]->(target:Function {{name: $function_name, path: $path}})
369
+ WHERE (caller:Function OR caller:Class OR caller:File) {repo_filter}
370
+ OPTIONAL MATCH (caller_file:File)-[:CONTAINS]->(caller)
371
+ RETURN DISTINCT
372
+ caller.name as caller_function,
373
+ COALESCE(caller.path, caller_file.path) as caller_file_path,
374
+ caller.line_number as caller_line_number,
375
+ caller.docstring as caller_docstring,
376
+ caller.is_dependency as caller_is_dependency,
377
+ call.line_number as call_line_number,
378
+ call.args as call_args,
379
+ call.full_call_name as full_call_name,
380
+ target.path as target_file_path
381
+ ORDER BY caller_is_dependency ASC, caller_file_path, caller_line_number
382
+ LIMIT 20
383
+ """, function_name=function_name, path=path, repo_path=repo_path)
384
+
385
+ results = result.data()
386
+ if not results:
387
+ result = session.run(f"""
388
+ MATCH (caller)-[call:CALLS]->(target:Function {{name: $function_name}})
389
+ WHERE (caller:Function OR caller:Class OR caller:File) {repo_filter}
390
+ OPTIONAL MATCH (caller_file:File)-[:CONTAINS]->(caller)
391
+ RETURN DISTINCT
392
+ caller.name as caller_function,
393
+ COALESCE(caller.path, caller_file.path) as caller_file_path,
394
+ caller.line_number as caller_line_number,
395
+ caller.docstring as caller_docstring,
396
+ caller.is_dependency as caller_is_dependency,
397
+ call.line_number as call_line_number,
398
+ call.args as call_args,
399
+ call.full_call_name as full_call_name,
400
+ target.path as target_file_path
401
+ ORDER BY caller_is_dependency ASC, caller_file_path, caller_line_number
402
+ LIMIT 20
403
+ """, function_name=function_name, repo_path=repo_path)
404
+ results = result.data()
405
+ else:
406
+ result = session.run(f"""
407
+ MATCH (caller:Function)-[call:CALLS]->(target:Function {{name: $function_name}})
408
+ WHERE 1=1 {repo_filter}
409
+ OPTIONAL MATCH (caller_file:File)-[:CONTAINS]->(caller)
410
+ RETURN DISTINCT
411
+ caller.name as caller_function,
412
+ caller.path as caller_file_path,
413
+ caller.line_number as caller_line_number,
414
+ caller.docstring as caller_docstring,
415
+ caller.is_dependency as caller_is_dependency,
416
+ call.line_number as call_line_number,
417
+ call.args as call_args,
418
+ call.full_call_name as full_call_name,
419
+ target.path as target_file_path
420
+ ORDER BY caller_is_dependency ASC, caller_file_path, caller_line_number
421
+ LIMIT 20
422
+ """, function_name=function_name, repo_path=repo_path)
423
+ results = result.data()
424
+
425
+ return results
426
+
427
+ def what_does_function_call(self, function_name: str, path: Optional[str] = None, repo_path: Optional[str] = None) -> List[Dict]:
428
+ """Find what functions a specific function calls using CALLS relationships"""
429
+ with self.driver.session() as session:
430
+ if path:
431
+ # Convert path to absolute path
432
+ absolute_file_path = str(Path(path).resolve())
433
+ result = session.run(f"""
434
+ MATCH (caller:Function {{name: $function_name, path: $absolute_file_path}})
435
+ MATCH (caller)-[call:CALLS]->(called:Function)
436
+ WHERE called.path STARTS WITH $repo_path OR $repo_path IS NULL
437
+ OPTIONAL MATCH (called_file:File)-[:CONTAINS]->(called)
438
+ RETURN DISTINCT
439
+ called.name as called_function,
440
+ called.path as called_file_path,
441
+ called.line_number as called_line_number,
442
+ called.docstring as called_docstring,
443
+ called.is_dependency as called_is_dependency,
444
+ call.line_number as call_line_number,
445
+ call.args as call_args,
446
+ call.full_call_name as full_call_name
447
+ ORDER BY called_is_dependency ASC, called_function
448
+ LIMIT 20
449
+ """, function_name=function_name, absolute_file_path=absolute_file_path, repo_path=repo_path)
450
+ else:
451
+ result = session.run(f"""
452
+ MATCH (caller:Function {{name: $function_name}})-[call:CALLS]->(called:Function)
453
+ WHERE called.path STARTS WITH $repo_path OR $repo_path IS NULL
454
+ OPTIONAL MATCH (called_file:File)-[:CONTAINS]->(called)
455
+ RETURN DISTINCT
456
+ called.name as called_function,
457
+ called.path as called_file_path,
458
+ called.line_number as called_line_number,
459
+ called.docstring as called_docstring,
460
+ called.is_dependency as called_is_dependency,
461
+ call.line_number as call_line_number,
462
+ call.args as call_args,
463
+ call.full_call_name as full_call_name
464
+ ORDER BY called_is_dependency ASC, called_function
465
+ LIMIT 20
466
+ """, function_name=function_name, repo_path=repo_path)
467
+
468
+ return result.data()
469
+
470
+ def who_imports_module(self, module_name: str, repo_path: Optional[str] = None) -> List[Dict]:
471
+ """Find what files import a specific module using IMPORTS relationships"""
472
+ with self.driver.session() as session:
473
+ repo_filter = "AND file.path STARTS WITH $repo_path" if repo_path else ""
474
+ result = session.run(f"""
475
+ MATCH (file:File)-[imp:IMPORTS]->(module:Module)
476
+ WHERE (module.name = $module_name OR module.full_import_name CONTAINS $module_name) {repo_filter}
477
+ OPTIONAL MATCH (repo:Repository)-[:CONTAINS]->(file)
478
+ WITH file, repo, COLLECT({{
479
+ imported_module: module.name,
480
+ import_alias: module.alias,
481
+ full_import_name: module.full_import_name
482
+ }}) AS imports
483
+ RETURN
484
+ file.name AS file_name,
485
+ file.path AS path,
486
+ file.relative_path AS file_relative_path,
487
+ file.is_dependency AS file_is_dependency,
488
+ repo.name AS repository_name,
489
+ imports
490
+ ORDER BY file.is_dependency ASC, file.path
491
+ LIMIT 20
492
+ """, module_name=module_name, repo_path=repo_path)
493
+
494
+ return result.data()
495
+
496
+ def who_modifies_variable(self, variable_name: str, repo_path: Optional[str] = None) -> List[Dict]:
497
+ """Find what functions contain or modify a specific variable"""
498
+ with self.driver.session() as session:
499
+ repo_filter = "AND container.path STARTS WITH $repo_path" if repo_path else ""
500
+ result = session.run(f"""
501
+ MATCH (var:Variable {{name: $variable_name}})
502
+ MATCH (container)-[:CONTAINS]->(var)
503
+ WHERE (container:Function OR container:Class OR container:File) {repo_filter}
504
+ OPTIONAL MATCH (file:File)-[:CONTAINS]->(container)
505
+ RETURN DISTINCT
506
+ CASE
507
+ WHEN container:Function THEN container.name
508
+ WHEN container:Class THEN container.name
509
+ ELSE 'file_level'
510
+ END as container_name,
511
+ CASE
512
+ WHEN container:Function THEN 'function'
513
+ WHEN container:Class THEN 'class'
514
+ ELSE 'file'
515
+ END as container_type,
516
+ COALESCE(container.path, file.path) as path,
517
+ container.line_number as container_line_number,
518
+ var.line_number as variable_line_number,
519
+ var.value as variable_value,
520
+ var.context as variable_context,
521
+ COALESCE(container.is_dependency, file.is_dependency, false) as is_dependency
522
+ ORDER BY is_dependency ASC, path, variable_line_number
523
+ LIMIT 20
524
+ """, variable_name=variable_name, repo_path=repo_path)
525
+
526
+ return result.data()
527
+
528
+ def find_class_hierarchy(self, class_name: str, path: Optional[str] = None, repo_path: Optional[str] = None) -> Dict[str, Any]:
529
+ """Find class inheritance relationships using INHERITS relationships"""
530
+ with self.driver.session() as session:
531
+ repo_filter = "AND parent.path STARTS WITH $repo_path" if repo_path else ""
532
+ if path:
533
+ match_clause = "MATCH (child:Class {name: $class_name, path: $path})"
534
+ else:
535
+ match_clause = "MATCH (child:Class {name: $class_name})"
536
+
537
+ parents_query = f"""
538
+ {match_clause}
539
+ MATCH (child)-[:INHERITS]->(parent:Class)
540
+ WHERE 1=1 {repo_filter}
541
+ OPTIONAL MATCH (parent_file:File)-[:CONTAINS]->(parent)
542
+ RETURN DISTINCT
543
+ parent.name as parent_class,
544
+ parent.path as parent_file_path,
545
+ parent.line_number as parent_line_number,
546
+ parent.docstring as parent_docstring,
547
+ parent.is_dependency as parent_is_dependency
548
+ ORDER BY parent_is_dependency ASC, parent_class
549
+ """
550
+ parents_result = session.run(parents_query, class_name=class_name, path=path, repo_path=repo_path)
551
+
552
+ repo_filter_child = "AND grandchild.path STARTS WITH $repo_path" if repo_path else ""
553
+ children_query = f"""
554
+ {match_clause}
555
+ MATCH (grandchild:Class)-[:INHERITS]->(child)
556
+ WHERE 1=1 {repo_filter_child}
557
+ OPTIONAL MATCH (child_file:File)-[:CONTAINS]->(grandchild)
558
+ RETURN DISTINCT
559
+ grandchild.name as child_class,
560
+ grandchild.path as child_file_path,
561
+ grandchild.line_number as child_line_number,
562
+ grandchild.docstring as child_docstring,
563
+ grandchild.is_dependency as child_is_dependency
564
+ ORDER BY child_is_dependency ASC, child_class
565
+ """
566
+ children_result = session.run(children_query, class_name=class_name, path=path, repo_path=repo_path)
567
+
568
+ repo_filter_method = "WHERE method.path STARTS WITH $repo_path" if repo_path else ""
569
+ methods_query = f"""
570
+ {match_clause}
571
+ MATCH (child)-[:CONTAINS]->(method:Function)
572
+ {repo_filter_method}
573
+ RETURN DISTINCT
574
+ method.name as method_name,
575
+ method.path as method_file_path,
576
+ method.line_number as method_line_number,
577
+ method.args as method_args,
578
+ method.docstring as method_docstring,
579
+ method.is_dependency as method_is_dependency
580
+ ORDER BY method_is_dependency ASC, method_line_number
581
+ """
582
+ methods_result = session.run(methods_query, class_name=class_name, path=path, repo_path=repo_path)
583
+
584
+ return {
585
+ "class_name": class_name,
586
+ "parent_classes": parents_result.data(),
587
+ "child_classes": children_result.data(),
588
+ "methods": methods_result.data()
589
+ }
590
+
591
+ def find_function_overrides(self, function_name: str, repo_path: Optional[str] = None) -> List[Dict]:
592
+ """Find all implementations of a function across different classes"""
593
+ with self.driver.session() as session:
594
+ repo_filter = "AND class.path STARTS WITH $repo_path" if repo_path else ""
595
+ result = session.run(f"""
596
+ MATCH (class:Class)-[:CONTAINS]->(func:Function {{name: $function_name}})
597
+ WHERE 1=1 {repo_filter}
598
+ OPTIONAL MATCH (file:File)-[:CONTAINS]->(class)
599
+ RETURN DISTINCT
600
+ class.name as class_name,
601
+ class.path as class_file_path,
602
+ func.name as function_name,
603
+ func.line_number as function_line_number,
604
+ func.args as function_args,
605
+ func.docstring as function_docstring,
606
+ func.is_dependency as is_dependency,
607
+ file.name as file_name
608
+ ORDER BY is_dependency ASC, class_name
609
+ LIMIT 20
610
+ """, function_name=function_name, repo_path=repo_path)
611
+
612
+ return result.data()
613
+
614
+ def find_dead_code(self, exclude_decorated_with: Optional[List[str]] = None, repo_path: Optional[str] = None) -> Dict[str, Any]:
615
+ """Find potentially unused functions (not called by other functions in the project), optionally excluding those with specific decorators."""
616
+ if exclude_decorated_with is None:
617
+ exclude_decorated_with = []
618
+
619
+ with self.driver.session() as session:
620
+ repo_filter = "AND func.path STARTS WITH $repo_path" if repo_path else ""
621
+ decorator_filter = "AND ALL(decorator_name IN $exclude_decorated_with WHERE NOT decorator_name IN func.decorators)" if exclude_decorated_with else ""
622
+ func_ignore = cypher_path_not_under_ignore_dirs("func.path")
623
+ caller_ignore = cypher_path_not_under_ignore_dirs("caller.path")
624
+
625
+ query = f"""
626
+ MATCH (func:Function)
627
+ WHERE func.is_dependency = false {repo_filter} {func_ignore}
628
+ AND NOT func.name IN ['main', 'setup', 'run']
629
+ AND NOT (func.name STARTS WITH '__' AND func.name ENDS WITH '__')
630
+ AND NOT func.name STARTS WITH '_test'
631
+ AND NOT func.name STARTS WITH 'test_'
632
+ AND NOT func.name CONTAINS 'main'
633
+ AND NOT toLower(func.name) CONTAINS 'application'
634
+ AND NOT toLower(func.name) CONTAINS 'entry'
635
+ AND NOT toLower(func.name) CONTAINS 'entrypoint'
636
+ {decorator_filter}
637
+ WITH func
638
+ OPTIONAL MATCH (caller:Function)-[:CALLS]->(func)
639
+ WHERE caller.is_dependency = false {caller_ignore}
640
+ WITH func, count(caller) as caller_count
641
+ WHERE caller_count = 0
642
+ OPTIONAL MATCH (file:File)-[:CONTAINS]->(func)
643
+ RETURN
644
+ func.name as function_name,
645
+ func.path as path,
646
+ func.line_number as line_number,
647
+ func.docstring as docstring,
648
+ func.context as context,
649
+ file.name as file_name
650
+ ORDER BY func.path, func.line_number
651
+ LIMIT 50
652
+ """
653
+
654
+ params = {}
655
+ if repo_path:
656
+ params["repo_path"] = repo_path
657
+ if exclude_decorated_with:
658
+ params["exclude_decorated_with"] = exclude_decorated_with
659
+
660
+ result = session.run(query, **params)
661
+
662
+ return {
663
+ "potentially_unused_functions": result.data(),
664
+ "note": "These functions might be unused, but could be entry points, callbacks, or called dynamically"
665
+ }
666
+
667
+ def find_all_callers(self, function_name: str, path: Optional[str] = None, repo_path: Optional[str] = None) -> List[Dict]:
668
+ """Find all direct and indirect callers of a specific function."""
669
+ with self.driver.session() as session:
670
+ repo_filter = "AND f.path STARTS WITH $repo_path" if repo_path else ""
671
+ if path:
672
+ # KùzuDB-compatible: Use anonymous end node and filter with WHERE
673
+ query = f"""
674
+ MATCH p = (f:Function)-[:CALLS*]->()
675
+ WITH f as f, p as p, nodes(p) as path_nodes
676
+ WITH f as f, path_nodes as path_nodes, path_nodes[size(path_nodes)] as target
677
+ WHERE target.name = $function_name AND target.path = $path {repo_filter}
678
+ RETURN DISTINCT f.name AS caller_name, f.path AS caller_file_path, f.line_number AS caller_line_number, f.is_dependency AS caller_is_dependency
679
+ ORDER BY caller_is_dependency ASC, caller_file_path, caller_line_number
680
+ LIMIT 50
681
+ """
682
+ result = session.run(query, function_name=function_name, path=path, repo_path=repo_path)
683
+ else:
684
+ # KùzuDB-compatible: Use anonymous end node and filter with WHERE
685
+ query = f"""
686
+ MATCH p = (f:Function)-[:CALLS*]->()
687
+ WITH f as f, p as p, nodes(p) as path_nodes
688
+ WITH f as f, path_nodes as path_nodes, path_nodes[size(path_nodes)] as target
689
+ WHERE target.name = $function_name {repo_filter}
690
+ RETURN DISTINCT f.name AS caller_name, f.path AS caller_file_path, f.line_number AS caller_line_number, f.is_dependency AS caller_is_dependency
691
+ ORDER BY caller_is_dependency ASC, caller_file_path, caller_line_number
692
+ LIMIT 50
693
+ """
694
+ result = session.run(query, function_name=function_name, repo_path=repo_path)
695
+ return result.data()
696
+
697
+ def find_all_callees(self, function_name: str, path: Optional[str] = None, repo_path: Optional[str] = None) -> List[Dict]:
698
+ """Find all direct and indirect callees of a specific function."""
699
+ with self.driver.session() as session:
700
+ repo_filter = "WHERE f.path STARTS WITH $repo_path" if repo_path else ""
701
+ if path:
702
+ # KùzuDB-compatible: Use anonymous end node and extract from path
703
+ query = f"""
704
+ MATCH (caller:Function {{name: $function_name, path: $path}})
705
+ MATCH p = (caller)-[:CALLS*]->()
706
+ WITH p as p, nodes(p) as path_nodes
707
+ WITH path_nodes[size(path_nodes)] as f
708
+ {repo_filter}
709
+ RETURN DISTINCT f.name AS callee_name, f.path AS callee_file_path, f.line_number AS callee_line_number, f.is_dependency AS callee_is_dependency
710
+ ORDER BY callee_is_dependency ASC, callee_file_path, callee_line_number
711
+ LIMIT 50
712
+ """
713
+ result = session.run(query, function_name=function_name, path=path, repo_path=repo_path)
714
+ else:
715
+ # KùzuDB-compatible: Use anonymous end node and extract from path
716
+ query = f"""
717
+ MATCH (caller:Function {{name: $function_name}})
718
+ MATCH p = (caller)-[:CALLS*]->()
719
+ WITH p as p, nodes(p) as path_nodes
720
+ WITH path_nodes[size(path_nodes)] as f
721
+ {repo_filter}
722
+ RETURN DISTINCT f.name AS callee_name, f.path AS callee_file_path, f.line_number AS callee_line_number, f.is_dependency AS callee_is_dependency
723
+ ORDER BY callee_is_dependency ASC, callee_file_path, callee_line_number
724
+ LIMIT 50
725
+ """
726
+ result = session.run(query, function_name=function_name, repo_path=repo_path)
727
+ return result.data()
728
+
729
+ def find_function_call_chain(self, start_function: str, end_function: str, max_depth: int = 5, start_file: Optional[str] = None, end_file: Optional[str] = None, repo_path: Optional[str] = None) -> List[Dict]:
730
+ """Find call chains between two functions"""
731
+ with self.driver.session() as session:
732
+ # Build match clauses based on whether files are specified
733
+ start_props = "{name: $start_function" + (", path: $start_file}" if start_file else "}")
734
+ end_props = "{name: $end_function" + (", path: $end_file}" if end_file else "}")
735
+
736
+ # KùzuDB-compatible: Use anonymous end node and filter
737
+ repo_filter = "WHERE 1=1 AND (start.path IS NULL OR start.path STARTS WITH $repo_path) AND (end_target.path IS NULL OR end_target.path STARTS WITH $repo_path)" if repo_path else ""
738
+ query = f"""
739
+ MATCH (start:Function {start_props}), (end_target:Function {end_props})
740
+ {repo_filter}
741
+ WITH start as start, end_target as end_target
742
+ MATCH path = (start)-[:CALLS*1..{max_depth}]->()
743
+ WITH path as path, end_target as end_target, nodes(path) as func_nodes, relationships(path) as call_rels
744
+ WITH path as path, func_nodes as func_nodes, call_rels as call_rels, end_target as end_target, func_nodes[size(func_nodes)] as path_end
745
+ WHERE path_end.name = end_target.name AND (end_target.path IS NULL OR path_end.path = end_target.path)
746
+ RETURN func_nodes as function_nodes, call_rels as call_nodes, size(call_rels) as chain_length
747
+ ORDER BY chain_length ASC
748
+ LIMIT 20
749
+ """
750
+
751
+ # Prepare parameters
752
+ params = {
753
+ "start_function": start_function,
754
+ "end_function": end_function,
755
+ "start_file": start_file,
756
+ "end_file": end_file,
757
+ "repo_path": repo_path
758
+ }
759
+
760
+ result = session.run(query, **params)
761
+
762
+ # Post-process Node/Rel objects into plain dicts so CLI output stays stable
763
+ rows = result.data()
764
+ transformed: List[Dict[str, Any]] = []
765
+ for row in rows:
766
+ func_nodes = row.get("function_nodes") or []
767
+ rel_nodes = row.get("call_nodes") or []
768
+ chain_len = row.get("chain_length", 0)
769
+
770
+ function_chain = []
771
+ for n in func_nodes:
772
+ # Depending on KùzuDB + driver wrapping, list elements can arrive
773
+ # either as Node/Rel objects or already-materialized dicts.
774
+ if isinstance(n, dict):
775
+ props = n
776
+ else:
777
+ props = None
778
+ try:
779
+ props = n.get_properties()
780
+ except Exception:
781
+ props = getattr(n, "properties", None)
782
+ if props is None:
783
+ props = {}
784
+ function_chain.append(
785
+ {
786
+ "name": props.get("name"),
787
+ "path": props.get("path"),
788
+ "line_number": props.get("line_number"),
789
+ "is_dependency": props.get("is_dependency"),
790
+ }
791
+ )
792
+
793
+ call_details = []
794
+ for r in rel_nodes:
795
+ if isinstance(r, dict):
796
+ props = r
797
+ else:
798
+ props = None
799
+ try:
800
+ props = r.get_properties()
801
+ except Exception:
802
+ props = getattr(r, "properties", None)
803
+ if props is None:
804
+ props = {}
805
+ call_details.append(
806
+ {
807
+ "call_line": props.get("line_number"),
808
+ "args": props.get("args"),
809
+ "full_call_name": props.get("full_call_name"),
810
+ }
811
+ )
812
+
813
+ transformed.append(
814
+ {
815
+ "function_chain": function_chain,
816
+ "call_details": call_details,
817
+ "chain_length": chain_len,
818
+ }
819
+ )
820
+
821
+ return transformed
822
+
823
+ def find_by_type(self, element_type: str, limit: int = 50) -> List[Dict]:
824
+ """Find all elements of a specific type (Function, Class, File, Module)."""
825
+ # Map input type to node label
826
+ type_map = {
827
+ "function": "Function",
828
+ "class": "Class",
829
+ "file": "File",
830
+ "module": "Module"
831
+ }
832
+ label = type_map.get(element_type.lower())
833
+
834
+ if not label:
835
+ return []
836
+
837
+ with self.driver.session() as session:
838
+ if label == "File":
839
+ query = f"""
840
+ MATCH (n:File)
841
+ RETURN n.name as name, n.path as path, n.is_dependency as is_dependency
842
+ ORDER BY n.path
843
+ LIMIT $limit
844
+ """
845
+ elif label == "Module":
846
+ query = f"""
847
+ MATCH (n:Module)
848
+ RETURN n.name as name, n.name as path, false as is_dependency
849
+ ORDER BY n.name
850
+ LIMIT $limit
851
+ """
852
+ else:
853
+ query = f"""
854
+ MATCH (n:{label})
855
+ RETURN n.name as name, n.path as path, n.line_number as line_number, n.is_dependency as is_dependency
856
+ ORDER BY is_dependency ASC, name
857
+ LIMIT $limit
858
+ """
859
+
860
+ result = session.run(query, limit=limit)
861
+ return result.data()
862
+
863
+ def find_module_dependencies(self, module_name: str, repo_path: Optional[str] = None) -> Dict[str, Any]:
864
+ """Find all dependencies and dependents of a module"""
865
+ with self.driver.session() as session:
866
+ repo_filter = "AND file.path STARTS WITH $repo_path" if repo_path else ""
867
+ # Find files that import this module (who imports this module)
868
+ importers_result = session.run(f"""
869
+ MATCH (file:File)-[imp:IMPORTS]->(module:Module {{name: $module_name}})
870
+ WHERE 1=1 {repo_filter}
871
+ OPTIONAL MATCH (repo:Repository)-[:CONTAINS]->(file)
872
+ RETURN DISTINCT
873
+ file.path as importer_file_path,
874
+ imp.line_number as import_line_number,
875
+ file.is_dependency as file_is_dependency,
876
+ repo.name as repository_name
877
+ ORDER BY file.is_dependency ASC, file.path
878
+ LIMIT 50
879
+ """, module_name=module_name, repo_path=repo_path)
880
+
881
+ # Find modules that are imported by files that also import the target module
882
+ # This helps understand what this module is typically used with
883
+ imports_result = session.run(f"""
884
+ MATCH (file:File)-[:IMPORTS]->(target_module:Module {{name: $module_name}})
885
+ MATCH (file)-[imp:IMPORTS]->(other_module:Module)
886
+ WHERE other_module <> target_module {repo_filter}
887
+ RETURN DISTINCT
888
+ other_module.name as imported_module,
889
+ imp.alias as import_alias
890
+ ORDER BY other_module.name
891
+ LIMIT 50
892
+ """, module_name=module_name, repo_path=repo_path)
893
+
894
+ return {
895
+ "module_name": module_name,
896
+ "importers": importers_result.data(),
897
+ "imports": imports_result.data()
898
+ }
899
+
900
+ def find_variable_usage_scope(self, variable_name: str, path: Optional[str] = None, repo_path: Optional[str] = None) -> Dict[str, Any]:
901
+ """Find the scope and usage patterns of a variable, optional file path filtering"""
902
+ with self.driver.session() as session:
903
+ repo_filter = "AND var.path STARTS WITH $repo_path" if repo_path else ""
904
+ path_filter = "(var.path ENDS WITH $path OR var.path = $path)" if path else "1=1"
905
+
906
+ # Two-pass approach for KuzuDB compatibility (doesn't support
907
+ # OPTIONAL MATCH referencing variables bound in a prior MATCH).
908
+ # Pass 1: variables WITH a container
909
+ contained = session.run(f"""
910
+ MATCH (container)-[:CONTAINS]->(var:Variable {{name: $variable_name}})
911
+ WHERE {path_filter} {repo_filter}
912
+ RETURN DISTINCT
913
+ var.name as variable_name,
914
+ var.value as variable_value,
915
+ var.line_number as line_number,
916
+ var.context as context,
917
+ var.path as path,
918
+ CASE
919
+ WHEN container:Function THEN 'function'
920
+ WHEN container:Class THEN 'class'
921
+ ELSE 'module'
922
+ END as scope_type,
923
+ CASE
924
+ WHEN container:Function THEN container.name
925
+ WHEN container:Class THEN container.name
926
+ ELSE 'module_level'
927
+ END as scope_name,
928
+ var.is_dependency as is_dependency
929
+ """, variable_name=variable_name, path=path, repo_path=repo_path)
930
+ instances = contained.data()
931
+
932
+ # Pass 2: variables WITHOUT any container (module-level)
933
+ try:
934
+ orphaned = session.run(f"""
935
+ MATCH (var:Variable {{name: $variable_name}})
936
+ WHERE {path_filter} {repo_filter}
937
+ AND NOT ()-[:CONTAINS]->(var)
938
+ RETURN DISTINCT
939
+ var.name as variable_name,
940
+ var.value as variable_value,
941
+ var.line_number as line_number,
942
+ var.context as context,
943
+ var.path as path,
944
+ 'module' as scope_type,
945
+ 'module_level' as scope_name,
946
+ var.is_dependency as is_dependency
947
+ """, variable_name=variable_name, path=path, repo_path=repo_path)
948
+ instances.extend(orphaned.data())
949
+ except Exception:
950
+ pass
951
+
952
+ instances.sort(key=lambda r: (
953
+ r.get("is_dependency") or False,
954
+ r.get("path") or "",
955
+ r.get("line_number") or 0,
956
+ ))
957
+
958
+ return {
959
+ "variable_name": variable_name,
960
+ "instances": instances,
961
+ }
962
+
963
+ def analyze_code_relationships(self, query_type: str, target: str, context: Optional[str] = None, repo_path: Optional[str] = None) -> Dict[str, Any]:
964
+ """Main method to analyze different types of code relationships with fixed return types"""
965
+ query_type = query_type.lower().strip()
966
+
967
+ try:
968
+ if query_type == "find_callers":
969
+ results = self.who_calls_function(target, context, repo_path=repo_path)
970
+ return {
971
+ "query_type": "find_callers", "target": target, "context": context, "results": results,
972
+ "summary": f"Found {len(results)} functions that call '{target}'"
973
+ }
974
+
975
+ elif query_type == "find_callees":
976
+ results = self.what_does_function_call(target, context, repo_path=repo_path)
977
+ return {
978
+ "query_type": "find_callees", "target": target, "context": context, "results": results,
979
+ "summary": f"Function '{target}' calls {len(results)} other functions"
980
+ }
981
+
982
+ elif query_type == "find_importers":
983
+ results = self.who_imports_module(target, repo_path=repo_path)
984
+ return {
985
+ "query_type": "find_importers", "target": target, "results": results,
986
+ "summary": f"Found {len(results)} files that import '{target}'"
987
+ }
988
+
989
+ elif query_type == "find_functions_by_argument":
990
+ results = self.find_functions_by_argument(target, context, repo_path=repo_path)
991
+ return {
992
+ "query_type": "find_functions_by_argument", "target": target, "context": context, "results": results,
993
+ "summary": f"Found {len(results)} functions that take '{target}' as an argument"
994
+ }
995
+
996
+ elif query_type == "find_functions_by_decorator":
997
+ results = self.find_functions_by_decorator(target, context, repo_path=repo_path)
998
+ return {
999
+ "query_type": "find_functions_by_decorator", "target": target, "context": context, "results": results,
1000
+ "summary": f"Found {len(results)} functions decorated with '{target}'"
1001
+ }
1002
+
1003
+ elif query_type in ["who_modifies", "modifies", "mutations", "changes", "variable_usage"]:
1004
+ results = self.who_modifies_variable(target, repo_path=repo_path)
1005
+ return {
1006
+ "query_type": "who_modifies", "target": target, "results": results,
1007
+ "summary": f"Found {len(results)} containers that hold variable '{target}'"
1008
+ }
1009
+
1010
+ elif query_type in ["class_hierarchy", "inheritance", "extends"]:
1011
+ results = self.find_class_hierarchy(target, context, repo_path=repo_path)
1012
+ return {
1013
+ "query_type": "class_hierarchy", "target": target, "results": results,
1014
+ "summary": f"Class '{target}' has {len(results['parent_classes'])} parents, {len(results['child_classes'])} children, and {len(results['methods'])} methods"
1015
+ }
1016
+
1017
+ elif query_type in ["overrides", "implementations", "polymorphism"]:
1018
+ results = self.find_function_overrides(target, repo_path=repo_path)
1019
+ return {
1020
+ "query_type": "overrides", "target": target, "results": results,
1021
+ "summary": f"Found {len(results)} implementations of function '{target}'"
1022
+ }
1023
+
1024
+ elif query_type in ["dead_code", "unused", "unreachable"]:
1025
+ results = self.find_dead_code(repo_path=repo_path)
1026
+ return {
1027
+ "query_type": "dead_code", "results": results,
1028
+ "summary": f"Found {len(results['potentially_unused_functions'])} potentially unused functions"
1029
+ }
1030
+
1031
+ elif query_type == "find_complexity":
1032
+ limit = int(context) if context and context.isdigit() else 10
1033
+ results = self.find_most_complex_functions(limit, repo_path=repo_path)
1034
+ return {
1035
+ "query_type": "find_complexity", "limit": limit, "results": results,
1036
+ "summary": f"Found the top {len(results)} most complex functions"
1037
+ }
1038
+
1039
+ elif query_type == "find_all_callers":
1040
+ results = self.find_all_callers(target, context, repo_path=repo_path)
1041
+ return {
1042
+ "query_type": "find_all_callers", "target": target, "context": context, "results": results,
1043
+ "summary": f"Found {len(results)} direct and indirect callers of '{target}'"
1044
+ }
1045
+
1046
+ elif query_type == "find_all_callees":
1047
+ results = self.find_all_callees(target, context, repo_path=repo_path)
1048
+ return {
1049
+ "query_type": "find_all_callees", "target": target, "context": context, "results": results,
1050
+ "summary": f"Found {len(results)} direct and indirect callees of '{target}'"
1051
+ }
1052
+
1053
+ elif query_type in ["call_chain", "path", "chain"]:
1054
+ if '->' in target:
1055
+ start_func, end_func = target.split('->', 1)
1056
+ # max_depth can be passed as context, default to 5 if not provided or invalid
1057
+ max_depth = int(context) if context and context.isdigit() else 5
1058
+ results = self.find_function_call_chain(start_func.strip(), end_func.strip(), max_depth, repo_path=repo_path)
1059
+ return {
1060
+ "query_type": "call_chain", "target": target, "results": results,
1061
+ "summary": f"Found {len(results)} call chains from '{start_func.strip()}' to '{end_func.strip()}' (max depth: {max_depth})"
1062
+ }
1063
+ else:
1064
+ return {
1065
+ "error": "For call_chain queries, use format 'start_function->end_function'",
1066
+ "example": "main->process_data"
1067
+ }
1068
+
1069
+ elif query_type in ["module_deps", "module_dependencies", "module_usage"]:
1070
+ results = self.find_module_dependencies(target, repo_path=repo_path)
1071
+ return {
1072
+ "query_type": "module_dependencies", "target": target, "results": results,
1073
+ "summary": f"Module '{target}' is imported by {len(results['importers'])} files"
1074
+ }
1075
+
1076
+ elif query_type in ["variable_scope", "var_scope", "variable_usage_scope"]:
1077
+ results = self.find_variable_usage_scope(target, repo_path=repo_path)
1078
+ return {
1079
+ "query_type": "variable_scope", "target": target, "results": results,
1080
+ "summary": f"Variable '{target}' has {len(results['instances'])} instances across different scopes"
1081
+ }
1082
+
1083
+ else:
1084
+ return {
1085
+ "error": f"Unknown query type: {query_type}",
1086
+ "supported_types": [
1087
+ "find_callers", "find_callees", "find_importers", "who_modifies",
1088
+ "class_hierarchy", "overrides", "dead_code", "call_chain",
1089
+ "module_deps", "variable_scope", "find_complexity"
1090
+ ]
1091
+ }
1092
+
1093
+ except Exception as e:
1094
+ return {
1095
+ "error": f"Error executing relationship query: {str(e)}",
1096
+ "query_type": query_type,
1097
+ "target": target
1098
+ }
1099
+
1100
+ def get_cyclomatic_complexity(self, function_name: str, path: Optional[str] = None, repo_path: Optional[str] = None) -> Optional[Dict]:
1101
+ """Get the cyclomatic complexity of a function."""
1102
+ with self.driver.session() as session:
1103
+ repo_filter = "AND f.path STARTS WITH $repo_path" if repo_path else ""
1104
+ if path:
1105
+ # Use ENDS WITH for flexible path matching, or exact match
1106
+ query = f"""
1107
+ MATCH (f:Function {{name: $function_name}})
1108
+ WHERE (f.path ENDS WITH $path OR f.path = $path) {repo_filter}
1109
+ RETURN f.name as function_name, f.cyclomatic_complexity as complexity,
1110
+ f.path as path, f.line_number as line_number
1111
+ """
1112
+ result = session.run(query, function_name=function_name, path=path, repo_path=repo_path)
1113
+ else:
1114
+ query = f"""
1115
+ MATCH (f:Function {{name: $function_name}})
1116
+ WHERE 1=1 {repo_filter}
1117
+ RETURN f.name as function_name, f.cyclomatic_complexity as complexity,
1118
+ f.path as path, f.line_number as line_number
1119
+ """
1120
+ result = session.run(query, function_name=function_name, repo_path=repo_path)
1121
+
1122
+ result_data = result.data()
1123
+ if result_data:
1124
+ return result_data[0]
1125
+ return None
1126
+
1127
+ def find_most_complex_functions(self, limit: int = 10, repo_path: Optional[str] = None) -> List[Dict]:
1128
+ """Find the most complex functions based on cyclomatic complexity."""
1129
+ with self.driver.session() as session:
1130
+ repo_filter = "AND f.path STARTS WITH $repo_path" if repo_path else ""
1131
+ path_ignore = cypher_path_not_under_ignore_dirs("f.path")
1132
+ query = f"""
1133
+ MATCH (f:Function)
1134
+ WHERE f.cyclomatic_complexity IS NOT NULL AND f.is_dependency = false {repo_filter} {path_ignore}
1135
+ RETURN f.name as function_name, f.path as path, f.cyclomatic_complexity as complexity, f.line_number as line_number
1136
+ ORDER BY f.cyclomatic_complexity DESC
1137
+ LIMIT $limit
1138
+ """
1139
+ result = session.run(query, limit=limit, repo_path=repo_path)
1140
+ return result.data()
1141
+
1142
+ def list_indexed_repositories(self) -> List[Dict]:
1143
+ """List all indexed repositories."""
1144
+ with self.driver.session() as session:
1145
+ result = session.run("""
1146
+ MATCH (r:Repository)
1147
+ RETURN r.name as name, r.path as path, r.is_dependency as is_dependency
1148
+ ORDER BY r.name
1149
+ """)
1150
+ rows = result.data()
1151
+ bad = [r for r in rows if r.get("path") in (None, "")]
1152
+ if bad:
1153
+ logger.warning(
1154
+ "Found %s Repository record(s) with missing path in the graph; "
1155
+ "they are ignored when matching filesystem paths. If this persists, "
1156
+ "remove stale Repository nodes (e.g. Neo4j: "
1157
+ "MATCH (r:Repository) WHERE r.path IS NULL DETACH DELETE r) and re-index.",
1158
+ len(bad),
1159
+ )
1160
+ return rows