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.
Files changed (101) hide show
  1. codegraphcontext/__init__.py +2 -0
  2. codegraphcontext/__main__.py +5 -0
  3. codegraphcontext/_cgc_rust.cp310-win_amd64.pyd +0 -0
  4. codegraphcontext/cli/__init__.py +1 -0
  5. codegraphcontext/cli/cli_helpers.py +774 -0
  6. codegraphcontext/cli/config_manager.py +1051 -0
  7. codegraphcontext/cli/main.py +2386 -0
  8. codegraphcontext/cli/registry_commands.py +404 -0
  9. codegraphcontext/cli/setup_macos.py +93 -0
  10. codegraphcontext/cli/setup_wizard.py +991 -0
  11. codegraphcontext/cli/visualizer.py +50 -0
  12. codegraphcontext/core/__init__.py +165 -0
  13. codegraphcontext/core/bundle_registry.py +181 -0
  14. codegraphcontext/core/cgc_bundle.py +858 -0
  15. codegraphcontext/core/cgcignore.py +118 -0
  16. codegraphcontext/core/database.py +273 -0
  17. codegraphcontext/core/database_falkordb.py +480 -0
  18. codegraphcontext/core/database_falkordb_remote.py +199 -0
  19. codegraphcontext/core/database_kuzu.py +636 -0
  20. codegraphcontext/core/falkor_worker.py +132 -0
  21. codegraphcontext/core/jobs.py +132 -0
  22. codegraphcontext/core/watcher.py +260 -0
  23. codegraphcontext/prompts.py +125 -0
  24. codegraphcontext/server.py +471 -0
  25. codegraphcontext/tool_definitions.py +220 -0
  26. codegraphcontext/tools/__init__.py +0 -0
  27. codegraphcontext/tools/advanced_language_query_tool.py +103 -0
  28. codegraphcontext/tools/code_finder.py +1160 -0
  29. codegraphcontext/tools/graph_builder.py +337 -0
  30. codegraphcontext/tools/handlers/analysis_handlers.py +114 -0
  31. codegraphcontext/tools/handlers/indexing_handlers.py +116 -0
  32. codegraphcontext/tools/handlers/management_handlers.py +339 -0
  33. codegraphcontext/tools/handlers/query_handlers.py +83 -0
  34. codegraphcontext/tools/handlers/watcher_handlers.py +83 -0
  35. codegraphcontext/tools/indexing/__init__.py +1 -0
  36. codegraphcontext/tools/indexing/constants.py +25 -0
  37. codegraphcontext/tools/indexing/discovery.py +64 -0
  38. codegraphcontext/tools/indexing/engine.py +109 -0
  39. codegraphcontext/tools/indexing/persistence/__init__.py +3 -0
  40. codegraphcontext/tools/indexing/persistence/parquet_loader.py +643 -0
  41. codegraphcontext/tools/indexing/persistence/writer.py +1114 -0
  42. codegraphcontext/tools/indexing/pipeline.py +171 -0
  43. codegraphcontext/tools/indexing/pre_scan.py +105 -0
  44. codegraphcontext/tools/indexing/resolution/__init__.py +8 -0
  45. codegraphcontext/tools/indexing/resolution/calls.py +253 -0
  46. codegraphcontext/tools/indexing/resolution/inheritance.py +91 -0
  47. codegraphcontext/tools/indexing/sanitize.py +41 -0
  48. codegraphcontext/tools/indexing/schema.py +79 -0
  49. codegraphcontext/tools/indexing/schema_contract.py +44 -0
  50. codegraphcontext/tools/indexing/scip_pipeline.py +140 -0
  51. codegraphcontext/tools/languages/c.py +563 -0
  52. codegraphcontext/tools/languages/cpp.py +616 -0
  53. codegraphcontext/tools/languages/csharp.py +551 -0
  54. codegraphcontext/tools/languages/dart.py +378 -0
  55. codegraphcontext/tools/languages/elixir.py +460 -0
  56. codegraphcontext/tools/languages/go.py +499 -0
  57. codegraphcontext/tools/languages/haskell.py +426 -0
  58. codegraphcontext/tools/languages/java.py +500 -0
  59. codegraphcontext/tools/languages/javascript.py +589 -0
  60. codegraphcontext/tools/languages/kotlin.py +640 -0
  61. codegraphcontext/tools/languages/perl.py +260 -0
  62. codegraphcontext/tools/languages/php.py +519 -0
  63. codegraphcontext/tools/languages/python.py +576 -0
  64. codegraphcontext/tools/languages/ruby.py +537 -0
  65. codegraphcontext/tools/languages/rust.py +295 -0
  66. codegraphcontext/tools/languages/scala.py +519 -0
  67. codegraphcontext/tools/languages/swift.py +491 -0
  68. codegraphcontext/tools/languages/typescript.py +575 -0
  69. codegraphcontext/tools/languages/typescriptjsx.py +151 -0
  70. codegraphcontext/tools/package_resolver.py +472 -0
  71. codegraphcontext/tools/query_tool_languages/c_toolkit.py +5 -0
  72. codegraphcontext/tools/query_tool_languages/cpp_toolkit.py +87 -0
  73. codegraphcontext/tools/query_tool_languages/csharp_toolkit.py +5 -0
  74. codegraphcontext/tools/query_tool_languages/dart_toolkit.py +5 -0
  75. codegraphcontext/tools/query_tool_languages/go_toolkit.py +5 -0
  76. codegraphcontext/tools/query_tool_languages/haskell_toolkit.py +5 -0
  77. codegraphcontext/tools/query_tool_languages/java_toolkit.py +5 -0
  78. codegraphcontext/tools/query_tool_languages/javascript_toolkit.py +5 -0
  79. codegraphcontext/tools/query_tool_languages/perl_toolkit.py +5 -0
  80. codegraphcontext/tools/query_tool_languages/python_toolkit.py +5 -0
  81. codegraphcontext/tools/query_tool_languages/ruby_toolkit.py +5 -0
  82. codegraphcontext/tools/query_tool_languages/rust_toolkit.py +5 -0
  83. codegraphcontext/tools/query_tool_languages/scala_toolkit.py +5 -0
  84. codegraphcontext/tools/query_tool_languages/swift_toolkit.py +5 -0
  85. codegraphcontext/tools/query_tool_languages/typescript_toolkit.py +5 -0
  86. codegraphcontext/tools/scip_indexer.py +467 -0
  87. codegraphcontext/tools/scip_pb2.py +2455 -0
  88. codegraphcontext/tools/system.py +133 -0
  89. codegraphcontext/tools/tree_sitter_parser.py +103 -0
  90. codegraphcontext/utils/debug_log.py +90 -0
  91. codegraphcontext/utils/path_ignore.py +55 -0
  92. codegraphcontext/utils/repo_path.py +27 -0
  93. codegraphcontext/utils/tree_sitter_manager.py +285 -0
  94. codegraphcontext/utils/visualize_graph.py +0 -0
  95. codegraphcontext/viz/server.py +282 -0
  96. codegraphcontext_rust-0.5.5.dist-info/METADATA +537 -0
  97. codegraphcontext_rust-0.5.5.dist-info/RECORD +101 -0
  98. codegraphcontext_rust-0.5.5.dist-info/WHEEL +4 -0
  99. codegraphcontext_rust-0.5.5.dist-info/entry_points.txt +3 -0
  100. codegraphcontext_rust-0.5.5.dist-info/licenses/LICENSE +21 -0
  101. codegraphcontext_rust-0.5.5.dist-info/sboms/cgc-python.cyclonedx.json +2333 -0
