codegraphcontext-rust 0.5.5__cp310-cp310-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.cp310-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,87 @@
|
|
|
1
|
+
class CppToolkit:
|
|
2
|
+
"""Handles Neo4j queries for C++ file graph."""
|
|
3
|
+
def get_cypher_query(query: str) -> str:
|
|
4
|
+
"""
|
|
5
|
+
Returns a Cypher query string based on the query type requested.
|
|
6
|
+
|
|
7
|
+
Supported query types:
|
|
8
|
+
- functions
|
|
9
|
+
- classes
|
|
10
|
+
- imports
|
|
11
|
+
- structs
|
|
12
|
+
- enums
|
|
13
|
+
- unions
|
|
14
|
+
- macros
|
|
15
|
+
- variables
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
query = query.strip().lower()
|
|
20
|
+
|
|
21
|
+
if query == "functions":
|
|
22
|
+
return """
|
|
23
|
+
MATCH (f:Function)
|
|
24
|
+
RETURN f.name AS name, f.path AS path,
|
|
25
|
+
f.line_number AS line_number, f.docstring AS docstring
|
|
26
|
+
ORDER BY f.path, f.line_number
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
elif query == "classes":
|
|
30
|
+
return """
|
|
31
|
+
MATCH (c:Class)
|
|
32
|
+
RETURN c.name AS name, c.path AS path,
|
|
33
|
+
c.line_number AS line_number, c.docstring AS docstring
|
|
34
|
+
ORDER BY c.path, c.line_number
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
elif query == "imports":
|
|
38
|
+
return """
|
|
39
|
+
MATCH (f:File)-[i:IMPORTS]->(m:Module)
|
|
40
|
+
RETURN f.name AS file_name, m.name AS module_name,
|
|
41
|
+
m.full_import_name AS full_import_name, m.alias AS alias
|
|
42
|
+
ORDER BY f.name
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
elif query == "structs":
|
|
46
|
+
return """
|
|
47
|
+
MATCH (s:Struct)
|
|
48
|
+
RETURN s.name AS name, s.path AS path,
|
|
49
|
+
s.line_number AS line_number, s.fields AS fields
|
|
50
|
+
ORDER BY s.path, s.line_number
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
elif query == "enums":
|
|
54
|
+
return """
|
|
55
|
+
MATCH (e:Enum)
|
|
56
|
+
RETURN e.name AS name, e.path AS path,
|
|
57
|
+
e.line_number AS line_number, e.values AS values
|
|
58
|
+
ORDER BY e.path, e.line_number
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
elif query == "unions":
|
|
62
|
+
return """
|
|
63
|
+
MATCH (u:Union)
|
|
64
|
+
RETURN u.name AS name, u.path AS path,
|
|
65
|
+
u.line_number AS line_number, u.members AS members
|
|
66
|
+
ORDER BY u.path, u.line_number
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
elif query == "macros":
|
|
70
|
+
return """
|
|
71
|
+
MATCH (m:Macro)
|
|
72
|
+
RETURN m.name AS name, m.path AS path,
|
|
73
|
+
m.line_number AS line_number, m.value AS value
|
|
74
|
+
ORDER BY m.path, m.line_number
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
elif query == "variables":
|
|
78
|
+
return """
|
|
79
|
+
MATCH (v:Variable)
|
|
80
|
+
RETURN v.name AS name, v.path AS path,
|
|
81
|
+
v.line_number AS line_number, v.value AS value,
|
|
82
|
+
v.context AS context
|
|
83
|
+
ORDER BY v.path, v.line_number
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
else:
|
|
87
|
+
raise ValueError(f"Unsupported query type: {query}")
|
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
"""
|
|
2
|
+
scip_indexer.py
|
|
3
|
+
---------------
|
|
4
|
+
SCIP-based indexing pipeline. Only activated when SCIP_INDEXER=true in config.
|
|
5
|
+
|
|
6
|
+
SCIP (Semantic Code Intelligence Protocol) is a language-agnostic protocol that
|
|
7
|
+
uses actual compiler / type-checker tooling (e.g. Pyright for Python, tsc for
|
|
8
|
+
TypeScript) to produce a single `index.scip` protobuf file containing:
|
|
9
|
+
- Every symbol definition (function, class, variable) with its file + line
|
|
10
|
+
- Every symbol reference, mapping back to its definition
|
|
11
|
+
- Type signatures, docstrings, and symbol kinds
|
|
12
|
+
|
|
13
|
+
This gives us compiler-level accuracy for CALLS and INHERITS edges instead of
|
|
14
|
+
the heuristic imports_map approach used in Tree-sitter mode.
|
|
15
|
+
|
|
16
|
+
Workflow (called by GraphBuilder.build_graph_from_path_async when enabled):
|
|
17
|
+
1. ScipIndexer.run(path) → runs the appropriate scip-<lang> CLI, returns path to index.scip
|
|
18
|
+
2. ScipIndexParser.parse(index_scip_path) → returns {nodes, edges} dicts
|
|
19
|
+
3. indexing.persistence.GraphWriter writes nodes + edges via the same Cypher MERGE queries as Tree-sitter mode.
|
|
20
|
+
4. Tree-sitter supplement pass adds: cyclomatic_complexity, source text, decorators.
|
|
21
|
+
|
|
22
|
+
Supported SCIP indexers and their install commands:
|
|
23
|
+
python → pip install scip-python (uses Pyright)
|
|
24
|
+
typescript → npm install -g @sourcegraph/scip-typescript
|
|
25
|
+
go → go install github.com/sourcegraph/scip-go/cmd/scip-go@latest
|
|
26
|
+
rust → cargo install scip-rust (or rustup component add rust-analyzer)
|
|
27
|
+
java → https://github.com/sourcegraph/scip-java
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
import os
|
|
31
|
+
# Fix for protobuf 4.x+ version mismatch with scip-python's generated protos
|
|
32
|
+
os.environ["PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION"] = "python"
|
|
33
|
+
import re
|
|
34
|
+
import shutil
|
|
35
|
+
import subprocess
|
|
36
|
+
import tempfile
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
39
|
+
|
|
40
|
+
from ..utils.debug_log import info_logger, warning_logger, error_logger, debug_log
|
|
41
|
+
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
# Language → SCIP indexer mapping
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
# Maps file extension → (language name, scip CLI binary name, install hint)
|
|
47
|
+
EXTENSION_TO_SCIP: Dict[str, Tuple[str, str, str]] = {
|
|
48
|
+
".py": ("python", "scip-python", "pip install scip-python"),
|
|
49
|
+
".ipynb":("python", "scip-python", "pip install scip-python"),
|
|
50
|
+
".ts": ("typescript", "scip-typescript", "npm install -g @sourcegraph/scip-typescript"),
|
|
51
|
+
".tsx": ("typescript", "scip-typescript", "npm install -g @sourcegraph/scip-typescript"),
|
|
52
|
+
".js": ("javascript", "scip-typescript", "npm install -g @sourcegraph/scip-typescript"),
|
|
53
|
+
".jsx": ("javascript", "scip-typescript", "npm install -g @sourcegraph/scip-typescript"),
|
|
54
|
+
".go": ("go", "scip-go", "go install github.com/sourcegraph/scip-go/...@latest"),
|
|
55
|
+
".rs": ("rust", "scip-rust", "cargo install scip-rust"),
|
|
56
|
+
".java": ("java", "scip-java", "see https://github.com/sourcegraph/scip-java"),
|
|
57
|
+
".cpp": ("cpp", "scip-clang", "brew install llvm"),
|
|
58
|
+
".hpp": ("cpp", "scip-clang", "brew install llvm"),
|
|
59
|
+
".c": ("c", "scip-clang", "brew install llvm"),
|
|
60
|
+
".h": ("cpp", "scip-clang", "brew install llvm"),
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def is_scip_available(lang: str) -> bool:
|
|
65
|
+
"""Check whether the SCIP indexer binary for this language is installed."""
|
|
66
|
+
for ext, (l, binary, _) in EXTENSION_TO_SCIP.items():
|
|
67
|
+
if l == lang:
|
|
68
|
+
return shutil.which(binary) is not None
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def detect_project_lang(path: Path, scip_languages: List[str]) -> Optional[str]:
|
|
73
|
+
"""
|
|
74
|
+
Detect the primary language of a project folder by counting files.
|
|
75
|
+
Only returns a language if it is in the user's SCIP_LANGUAGES list.
|
|
76
|
+
"""
|
|
77
|
+
if not path.is_dir():
|
|
78
|
+
ext = path.suffix
|
|
79
|
+
lang = EXTENSION_TO_SCIP.get(ext, (None,))[0]
|
|
80
|
+
return lang if lang in scip_languages else None
|
|
81
|
+
|
|
82
|
+
counts: Dict[str, int] = {}
|
|
83
|
+
for ext, (lang, _, _) in EXTENSION_TO_SCIP.items():
|
|
84
|
+
if lang not in scip_languages:
|
|
85
|
+
continue
|
|
86
|
+
counts[lang] = counts.get(lang, 0) + sum(
|
|
87
|
+
1 for _ in path.rglob(f"*{ext}")
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
if not counts:
|
|
91
|
+
return None
|
|
92
|
+
return max(counts, key=counts.__getitem__)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# ---------------------------------------------------------------------------
|
|
96
|
+
# SCIP runner
|
|
97
|
+
# ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
class ScipIndexer:
|
|
100
|
+
"""
|
|
101
|
+
Runs the appropriate scip-<lang> CLI tool on a project directory and
|
|
102
|
+
returns the path to the resulting index.scip file.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
def run(self, project_path: Path, lang: str, output_dir: Path) -> Optional[Path]:
|
|
106
|
+
"""
|
|
107
|
+
Run the SCIP indexer for `lang` on `project_path`.
|
|
108
|
+
Returns path to index.scip, or None if the indexer failed / is not installed.
|
|
109
|
+
"""
|
|
110
|
+
binary, install_hint = self._get_binary(lang)
|
|
111
|
+
if not binary:
|
|
112
|
+
warning_logger(
|
|
113
|
+
f"SCIP indexer for '{lang}' not found. "
|
|
114
|
+
f"Install with: {install_hint}"
|
|
115
|
+
)
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
output_file = output_dir / "index.scip"
|
|
119
|
+
cmd = self._build_command(lang, binary, project_path, output_file)
|
|
120
|
+
if not cmd:
|
|
121
|
+
warning_logger(f"No SCIP command template defined for language: {lang}")
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
info_logger(f"Running SCIP indexer: {' '.join(str(c) for c in cmd)}")
|
|
125
|
+
try:
|
|
126
|
+
result = subprocess.run(
|
|
127
|
+
cmd,
|
|
128
|
+
cwd=str(project_path),
|
|
129
|
+
capture_output=True,
|
|
130
|
+
text=True,
|
|
131
|
+
timeout=300, # 5 minute hard limit
|
|
132
|
+
)
|
|
133
|
+
if result.returncode != 0:
|
|
134
|
+
warning_logger(
|
|
135
|
+
f"SCIP indexer exited with code {result.returncode}.\n"
|
|
136
|
+
f"stderr: {result.stderr[:500]}"
|
|
137
|
+
)
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
if not output_file.exists():
|
|
141
|
+
warning_logger(f"SCIP indexer ran but no index.scip produced at {output_file}")
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
info_logger(f"SCIP index written to {output_file} ({output_file.stat().st_size // 1024} KB)")
|
|
145
|
+
return output_file
|
|
146
|
+
|
|
147
|
+
except subprocess.TimeoutExpired:
|
|
148
|
+
warning_logger("SCIP indexer timed out after 5 minutes.")
|
|
149
|
+
return None
|
|
150
|
+
except Exception as e:
|
|
151
|
+
warning_logger(f"SCIP indexer failed with exception: {e}")
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
def _get_binary(self, lang: str) -> Tuple[Optional[str], str]:
|
|
155
|
+
for ext, (l, binary, install_hint) in EXTENSION_TO_SCIP.items():
|
|
156
|
+
if l == lang:
|
|
157
|
+
found = shutil.which(binary)
|
|
158
|
+
return found, install_hint
|
|
159
|
+
return None, "unknown language"
|
|
160
|
+
|
|
161
|
+
def _build_command(self, lang: str, binary: str, project_path: Path, output_file: Path) -> Optional[List]:
|
|
162
|
+
"""Build the CLI command for each supported SCIP indexer."""
|
|
163
|
+
out = str(output_file)
|
|
164
|
+
proj = str(project_path)
|
|
165
|
+
|
|
166
|
+
if lang == "python":
|
|
167
|
+
# scip-python index . --output index.scip
|
|
168
|
+
return [binary, "index", ".", "--output", out]
|
|
169
|
+
|
|
170
|
+
elif lang in ("typescript", "javascript"):
|
|
171
|
+
# scip-typescript index --output index.scip
|
|
172
|
+
return [binary, "index", "--output", out]
|
|
173
|
+
|
|
174
|
+
elif lang == "go":
|
|
175
|
+
# scip-go --output index.scip
|
|
176
|
+
return [binary, "--output", out]
|
|
177
|
+
|
|
178
|
+
elif lang == "rust":
|
|
179
|
+
# scip-rust index --output index.scip
|
|
180
|
+
return [binary, "index", "--output", out]
|
|
181
|
+
|
|
182
|
+
elif lang == "java":
|
|
183
|
+
# scip-java index --build-tool gradle/maven --output index.scip
|
|
184
|
+
return [binary, "index", "--output", out]
|
|
185
|
+
|
|
186
|
+
elif lang in ("cpp", "c"):
|
|
187
|
+
# scip-clang --index-output-path index.scip
|
|
188
|
+
return [binary, f"--index-output-path={out}"]
|
|
189
|
+
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
# ---------------------------------------------------------------------------
|
|
194
|
+
# SCIP proto parser
|
|
195
|
+
# ---------------------------------------------------------------------------
|
|
196
|
+
|
|
197
|
+
class ScipIndexParser:
|
|
198
|
+
"""
|
|
199
|
+
Parses a SCIP index.scip protobuf file and converts it into the same
|
|
200
|
+
dict structures consumed by indexing.persistence.GraphWriter (same shape as Tree-sitter output).
|
|
201
|
+
|
|
202
|
+
Output format mirrors what Tree-sitter produces:
|
|
203
|
+
nodes: {"functions": [...], "classes": [...], "variables": [...], "imports": [...]}
|
|
204
|
+
edges: [{"type": "CALLS"|"INHERITS"|"IMPORTS", "from_*": ..., "to_*": ...}, ...]
|
|
205
|
+
|
|
206
|
+
NOTE: This requires the `scip` Python package:
|
|
207
|
+
pip install scip-python (includes the protobuf bindings)
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
def parse(self, index_scip_path: Path, project_path: Path) -> Dict[str, Any]:
|
|
211
|
+
"""
|
|
212
|
+
Parse index.scip and return a dict:
|
|
213
|
+
{
|
|
214
|
+
"files": {
|
|
215
|
+
"relative/path.py": {
|
|
216
|
+
"functions": [...],
|
|
217
|
+
"classes": [...],
|
|
218
|
+
"variables": [...],
|
|
219
|
+
"imports": [...],
|
|
220
|
+
"function_calls_scip": [ ← edges, not tree-sitter calls list
|
|
221
|
+
{"caller_symbol": ..., "callee_file": ..., "callee_line": ...}
|
|
222
|
+
]
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
"""
|
|
227
|
+
try:
|
|
228
|
+
from . import scip_pb2 # type: ignore
|
|
229
|
+
except ImportError:
|
|
230
|
+
error_logger(
|
|
231
|
+
"scip_pb2.py not found in tools directory."
|
|
232
|
+
)
|
|
233
|
+
return {}
|
|
234
|
+
|
|
235
|
+
try:
|
|
236
|
+
with open(index_scip_path, "rb") as f:
|
|
237
|
+
index = scip_pb2.Index()
|
|
238
|
+
index.ParseFromString(f.read())
|
|
239
|
+
except Exception as e:
|
|
240
|
+
error_logger(f"Failed to parse SCIP index at {index_scip_path}: {e}")
|
|
241
|
+
return {}
|
|
242
|
+
|
|
243
|
+
# Build a global symbol → (file, line, kind) lookup table
|
|
244
|
+
# from all definition occurrences across all documents
|
|
245
|
+
symbol_def_table: Dict[str, Dict] = {} # symbol_str → {file, line, kind, display_name, doc}
|
|
246
|
+
|
|
247
|
+
# First pass: collect all definitions
|
|
248
|
+
for doc in index.documents:
|
|
249
|
+
for occ in doc.occurrences:
|
|
250
|
+
if occ.symbol.startswith("local "):
|
|
251
|
+
continue
|
|
252
|
+
# role bit 0 = Definition (SCIP 0.6.0+ uses symbol_roles)
|
|
253
|
+
# Try symbol_roles first, then fallback to role if present
|
|
254
|
+
role = getattr(occ, "symbol_roles", getattr(occ, "role", 0))
|
|
255
|
+
if role & 1:
|
|
256
|
+
symbol_def_table[occ.symbol] = {
|
|
257
|
+
"file": doc.relative_path,
|
|
258
|
+
"line": occ.range[0] + 1 if occ.range else 0,
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
# Enrich with metadata from the symbols table
|
|
262
|
+
# SCIP 0.6.0+ stores symbols defined in the document inside doc.symbols
|
|
263
|
+
for doc in index.documents:
|
|
264
|
+
for sym_info in doc.symbols:
|
|
265
|
+
if sym_info.symbol in symbol_def_table:
|
|
266
|
+
symbol_def_table[sym_info.symbol]["display_name"] = sym_info.display_name
|
|
267
|
+
symbol_def_table[sym_info.symbol]["documentation"] = "\n".join(sym_info.documentation)
|
|
268
|
+
symbol_def_table[sym_info.symbol]["kind"] = sym_info.kind
|
|
269
|
+
|
|
270
|
+
# Also check external_symbols
|
|
271
|
+
for sym_info in index.external_symbols:
|
|
272
|
+
if sym_info.symbol in symbol_def_table:
|
|
273
|
+
symbol_def_table[sym_info.symbol]["display_name"] = sym_info.display_name
|
|
274
|
+
symbol_def_table[sym_info.symbol]["documentation"] = "\n".join(sym_info.documentation)
|
|
275
|
+
symbol_def_table[sym_info.symbol]["kind"] = sym_info.kind
|
|
276
|
+
|
|
277
|
+
# Second pass: extract per-file nodes and reference edges
|
|
278
|
+
files_data: Dict[str, Dict] = {}
|
|
279
|
+
|
|
280
|
+
for doc in index.documents:
|
|
281
|
+
rel_path = doc.relative_path
|
|
282
|
+
abs_path = str((project_path / rel_path).resolve())
|
|
283
|
+
|
|
284
|
+
file_data: Dict[str, Any] = {
|
|
285
|
+
"functions": [],
|
|
286
|
+
"classes": [],
|
|
287
|
+
"variables": [],
|
|
288
|
+
"imports": [],
|
|
289
|
+
"function_calls_scip": [],
|
|
290
|
+
"path": abs_path,
|
|
291
|
+
"lang": self._lang_from_path(rel_path),
|
|
292
|
+
"is_dependency": False,
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
# Track which symbol is the enclosing definition at each line
|
|
296
|
+
# so we know what "calls" what
|
|
297
|
+
definition_symbols_in_doc = []
|
|
298
|
+
for occ in doc.occurrences:
|
|
299
|
+
role = getattr(occ, "symbol_roles", getattr(occ, "role", 0))
|
|
300
|
+
if role & 1: # Definition
|
|
301
|
+
definition_symbols_in_doc.append(occ)
|
|
302
|
+
|
|
303
|
+
for occ in doc.occurrences:
|
|
304
|
+
sym = occ.symbol
|
|
305
|
+
if sym.startswith("local "):
|
|
306
|
+
continue
|
|
307
|
+
line = occ.range[0] + 1 if occ.range else 0
|
|
308
|
+
# Try symbol_roles first, then fallback to role if present
|
|
309
|
+
role = getattr(occ, "symbol_roles", getattr(occ, "role", 0))
|
|
310
|
+
|
|
311
|
+
if role & 1: # Definition
|
|
312
|
+
defn = symbol_def_table.get(sym, {})
|
|
313
|
+
kind = defn.get("kind", 0)
|
|
314
|
+
|
|
315
|
+
# If kind is 0 (Unspecified), guess from symbol string
|
|
316
|
+
if kind == 0:
|
|
317
|
+
if sym.endswith("()."):
|
|
318
|
+
kind = 17 # Function
|
|
319
|
+
elif "#" in sym and not sym.endswith("."):
|
|
320
|
+
# If it ends with # (e.g. MyClass#) or has # then members
|
|
321
|
+
if sym.endswith("#"):
|
|
322
|
+
kind = 7 # Class
|
|
323
|
+
elif sym.endswith("()."):
|
|
324
|
+
kind = 26 # Method
|
|
325
|
+
else:
|
|
326
|
+
# Possibly a field or nested class or parameter
|
|
327
|
+
pass
|
|
328
|
+
|
|
329
|
+
display = defn.get("display_name", "")
|
|
330
|
+
doc_str = defn.get("documentation", "")
|
|
331
|
+
name = self._name_from_symbol(sym)
|
|
332
|
+
args, return_type = self._parse_signature(display, kind)
|
|
333
|
+
|
|
334
|
+
node = {
|
|
335
|
+
"name": name,
|
|
336
|
+
"line_number": line,
|
|
337
|
+
"end_line": line,
|
|
338
|
+
"docstring": doc_str or None,
|
|
339
|
+
"lang": file_data["lang"],
|
|
340
|
+
"is_dependency": False,
|
|
341
|
+
# SCIP gives us these for free:
|
|
342
|
+
"return_type": return_type,
|
|
343
|
+
"args": args,
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
# kind values from SCIP 0.6.0+ proto:
|
|
347
|
+
# 26=Method, 17=Function -> Function node
|
|
348
|
+
# 7=Class -> Class node
|
|
349
|
+
# 61=Variable, 15=Field -> Variable node
|
|
350
|
+
if kind in (26, 17): # Method, Function
|
|
351
|
+
node["cyclomatic_complexity"] = 1 # filled by Tree-sitter supplement
|
|
352
|
+
node["decorators"] = []
|
|
353
|
+
node["context"] = None
|
|
354
|
+
node["class_context"] = None
|
|
355
|
+
file_data["functions"].append(node)
|
|
356
|
+
elif kind == 7: # Class
|
|
357
|
+
node["bases"] = []
|
|
358
|
+
node["context"] = None
|
|
359
|
+
file_data["classes"].append(node)
|
|
360
|
+
elif kind in (61, 15): # Variable, Field
|
|
361
|
+
node["value"] = None
|
|
362
|
+
node["type"] = return_type
|
|
363
|
+
node["context"] = None
|
|
364
|
+
node["class_context"] = None
|
|
365
|
+
file_data["variables"].append(node)
|
|
366
|
+
|
|
367
|
+
else: # Reference — find its definition for CALLS edge
|
|
368
|
+
if sym in symbol_def_table:
|
|
369
|
+
callee_info = symbol_def_table[sym]
|
|
370
|
+
# Find the enclosing definition in THIS document
|
|
371
|
+
caller_sym = self._find_enclosing_definition(
|
|
372
|
+
line, definition_symbols_in_doc
|
|
373
|
+
)
|
|
374
|
+
if caller_sym:
|
|
375
|
+
caller_info = symbol_def_table.get(caller_sym, {})
|
|
376
|
+
file_data["function_calls_scip"].append({
|
|
377
|
+
"caller_symbol": caller_sym,
|
|
378
|
+
"caller_file": abs_path,
|
|
379
|
+
"caller_line": caller_info.get("line", 0),
|
|
380
|
+
"callee_symbol": sym,
|
|
381
|
+
"callee_file": str(
|
|
382
|
+
(project_path / callee_info["file"]).resolve()
|
|
383
|
+
),
|
|
384
|
+
"callee_line": callee_info["line"],
|
|
385
|
+
"callee_name": self._name_from_symbol(sym),
|
|
386
|
+
"ref_line": line,
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
files_data[abs_path] = file_data
|
|
390
|
+
|
|
391
|
+
info_logger(
|
|
392
|
+
f"SCIP parse complete: {len(files_data)} files, "
|
|
393
|
+
f"{sum(len(v.get('function_calls_scip',[])) for v in files_data.values())} reference edges"
|
|
394
|
+
)
|
|
395
|
+
return {"files": files_data, "symbol_table": symbol_def_table}
|
|
396
|
+
|
|
397
|
+
def _name_from_symbol(self, symbol: str) -> str:
|
|
398
|
+
"""Extract the human-readable name from a SCIP symbol ID."""
|
|
399
|
+
# SCIP symbols look like: "scip-python . . mymodule/MyClass#method()."
|
|
400
|
+
import re
|
|
401
|
+
s = symbol.rstrip(".#")
|
|
402
|
+
s = re.sub(r"\(\)\.?$", "", s) # Remove trailing () or ().
|
|
403
|
+
parts = re.split(r'[/#]', s)
|
|
404
|
+
last = parts[-1] if parts else symbol
|
|
405
|
+
return last or symbol
|
|
406
|
+
|
|
407
|
+
def _lang_from_path(self, rel_path: str) -> str:
|
|
408
|
+
"""Guess language from file extension."""
|
|
409
|
+
ext_map = {
|
|
410
|
+
".py": "python", ".ipynb": "python",
|
|
411
|
+
".ts": "typescript", ".tsx": "typescript",
|
|
412
|
+
".js": "javascript", ".jsx": "javascript",
|
|
413
|
+
".go": "go", ".rs": "rust",
|
|
414
|
+
".java": "java", ".cpp": "cpp", ".c": "c", ".h": "cpp",
|
|
415
|
+
}
|
|
416
|
+
suffix = Path(rel_path).suffix
|
|
417
|
+
return ext_map.get(suffix, "unknown")
|
|
418
|
+
|
|
419
|
+
def _parse_signature(self, display_name: str, kind: int) -> Tuple[List[str], Optional[str]]:
|
|
420
|
+
"""
|
|
421
|
+
Extract parameter names and return type from a SCIP display_name string.
|
|
422
|
+
e.g. "def method(self, x: int, y: str) -> Response"
|
|
423
|
+
→ (["self", "x", "y"], "Response")
|
|
424
|
+
"""
|
|
425
|
+
args: List[str] = []
|
|
426
|
+
return_type: Optional[str] = None
|
|
427
|
+
|
|
428
|
+
if not display_name:
|
|
429
|
+
return args, return_type
|
|
430
|
+
|
|
431
|
+
# Return type after '->'
|
|
432
|
+
if "->" in display_name:
|
|
433
|
+
parts = display_name.rsplit("->", 1)
|
|
434
|
+
return_type = parts[1].strip().rstrip(":")
|
|
435
|
+
|
|
436
|
+
# Parameters between first ( and last )
|
|
437
|
+
param_match = re.search(r"\(([^)]*)\)", display_name)
|
|
438
|
+
if param_match:
|
|
439
|
+
raw_params = param_match.group(1)
|
|
440
|
+
for param in raw_params.split(","):
|
|
441
|
+
param = param.strip()
|
|
442
|
+
if not param:
|
|
443
|
+
continue
|
|
444
|
+
# "x: int = 5" → "x"
|
|
445
|
+
name = param.split(":")[0].split("=")[0].strip()
|
|
446
|
+
# Remove * and ** prefixes
|
|
447
|
+
name = name.lstrip("*")
|
|
448
|
+
if name:
|
|
449
|
+
args.append(name)
|
|
450
|
+
|
|
451
|
+
return args, return_type
|
|
452
|
+
|
|
453
|
+
def _find_enclosing_definition(
|
|
454
|
+
self, ref_line: int, definition_occurrences: list
|
|
455
|
+
) -> Optional[str]:
|
|
456
|
+
"""
|
|
457
|
+
Given a reference at `ref_line`, find the symbol of the most recent
|
|
458
|
+
definition that started before this line. That's the 'caller'.
|
|
459
|
+
"""
|
|
460
|
+
best = None
|
|
461
|
+
best_line = -1
|
|
462
|
+
for occ in definition_occurrences:
|
|
463
|
+
occ_line = occ.range[0] + 1 if occ.range else 0
|
|
464
|
+
if occ_line <= ref_line and occ_line > best_line:
|
|
465
|
+
best = occ.symbol
|
|
466
|
+
best_line = occ_line
|
|
467
|
+
return best
|