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,337 @@
|
|
|
1
|
+
|
|
2
|
+
# src/codegraphcontext/tools/graph_builder.py
|
|
3
|
+
"""Facade for graph indexing; implementation lives in indexing/."""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Dict, Optional, Tuple
|
|
9
|
+
|
|
10
|
+
from ..cli.config_manager import get_config_value
|
|
11
|
+
from ..core.database import DatabaseManager
|
|
12
|
+
from ..core.jobs import JobManager, JobStatus
|
|
13
|
+
from ..utils.debug_log import debug_log, error_logger, info_logger, warning_logger
|
|
14
|
+
from .indexing.constants import DEFAULT_IGNORE_PATTERNS
|
|
15
|
+
from .indexing.persistence.writer import GraphWriter
|
|
16
|
+
from .indexing.pipeline import run_tree_sitter_index_async
|
|
17
|
+
from .indexing.pre_scan import pre_scan_for_imports
|
|
18
|
+
from .indexing.resolution.calls import build_function_call_groups, resolve_function_call
|
|
19
|
+
from .indexing.resolution.inheritance import build_inheritance_and_csharp_files
|
|
20
|
+
from .indexing.sanitize import MAX_STR_LEN, sanitize_props
|
|
21
|
+
from .indexing.schema import create_graph_schema
|
|
22
|
+
from .indexing.scip_pipeline import name_from_symbol, run_scip_index_async
|
|
23
|
+
from .tree_sitter_parser import TreeSitterParser
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class GraphBuilder:
|
|
27
|
+
"""Module for building and managing the code graph (Neo4j / Falkor / Kùzu)."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, db_manager: DatabaseManager, job_manager: JobManager, loop: asyncio.AbstractEventLoop):
|
|
30
|
+
self.db_manager = db_manager
|
|
31
|
+
self.job_manager = job_manager
|
|
32
|
+
self.loop = loop
|
|
33
|
+
self.driver = self.db_manager.get_driver()
|
|
34
|
+
self._writer = GraphWriter(self.driver)
|
|
35
|
+
self.parsers = {
|
|
36
|
+
".py": "python",
|
|
37
|
+
".ipynb": "python",
|
|
38
|
+
".js": "javascript",
|
|
39
|
+
".jsx": "javascript",
|
|
40
|
+
".mjs": "javascript",
|
|
41
|
+
".cjs": "javascript",
|
|
42
|
+
".go": "go",
|
|
43
|
+
".ts": "typescript",
|
|
44
|
+
".tsx": "tsx",
|
|
45
|
+
".cpp": "cpp",
|
|
46
|
+
".h": "cpp",
|
|
47
|
+
".hpp": "cpp",
|
|
48
|
+
".hh": "cpp",
|
|
49
|
+
".rs": "rust",
|
|
50
|
+
".c": "c",
|
|
51
|
+
".java": "java",
|
|
52
|
+
".rb": "ruby",
|
|
53
|
+
".cs": "c_sharp",
|
|
54
|
+
".php": "php",
|
|
55
|
+
".kt": "kotlin",
|
|
56
|
+
".scala": "scala",
|
|
57
|
+
".sc": "scala",
|
|
58
|
+
".swift": "swift",
|
|
59
|
+
".hs": "haskell",
|
|
60
|
+
".dart": "dart",
|
|
61
|
+
".pl": "perl",
|
|
62
|
+
".pm": "perl",
|
|
63
|
+
".ex": "elixir",
|
|
64
|
+
".exs": "elixir",
|
|
65
|
+
}
|
|
66
|
+
self._parsed_cache = {}
|
|
67
|
+
self.create_schema()
|
|
68
|
+
|
|
69
|
+
def get_parser(self, extension: str) -> Optional[TreeSitterParser]:
|
|
70
|
+
"""Gets or creates a TreeSitterParser for the given extension."""
|
|
71
|
+
lang_name = self.parsers.get(extension)
|
|
72
|
+
if not lang_name:
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
if lang_name not in self._parsed_cache:
|
|
76
|
+
try:
|
|
77
|
+
self._parsed_cache[lang_name] = TreeSitterParser(lang_name)
|
|
78
|
+
except Exception as e:
|
|
79
|
+
warning_logger(f"Failed to initialize parser for {lang_name}: {e}")
|
|
80
|
+
return None
|
|
81
|
+
return self._parsed_cache[lang_name]
|
|
82
|
+
|
|
83
|
+
def create_schema(self) -> None:
|
|
84
|
+
create_graph_schema(self.driver, self.db_manager)
|
|
85
|
+
|
|
86
|
+
_MAX_STR_LEN = MAX_STR_LEN
|
|
87
|
+
|
|
88
|
+
@staticmethod
|
|
89
|
+
def _sanitize_props(props: Dict) -> Dict:
|
|
90
|
+
return sanitize_props(props)
|
|
91
|
+
|
|
92
|
+
def _resolve_function_call(
|
|
93
|
+
self,
|
|
94
|
+
call: Dict,
|
|
95
|
+
caller_file_path: str,
|
|
96
|
+
local_names: set,
|
|
97
|
+
local_imports: dict,
|
|
98
|
+
imports_map: dict,
|
|
99
|
+
skip_external: bool,
|
|
100
|
+
):
|
|
101
|
+
return resolve_function_call(
|
|
102
|
+
call, caller_file_path, local_names, local_imports, imports_map, skip_external
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
def pre_scan_imports(self, files: list[Path]) -> dict:
|
|
106
|
+
"""Build global imports_map from language pre-scans (public API for watchers/pipeline)."""
|
|
107
|
+
return pre_scan_for_imports(files, self.parsers.keys(), self.get_parser)
|
|
108
|
+
|
|
109
|
+
def _pre_scan_for_imports(self, files: list[Path]) -> dict:
|
|
110
|
+
return self.pre_scan_imports(files)
|
|
111
|
+
|
|
112
|
+
def add_repository_to_graph(self, repo_path: Path, is_dependency: bool = False) -> None:
|
|
113
|
+
self._writer.add_repository_to_graph(repo_path, is_dependency)
|
|
114
|
+
|
|
115
|
+
def add_file_to_graph(
|
|
116
|
+
self, file_data: Dict, repo_name: str, imports_map: dict, repo_path_str: str = None
|
|
117
|
+
) -> None:
|
|
118
|
+
self._writer.add_file_to_graph(file_data, repo_name, imports_map, repo_path_str=repo_path_str)
|
|
119
|
+
|
|
120
|
+
def link_function_calls(
|
|
121
|
+
self,
|
|
122
|
+
all_file_data: list[Dict],
|
|
123
|
+
imports_map: dict,
|
|
124
|
+
file_class_lookup: Optional[Dict[str, set]] = None,
|
|
125
|
+
) -> None:
|
|
126
|
+
"""Resolve and persist CALLS relationships (public API)."""
|
|
127
|
+
groups = build_function_call_groups(all_file_data, imports_map, file_class_lookup)
|
|
128
|
+
self._writer.write_function_call_groups(*groups)
|
|
129
|
+
|
|
130
|
+
def _create_all_function_calls(
|
|
131
|
+
self, all_file_data: list[Dict], imports_map: dict, file_class_lookup: Optional[Dict[str, set]] = None
|
|
132
|
+
) -> None:
|
|
133
|
+
self.link_function_calls(all_file_data, imports_map, file_class_lookup)
|
|
134
|
+
|
|
135
|
+
def link_inheritance(self, all_file_data: list[Dict], imports_map: dict) -> None:
|
|
136
|
+
"""Resolve and persist INHERITS / C# IMPLEMENTS (public API)."""
|
|
137
|
+
info_logger(f"[INHERITS] Resolving inheritance links across {len(all_file_data)} files...")
|
|
138
|
+
inheritance_batch, csharp_files = build_inheritance_and_csharp_files(all_file_data, imports_map)
|
|
139
|
+
self._writer.write_inheritance_links(inheritance_batch, csharp_files, imports_map)
|
|
140
|
+
|
|
141
|
+
def _create_all_inheritance_links(self, all_file_data: list[Dict], imports_map: dict) -> None:
|
|
142
|
+
self.link_inheritance(all_file_data, imports_map)
|
|
143
|
+
|
|
144
|
+
def delete_file_from_graph(self, path: str) -> None:
|
|
145
|
+
self._writer.delete_file_from_graph(path)
|
|
146
|
+
|
|
147
|
+
def delete_repository_from_graph(self, repo_path: str) -> bool:
|
|
148
|
+
return self._writer.delete_repository_from_graph(repo_path)
|
|
149
|
+
|
|
150
|
+
def get_caller_file_paths(self, file_path_str: str) -> set:
|
|
151
|
+
return self._writer.get_caller_file_paths(file_path_str)
|
|
152
|
+
|
|
153
|
+
def get_inheritance_neighbor_paths(self, file_path_str: str) -> set:
|
|
154
|
+
return self._writer.get_inheritance_neighbor_paths(file_path_str)
|
|
155
|
+
|
|
156
|
+
def delete_outgoing_calls_from_files(self, file_paths: list) -> None:
|
|
157
|
+
self._writer.delete_outgoing_calls_from_files(file_paths)
|
|
158
|
+
|
|
159
|
+
def delete_inherits_for_files(self, file_paths: list) -> None:
|
|
160
|
+
self._writer.delete_inherits_for_files(file_paths)
|
|
161
|
+
|
|
162
|
+
def get_repo_class_lookup(self, repo_path: Path) -> dict:
|
|
163
|
+
return self._writer.get_repo_class_lookup(repo_path)
|
|
164
|
+
|
|
165
|
+
def delete_relationship_links(self, repo_path: Path) -> None:
|
|
166
|
+
self._writer.delete_relationship_links(repo_path)
|
|
167
|
+
|
|
168
|
+
def update_file_in_graph(self, path: Path, repo_path: Path, imports_map: dict):
|
|
169
|
+
file_path_str = str(path.resolve())
|
|
170
|
+
repo_name = repo_path.name
|
|
171
|
+
|
|
172
|
+
self.delete_file_from_graph(file_path_str)
|
|
173
|
+
|
|
174
|
+
if path.exists():
|
|
175
|
+
file_data = self.parse_file(repo_path, path)
|
|
176
|
+
|
|
177
|
+
if "error" not in file_data:
|
|
178
|
+
self.add_file_to_graph(file_data, repo_name, imports_map)
|
|
179
|
+
return file_data
|
|
180
|
+
error_logger(f"Skipping graph add for {file_path_str} due to parsing error: {file_data['error']}")
|
|
181
|
+
return None
|
|
182
|
+
return {"deleted": True, "path": file_path_str}
|
|
183
|
+
|
|
184
|
+
def parse_file(self, repo_path: Path, path: Path, is_dependency: bool = False) -> Dict:
|
|
185
|
+
lang_name = self.parsers.get(path.suffix)
|
|
186
|
+
if not lang_name:
|
|
187
|
+
warning_logger(f"No parser found for file extension {path.suffix}. Skipping {path}")
|
|
188
|
+
return {"path": str(path), "error": f"No parser for {path.suffix}", "unsupported": True}
|
|
189
|
+
|
|
190
|
+
# Try Rust engine first (faster, no Python overhead)
|
|
191
|
+
try:
|
|
192
|
+
from .indexing.engine import RUST_AVAILABLE, _RUST_SUPPORTED_LANGS, _rust_parse_file
|
|
193
|
+
if RUST_AVAILABLE and lang_name in _RUST_SUPPORTED_LANGS and path.suffix != ".ipynb":
|
|
194
|
+
index_source = (get_config_value("INDEX_SOURCE") or "false").lower() == "true"
|
|
195
|
+
file_data = _rust_parse_file(str(path), lang_name, is_dependency, index_source)
|
|
196
|
+
if "error" not in file_data:
|
|
197
|
+
file_data["repo_path"] = str(repo_path)
|
|
198
|
+
return file_data
|
|
199
|
+
except Exception:
|
|
200
|
+
pass # Fall through to Python parser
|
|
201
|
+
|
|
202
|
+
# Python fallback (for .ipynb, unsupported langs, or Rust errors)
|
|
203
|
+
parser = self.get_parser(path.suffix)
|
|
204
|
+
if not parser:
|
|
205
|
+
return {"path": str(path), "error": f"No parser for {path.suffix}", "unsupported": True}
|
|
206
|
+
|
|
207
|
+
debug_log(f"[parse_file] Starting parsing for: {path} with {parser.language_name} parser")
|
|
208
|
+
try:
|
|
209
|
+
index_source = (get_config_value("INDEX_SOURCE") or "false").lower() == "true"
|
|
210
|
+
if parser.language_name == "python":
|
|
211
|
+
is_notebook = path.suffix == ".ipynb"
|
|
212
|
+
file_data = parser.parse(
|
|
213
|
+
path,
|
|
214
|
+
is_dependency,
|
|
215
|
+
is_notebook=is_notebook,
|
|
216
|
+
index_source=index_source,
|
|
217
|
+
)
|
|
218
|
+
else:
|
|
219
|
+
file_data = parser.parse(path, is_dependency, index_source=index_source)
|
|
220
|
+
file_data["repo_path"] = str(repo_path)
|
|
221
|
+
return file_data
|
|
222
|
+
except Exception as e:
|
|
223
|
+
error_logger(f"Error parsing {path} with {parser.language_name} parser: {e}")
|
|
224
|
+
debug_log(f"[parse_file] Error parsing {path}: {e}")
|
|
225
|
+
return {"path": str(path), "error": str(e)}
|
|
226
|
+
|
|
227
|
+
def estimate_processing_time(self, path: Path) -> Optional[Tuple[int, float]]:
|
|
228
|
+
try:
|
|
229
|
+
supported_extensions = self.parsers.keys()
|
|
230
|
+
if path.is_file():
|
|
231
|
+
if path.suffix in supported_extensions:
|
|
232
|
+
files = [path]
|
|
233
|
+
else:
|
|
234
|
+
return 0, 0.0
|
|
235
|
+
else:
|
|
236
|
+
all_files = path.rglob("*")
|
|
237
|
+
files = [f for f in all_files if f.is_file() and f.suffix in supported_extensions]
|
|
238
|
+
|
|
239
|
+
ignore_dirs_str = get_config_value("IGNORE_DIRS") or ""
|
|
240
|
+
if ignore_dirs_str:
|
|
241
|
+
ignore_dirs = {d.strip().lower() for d in ignore_dirs_str.split(",") if d.strip()}
|
|
242
|
+
if ignore_dirs:
|
|
243
|
+
kept_files = []
|
|
244
|
+
for f in files:
|
|
245
|
+
try:
|
|
246
|
+
parts = set(p.lower() for p in f.relative_to(path).parent.parts)
|
|
247
|
+
if not parts.intersection(ignore_dirs):
|
|
248
|
+
kept_files.append(f)
|
|
249
|
+
except ValueError:
|
|
250
|
+
kept_files.append(f)
|
|
251
|
+
files = kept_files
|
|
252
|
+
|
|
253
|
+
total_files = len(files)
|
|
254
|
+
estimated_time = total_files * 0.05
|
|
255
|
+
return total_files, estimated_time
|
|
256
|
+
except Exception as e:
|
|
257
|
+
error_logger(f"Could not estimate processing time for {path}: {e}")
|
|
258
|
+
return None
|
|
259
|
+
|
|
260
|
+
async def _build_graph_from_scip(
|
|
261
|
+
self, path: Path, is_dependency: bool, job_id: Optional[str], lang: str
|
|
262
|
+
):
|
|
263
|
+
from . import scip_indexer
|
|
264
|
+
|
|
265
|
+
await run_scip_index_async(
|
|
266
|
+
path,
|
|
267
|
+
is_dependency,
|
|
268
|
+
job_id,
|
|
269
|
+
lang,
|
|
270
|
+
self._writer,
|
|
271
|
+
self.job_manager,
|
|
272
|
+
self.parsers.keys(),
|
|
273
|
+
self.get_parser,
|
|
274
|
+
scip_indexer,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
def _name_from_symbol(self, symbol: str) -> str:
|
|
278
|
+
return name_from_symbol(symbol)
|
|
279
|
+
|
|
280
|
+
async def build_graph_from_path_async(
|
|
281
|
+
self, path: Path, is_dependency: bool = False, job_id: str = None, cgcignore_path: str = None
|
|
282
|
+
):
|
|
283
|
+
try:
|
|
284
|
+
scip_enabled = (get_config_value("SCIP_INDEXER") or "false").lower() == "true"
|
|
285
|
+
if scip_enabled:
|
|
286
|
+
from .scip_indexer import detect_project_lang, is_scip_available
|
|
287
|
+
|
|
288
|
+
scip_langs_str = get_config_value("SCIP_LANGUAGES") or "python,typescript,go,rust,java"
|
|
289
|
+
scip_languages = [l.strip() for l in scip_langs_str.split(",") if l.strip()]
|
|
290
|
+
detected_lang = detect_project_lang(path, scip_languages)
|
|
291
|
+
|
|
292
|
+
if detected_lang and is_scip_available(detected_lang):
|
|
293
|
+
info_logger(f"SCIP_INDEXER=true — using SCIP for language: {detected_lang}")
|
|
294
|
+
await self._build_graph_from_scip(path, is_dependency, job_id, detected_lang)
|
|
295
|
+
return
|
|
296
|
+
if detected_lang:
|
|
297
|
+
warning_logger(
|
|
298
|
+
f"SCIP_INDEXER=true but scip-{detected_lang} binary not found. "
|
|
299
|
+
f"Falling back to Tree-sitter. Install it first."
|
|
300
|
+
)
|
|
301
|
+
else:
|
|
302
|
+
info_logger(
|
|
303
|
+
"SCIP_INDEXER=true but no SCIP-supported language detected. "
|
|
304
|
+
"Falling back to Tree-sitter."
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
await run_tree_sitter_index_async(
|
|
308
|
+
path,
|
|
309
|
+
is_dependency,
|
|
310
|
+
job_id,
|
|
311
|
+
cgcignore_path,
|
|
312
|
+
self._writer,
|
|
313
|
+
self.job_manager,
|
|
314
|
+
self.parsers,
|
|
315
|
+
self.get_parser,
|
|
316
|
+
self.parse_file,
|
|
317
|
+
self.add_minimal_file_node,
|
|
318
|
+
)
|
|
319
|
+
except Exception as e:
|
|
320
|
+
error_message = str(e)
|
|
321
|
+
error_logger(f"Failed to build graph for path {path}: {error_message}")
|
|
322
|
+
if job_id:
|
|
323
|
+
if (
|
|
324
|
+
"no such file found" in error_message
|
|
325
|
+
or "deleted" in error_message
|
|
326
|
+
or "not found" in error_message
|
|
327
|
+
):
|
|
328
|
+
status = JobStatus.CANCELLED
|
|
329
|
+
else:
|
|
330
|
+
status = JobStatus.FAILED
|
|
331
|
+
|
|
332
|
+
self.job_manager.update_job(
|
|
333
|
+
job_id, status=status, end_time=datetime.now(), errors=[str(e)]
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
def add_minimal_file_node(self, file_path: Path, repo_path: Path, is_dependency: bool = False) -> None:
|
|
337
|
+
self._writer.add_minimal_file_node(file_path, repo_path, is_dependency)
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
from typing import Any, Dict
|
|
2
|
+
from ..code_finder import CodeFinder
|
|
3
|
+
from ...utils.debug_log import debug_log
|
|
4
|
+
|
|
5
|
+
def find_dead_code(code_finder: CodeFinder, **args) -> Dict[str, Any]:
|
|
6
|
+
"""Tool to find potentially dead code across the entire project."""
|
|
7
|
+
exclude_decorated_with = args.get("exclude_decorated_with", [])
|
|
8
|
+
repo_path = args.get("repo_path")
|
|
9
|
+
try:
|
|
10
|
+
debug_log(f"Finding dead code. repo_path={repo_path}")
|
|
11
|
+
results = code_finder.find_dead_code(exclude_decorated_with=exclude_decorated_with, repo_path=repo_path)
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
"success": True,
|
|
15
|
+
"query_type": "dead_code",
|
|
16
|
+
"results": results
|
|
17
|
+
}
|
|
18
|
+
except Exception as e:
|
|
19
|
+
debug_log(f"Error finding dead code: {str(e)}")
|
|
20
|
+
return {"error": f"Failed to find dead code: {str(e)}"}
|
|
21
|
+
|
|
22
|
+
def calculate_cyclomatic_complexity(code_finder: CodeFinder, **args) -> Dict[str, Any]:
|
|
23
|
+
"""Tool to calculate cyclomatic complexity for a given function."""
|
|
24
|
+
function_name = args.get("function_name")
|
|
25
|
+
path = args.get("path")
|
|
26
|
+
repo_path = args.get("repo_path")
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
debug_log(f"Calculating cyclomatic complexity for function: {function_name}, repo_path={repo_path}")
|
|
30
|
+
results = code_finder.get_cyclomatic_complexity(function_name, path, repo_path=repo_path)
|
|
31
|
+
|
|
32
|
+
response = {
|
|
33
|
+
"success": True,
|
|
34
|
+
"function_name": function_name,
|
|
35
|
+
"results": results
|
|
36
|
+
}
|
|
37
|
+
if path:
|
|
38
|
+
response["path"] = path
|
|
39
|
+
|
|
40
|
+
return response
|
|
41
|
+
except Exception as e:
|
|
42
|
+
debug_log(f"Error calculating cyclomatic complexity: {str(e)}")
|
|
43
|
+
return {"error": f"Failed to calculate cyclomatic complexity: {str(e)}"}
|
|
44
|
+
|
|
45
|
+
def find_most_complex_functions(code_finder: CodeFinder, **args) -> Dict[str, Any]:
|
|
46
|
+
"""Tool to find the most complex functions."""
|
|
47
|
+
limit = args.get("limit", 10)
|
|
48
|
+
repo_path = args.get("repo_path")
|
|
49
|
+
try:
|
|
50
|
+
debug_log(f"Finding the top {limit} most complex functions. repo_path={repo_path}")
|
|
51
|
+
results = code_finder.find_most_complex_functions(limit, repo_path=repo_path)
|
|
52
|
+
return {
|
|
53
|
+
"success": True,
|
|
54
|
+
"limit": limit,
|
|
55
|
+
"results": results
|
|
56
|
+
}
|
|
57
|
+
except Exception as e:
|
|
58
|
+
debug_log(f"Error finding most complex functions: {str(e)}")
|
|
59
|
+
return {"error": f"Failed to find most complex functions: {str(e)}"}
|
|
60
|
+
|
|
61
|
+
def analyze_code_relationships(code_finder: CodeFinder, **args) -> Dict[str, Any]:
|
|
62
|
+
"""Tool to analyze code relationships"""
|
|
63
|
+
query_type = args.get("query_type")
|
|
64
|
+
target = args.get("target")
|
|
65
|
+
context = args.get("context")
|
|
66
|
+
repo_path = args.get("repo_path")
|
|
67
|
+
|
|
68
|
+
if not query_type or not target:
|
|
69
|
+
return {
|
|
70
|
+
"error": "Both 'query_type' and 'target' are required",
|
|
71
|
+
"supported_query_types": [
|
|
72
|
+
"find_callers", "find_callees", "find_all_callers", "find_all_callees", "find_importers", "who_modifies",
|
|
73
|
+
"class_hierarchy", "overrides", "dead_code", "call_chain",
|
|
74
|
+
"module_deps", "variable_scope", "find_complexity", "find_functions_by_argument", "find_functions_by_decorator"
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
debug_log(f"Analyzing relationships: {query_type} for {target}, repo_path={repo_path}")
|
|
80
|
+
results = code_finder.analyze_code_relationships(query_type, target, context, repo_path=repo_path)
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
"success": True, "query_type": query_type, "target": target,
|
|
84
|
+
"context": context, "results": results
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
except Exception as e:
|
|
88
|
+
debug_log(f"Error analyzing relationships: {str(e)}")
|
|
89
|
+
return {"error": f"Failed to analyze relationships: {str(e)}"}
|
|
90
|
+
|
|
91
|
+
def find_code(code_finder: CodeFinder, **args) -> Dict[str, Any]:
|
|
92
|
+
"""Tool to find relevant code snippets"""
|
|
93
|
+
query = args.get("query")
|
|
94
|
+
DEFAULT_EDIT_DISTANCE = 2
|
|
95
|
+
DEFAULT_FUZZY_SEARCH = False
|
|
96
|
+
|
|
97
|
+
fuzzy_search = args.get("fuzzy_search", DEFAULT_FUZZY_SEARCH)
|
|
98
|
+
edit_distance = args.get("edit_distance", DEFAULT_EDIT_DISTANCE)
|
|
99
|
+
repo_path = args.get("repo_path")
|
|
100
|
+
|
|
101
|
+
if fuzzy_search:
|
|
102
|
+
# Preserve case for Lucene / Levenshtein name matching; lowercasing breaks
|
|
103
|
+
# camelCase fuzzy hits (see GH #758).
|
|
104
|
+
query = query.replace("_", " ").strip()
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
debug_log(f"Finding code for query: {query} with fuzzy_search={fuzzy_search}, edit_distance={edit_distance}, repo_path={repo_path}")
|
|
108
|
+
results = code_finder.find_related_code(query, fuzzy_search, edit_distance, repo_path=repo_path)
|
|
109
|
+
|
|
110
|
+
return {"success": True, "query": query, "results": results}
|
|
111
|
+
|
|
112
|
+
except Exception as e:
|
|
113
|
+
debug_log(f"Error finding code: {str(e)}")
|
|
114
|
+
return {"error": f"Failed to find code: {str(e)}"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
from typing import Any, Dict
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import asyncio
|
|
4
|
+
import os
|
|
5
|
+
from ...utils.debug_log import debug_log
|
|
6
|
+
from ...utils.repo_path import repo_record_matches_path
|
|
7
|
+
from ..package_resolver import get_local_package_path
|
|
8
|
+
|
|
9
|
+
def add_code_to_graph(graph_builder, job_manager, loop, list_repos_func, **args) -> Dict[str, Any]:
|
|
10
|
+
"""
|
|
11
|
+
Tool implementation to index a directory of code.
|
|
12
|
+
Runs indexing asynchronously via a background job.
|
|
13
|
+
"""
|
|
14
|
+
path = args.get("path")
|
|
15
|
+
is_dependency = args.get("is_dependency", False)
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
path_obj = Path(path).resolve()
|
|
19
|
+
|
|
20
|
+
if not path_obj.exists():
|
|
21
|
+
return {
|
|
22
|
+
"success": True,
|
|
23
|
+
"status": "path_not_found",
|
|
24
|
+
"message": f"Path '{path}' does not exist."
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
# Prevent re-indexing the same repository.
|
|
28
|
+
indexed_repos = list_repos_func().get("repositories", [])
|
|
29
|
+
for repo in indexed_repos:
|
|
30
|
+
if repo_record_matches_path(repo, path_obj):
|
|
31
|
+
return {
|
|
32
|
+
"success": False,
|
|
33
|
+
"message": f"Repository '{path}' is already indexed."
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# Estimate time and create a job for the user to track.
|
|
37
|
+
total_files, estimated_time = graph_builder.estimate_processing_time(path_obj)
|
|
38
|
+
job_id = job_manager.create_job(str(path_obj), is_dependency)
|
|
39
|
+
job_manager.update_job(job_id, total_files=total_files, estimated_duration=estimated_time)
|
|
40
|
+
|
|
41
|
+
# Create the coroutine for the background task and schedule it on the main event loop.
|
|
42
|
+
coro = graph_builder.build_graph_from_path_async(
|
|
43
|
+
path_obj, is_dependency, job_id
|
|
44
|
+
)
|
|
45
|
+
asyncio.run_coroutine_threadsafe(coro, loop)
|
|
46
|
+
|
|
47
|
+
debug_log(f"Started background job {job_id} for path: {str(path_obj)}, is_dependency: {is_dependency}")
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
"success": True, "job_id": job_id,
|
|
51
|
+
"message": f"Background processing started for {str(path_obj)}",
|
|
52
|
+
"estimated_files": total_files,
|
|
53
|
+
"estimated_duration_seconds": round(estimated_time, 2),
|
|
54
|
+
"estimated_duration_human": f"{int(estimated_time // 60)}m {int(estimated_time % 60)}s" if estimated_time >= 60 else f"{int(estimated_time)}s",
|
|
55
|
+
"instructions": f"Use 'check_job_status' with job_id '{job_id}' to monitor progress"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
except Exception as e:
|
|
59
|
+
debug_log(f"Error creating background job: {str(e)}")
|
|
60
|
+
return {"error": f"Failed to start background processing: {str(e)}"}
|
|
61
|
+
|
|
62
|
+
def add_package_to_graph(graph_builder, job_manager, loop, list_repos_func, **args) -> Dict[str, Any]:
|
|
63
|
+
"""Tool to add a package to the graph by auto-discovering its location"""
|
|
64
|
+
package_name = args.get("package_name")
|
|
65
|
+
language = args.get("language")
|
|
66
|
+
is_dependency = args.get("is_dependency", True)
|
|
67
|
+
|
|
68
|
+
if not language:
|
|
69
|
+
return {"error": "The 'language' parameter is required."}
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
# Check if the package is already indexed
|
|
73
|
+
indexed_repos = list_repos_func().get("repositories", [])
|
|
74
|
+
for repo in indexed_repos:
|
|
75
|
+
if repo.get("is_dependency") and (repo.get("name") == package_name or repo.get("name") == f"{package_name}.py"):
|
|
76
|
+
return {
|
|
77
|
+
"success": False,
|
|
78
|
+
"message": f"Package '{package_name}' is already indexed."
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
package_path = get_local_package_path(package_name, language)
|
|
82
|
+
|
|
83
|
+
if not package_path:
|
|
84
|
+
return {"error": f"Could not find package '{package_name}' for language '{language}'. Make sure it's installed."}
|
|
85
|
+
|
|
86
|
+
if not os.path.exists(package_path):
|
|
87
|
+
return {"error": f"Package path '{package_path}' does not exist"}
|
|
88
|
+
|
|
89
|
+
path_obj = Path(package_path)
|
|
90
|
+
|
|
91
|
+
total_files, estimated_time = graph_builder.estimate_processing_time(path_obj)
|
|
92
|
+
|
|
93
|
+
job_id = job_manager.create_job(package_path, is_dependency)
|
|
94
|
+
|
|
95
|
+
job_manager.update_job(job_id, total_files=total_files, estimated_duration=estimated_time)
|
|
96
|
+
|
|
97
|
+
coro = graph_builder.build_graph_from_path_async(
|
|
98
|
+
path_obj, is_dependency, job_id
|
|
99
|
+
)
|
|
100
|
+
asyncio.run_coroutine_threadsafe(coro, loop)
|
|
101
|
+
|
|
102
|
+
debug_log(f"Started background job {job_id} for package: {package_name} at {package_path}, is_dependency: {is_dependency}")
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
"success": True, "job_id": job_id, "package_name": package_name,
|
|
106
|
+
"discovered_path": package_path,
|
|
107
|
+
"message": f"Background processing started for package '{package_name}'",
|
|
108
|
+
"estimated_files": total_files,
|
|
109
|
+
"estimated_duration_seconds": round(estimated_time, 2),
|
|
110
|
+
"estimated_duration_human": f"{int(estimated_time // 60)}m {int(estimated_time % 60)}s" if estimated_time >= 60 else f"{int(estimated_time)}s",
|
|
111
|
+
"instructions": f"Use 'check_job_status' with job_id '{job_id}' to monitor progress"
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
except Exception as e:
|
|
115
|
+
debug_log(f"Error creating background job for package {package_name}: {str(e)}")
|
|
116
|
+
return {"error": f"Failed to start background processing for package '{package_name}': {str(e)}"}
|