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.
- codegraphcontext/__init__.py +2 -0
- codegraphcontext/__main__.py +5 -0
- codegraphcontext/_cgc_rust.cp311-win_amd64.pyd +0 -0
- codegraphcontext/cli/__init__.py +1 -0
- codegraphcontext/cli/cli_helpers.py +774 -0
- codegraphcontext/cli/config_manager.py +1051 -0
- codegraphcontext/cli/main.py +2386 -0
- codegraphcontext/cli/registry_commands.py +404 -0
- codegraphcontext/cli/setup_macos.py +93 -0
- codegraphcontext/cli/setup_wizard.py +991 -0
- codegraphcontext/cli/visualizer.py +50 -0
- codegraphcontext/core/__init__.py +165 -0
- codegraphcontext/core/bundle_registry.py +181 -0
- codegraphcontext/core/cgc_bundle.py +858 -0
- codegraphcontext/core/cgcignore.py +118 -0
- codegraphcontext/core/database.py +273 -0
- codegraphcontext/core/database_falkordb.py +480 -0
- codegraphcontext/core/database_falkordb_remote.py +199 -0
- codegraphcontext/core/database_kuzu.py +636 -0
- codegraphcontext/core/falkor_worker.py +132 -0
- codegraphcontext/core/jobs.py +132 -0
- codegraphcontext/core/watcher.py +260 -0
- codegraphcontext/prompts.py +125 -0
- codegraphcontext/server.py +471 -0
- codegraphcontext/tool_definitions.py +220 -0
- codegraphcontext/tools/__init__.py +0 -0
- codegraphcontext/tools/advanced_language_query_tool.py +103 -0
- codegraphcontext/tools/code_finder.py +1160 -0
- codegraphcontext/tools/graph_builder.py +337 -0
- codegraphcontext/tools/handlers/analysis_handlers.py +114 -0
- codegraphcontext/tools/handlers/indexing_handlers.py +116 -0
- codegraphcontext/tools/handlers/management_handlers.py +339 -0
- codegraphcontext/tools/handlers/query_handlers.py +83 -0
- codegraphcontext/tools/handlers/watcher_handlers.py +83 -0
- codegraphcontext/tools/indexing/__init__.py +1 -0
- codegraphcontext/tools/indexing/constants.py +25 -0
- codegraphcontext/tools/indexing/discovery.py +64 -0
- codegraphcontext/tools/indexing/engine.py +109 -0
- codegraphcontext/tools/indexing/persistence/__init__.py +3 -0
- codegraphcontext/tools/indexing/persistence/parquet_loader.py +643 -0
- codegraphcontext/tools/indexing/persistence/writer.py +1114 -0
- codegraphcontext/tools/indexing/pipeline.py +171 -0
- codegraphcontext/tools/indexing/pre_scan.py +105 -0
- codegraphcontext/tools/indexing/resolution/__init__.py +8 -0
- codegraphcontext/tools/indexing/resolution/calls.py +253 -0
- codegraphcontext/tools/indexing/resolution/inheritance.py +91 -0
- codegraphcontext/tools/indexing/sanitize.py +41 -0
- codegraphcontext/tools/indexing/schema.py +79 -0
- codegraphcontext/tools/indexing/schema_contract.py +44 -0
- codegraphcontext/tools/indexing/scip_pipeline.py +140 -0
- codegraphcontext/tools/languages/c.py +563 -0
- codegraphcontext/tools/languages/cpp.py +616 -0
- codegraphcontext/tools/languages/csharp.py +551 -0
- codegraphcontext/tools/languages/dart.py +378 -0
- codegraphcontext/tools/languages/elixir.py +460 -0
- codegraphcontext/tools/languages/go.py +499 -0
- codegraphcontext/tools/languages/haskell.py +426 -0
- codegraphcontext/tools/languages/java.py +500 -0
- codegraphcontext/tools/languages/javascript.py +589 -0
- codegraphcontext/tools/languages/kotlin.py +640 -0
- codegraphcontext/tools/languages/perl.py +260 -0
- codegraphcontext/tools/languages/php.py +519 -0
- codegraphcontext/tools/languages/python.py +576 -0
- codegraphcontext/tools/languages/ruby.py +537 -0
- codegraphcontext/tools/languages/rust.py +295 -0
- codegraphcontext/tools/languages/scala.py +519 -0
- codegraphcontext/tools/languages/swift.py +491 -0
- codegraphcontext/tools/languages/typescript.py +575 -0
- codegraphcontext/tools/languages/typescriptjsx.py +151 -0
- codegraphcontext/tools/package_resolver.py +472 -0
- codegraphcontext/tools/query_tool_languages/c_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/cpp_toolkit.py +87 -0
- codegraphcontext/tools/query_tool_languages/csharp_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/dart_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/go_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/haskell_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/java_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/javascript_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/perl_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/python_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/ruby_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/rust_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/scala_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/swift_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/typescript_toolkit.py +5 -0
- codegraphcontext/tools/scip_indexer.py +467 -0
- codegraphcontext/tools/scip_pb2.py +2455 -0
- codegraphcontext/tools/system.py +133 -0
- codegraphcontext/tools/tree_sitter_parser.py +103 -0
- codegraphcontext/utils/debug_log.py +90 -0
- codegraphcontext/utils/path_ignore.py +55 -0
- codegraphcontext/utils/repo_path.py +27 -0
- codegraphcontext/utils/tree_sitter_manager.py +285 -0
- codegraphcontext/utils/visualize_graph.py +0 -0
- codegraphcontext/viz/server.py +282 -0
- codegraphcontext_rust-0.5.5.dist-info/METADATA +537 -0
- codegraphcontext_rust-0.5.5.dist-info/RECORD +101 -0
- codegraphcontext_rust-0.5.5.dist-info/WHEEL +4 -0
- codegraphcontext_rust-0.5.5.dist-info/entry_points.txt +3 -0
- codegraphcontext_rust-0.5.5.dist-info/licenses/LICENSE +21 -0
- 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
|