@@ -0,0 +1,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,5 @@
1
+ class CSharpToolkit:
2
+ """Template placeholder for future implementation."""
3
+
4
+ def get_cypher_query(query: str) -> str:
5
+ raise NotImplementedError("AdvancedLanguageQuery is not implemented yet.")
@@ -0,0 +1,5 @@
1
+ class DartToolkit:
2
+ """Template placeholder for future implementation."""
3
+
4
+ def get_cypher_query(query: str) -> str:
5
+ raise NotImplementedError("AdvancedLanguageQuery is not implemented yet.")
@@ -0,0 +1,5 @@
1
+ class GoToolkit:
2
+ """Template placeholder for future implementation."""
3
+
4
+ def get_cypher_query(query: str) -> str:
5
+ raise NotImplementedError("AdvancedLanguageQuery is not implemented yet.")
@@ -0,0 +1,5 @@
1
+ class HaskellToolkit:
2
+ """ Template placeholder for future implementation."""
3
+
4
+ def get_cypher_query(query: str) -> str:
5
+ raise NotImplementedError("AdvancedLanguageQuery is not implemented yet.")
@@ -0,0 +1,5 @@
1
+ class JavaToolkit:
2
+ """Template placeholder for future implementation."""
3
+
4
+ def get_cypher_query(query: str) -> str:
5
+ raise NotImplementedError("AdvancedLanguageQuery is not implemented yet.")
@@ -0,0 +1,5 @@
1
+ class JavascriptToolkit:
2
+ """Template placeholder for future implementation."""
3
+
4
+ def get_cypher_query(query: str) -> str:
5
+ raise NotImplementedError("AdvancedLanguageQuery is not implemented yet.")
@@ -0,0 +1,5 @@
1
+ class PerlToolkit:
2
+ """Template placeholder for future implementation."""
3
+
4
+ def get_cypher_query(query: str) -> str:
5
+ raise NotImplementedError("AdvancedLanguageQuery is not implemented yet.")
@@ -0,0 +1,5 @@
1
+ class PythonToolkit:
2
+ """Template placeholder for future implementation."""
3
+
4
+ def get_cypher_query(query: str) -> str:
5
+ raise NotImplementedError("AdvancedLanguageQuery is not implemented yet.")
@@ -0,0 +1,5 @@
1
+ class RubyToolkit:
2
+ """Template placeholder for future implementation."""
3
+
4
+ def get_cypher_query(query: str) -> str:
5
+ raise NotImplementedError("AdvancedLanguageQuery is not implemented yet.")
@@ -0,0 +1,5 @@
1
+ class RustToolkit:
2
+ """Template placeholder for future implementation."""
3
+
4
+ def get_cypher_query(query: str) -> str:
5
+ raise NotImplementedError("AdvancedLanguageQuery is not implemented yet.")
@@ -0,0 +1,5 @@
1
+ class ScalaToolkit:
2
+ """Template placeholder for future implementation."""
3
+
4
+ def get_cypher_query(query: str) -> str:
5
+ raise NotImplementedError("AdvancedLanguageQuery is not implemented yet.")
@@ -0,0 +1,5 @@
1
+ class SwiftToolkit:
2
+ """Template placeholder for future implementation."""
3
+
4
+ def get_cypher_query(query: str) -> str:
5
+ raise NotImplementedError("AdvancedLanguageQuery is not implemented yet.")
@@ -0,0 +1,5 @@
1
+ class TypescriptToolkit:
2
+ """Template placeholder for future implementation."""
3
+
4
+ def get_cypher_query(query: str) -> str:
5
+ raise NotImplementedError("AdvancedLanguageQuery is not implemented yet.")
@@ -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