cg-code-graph 0.10.1__py3-none-any.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 (174) hide show
  1. cg_code_graph-0.10.1.dist-info/METADATA +678 -0
  2. cg_code_graph-0.10.1.dist-info/RECORD +174 -0
  3. cg_code_graph-0.10.1.dist-info/WHEEL +5 -0
  4. cg_code_graph-0.10.1.dist-info/entry_points.txt +3 -0
  5. cg_code_graph-0.10.1.dist-info/licenses/LICENSE +21 -0
  6. cg_code_graph-0.10.1.dist-info/top_level.txt +1 -0
  7. codegraph/__init__.py +2 -0
  8. codegraph/aitools.py +129 -0
  9. codegraph/apps.py +76 -0
  10. codegraph/blindspots.py +428 -0
  11. codegraph/bridges.py +1701 -0
  12. codegraph/cli.py +725 -0
  13. codegraph/concepts.py +362 -0
  14. codegraph/config.py +559 -0
  15. codegraph/core/__init__.py +0 -0
  16. codegraph/core/cache.py +375 -0
  17. codegraph/core/detect.py +80 -0
  18. codegraph/core/extractors.py +187 -0
  19. codegraph/core/fsutil.py +61 -0
  20. codegraph/core/generated.py +575 -0
  21. codegraph/core/model.py +174 -0
  22. codegraph/core/paths.py +175 -0
  23. codegraph/core/plugin.py +160 -0
  24. codegraph/core/store.py +80 -0
  25. codegraph/core/syntax_errors.py +132 -0
  26. codegraph/coverage.py +928 -0
  27. codegraph/doctor.py +453 -0
  28. codegraph/external.py +613 -0
  29. codegraph/indexer.py +336 -0
  30. codegraph/link.py +434 -0
  31. codegraph/lint_async.py +524 -0
  32. codegraph/mcp_server.py +1303 -0
  33. codegraph/parity.py +473 -0
  34. codegraph/parity_structure.py +307 -0
  35. codegraph/payload.py +321 -0
  36. codegraph/plans.py +1285 -0
  37. codegraph/platform_scan.py +643 -0
  38. codegraph/platforms.py +1369 -0
  39. codegraph/plugins/__init__.py +0 -0
  40. codegraph/plugins/cfamily/__init__.py +0 -0
  41. codegraph/plugins/cfamily/plugin.py +930 -0
  42. codegraph/plugins/cfamily/syntax.py +881 -0
  43. codegraph/plugins/dart/__init__.py +0 -0
  44. codegraph/plugins/dart/bridges.py +345 -0
  45. codegraph/plugins/dart/extractor/bin/extract.dart +717 -0
  46. codegraph/plugins/dart/extractor/pubspec.lock +149 -0
  47. codegraph/plugins/dart/extractor/pubspec.yaml +7 -0
  48. codegraph/plugins/dart/http.py +904 -0
  49. codegraph/plugins/dart/models.py +308 -0
  50. codegraph/plugins/dart/plugin.py +625 -0
  51. codegraph/plugins/dart/program.py +907 -0
  52. codegraph/plugins/django/__init__.py +0 -0
  53. codegraph/plugins/django/extras.py +378 -0
  54. codegraph/plugins/django/models.py +508 -0
  55. codegraph/plugins/django/plugin.py +728 -0
  56. codegraph/plugins/django/schemas.py +339 -0
  57. codegraph/plugins/django/shapes.py +216 -0
  58. codegraph/plugins/django/urls.py +603 -0
  59. codegraph/plugins/express/__init__.py +0 -0
  60. codegraph/plugins/express/plugin.py +428 -0
  61. codegraph/plugins/flutter/__init__.py +0 -0
  62. codegraph/plugins/flutter/plugin.py +538 -0
  63. codegraph/plugins/kotlin/__init__.py +0 -0
  64. codegraph/plugins/kotlin/exact.py +457 -0
  65. codegraph/plugins/kotlin/plugin.py +1961 -0
  66. codegraph/plugins/kotlin/reparse.py +234 -0
  67. codegraph/plugins/laravel/__init__.py +0 -0
  68. codegraph/plugins/laravel/broadcast.py +351 -0
  69. codegraph/plugins/laravel/plugin.py +863 -0
  70. codegraph/plugins/laravel/tests.py +262 -0
  71. codegraph/plugins/laravel/values.py +728 -0
  72. codegraph/plugins/native/__init__.py +0 -0
  73. codegraph/plugins/native/gates.py +286 -0
  74. codegraph/plugins/native/runner.py +183 -0
  75. codegraph/plugins/native/scipread.py +194 -0
  76. codegraph/plugins/native/ts.py +54 -0
  77. codegraph/plugins/nest/__init__.py +0 -0
  78. codegraph/plugins/nest/plugin.py +654 -0
  79. codegraph/plugins/nextjs/__init__.py +0 -0
  80. codegraph/plugins/nextjs/plugin.py +336 -0
  81. codegraph/plugins/nuxt/__init__.py +0 -0
  82. codegraph/plugins/nuxt/plugin.py +308 -0
  83. codegraph/plugins/php/__init__.py +0 -0
  84. codegraph/plugins/php/extractor/composer.json +5 -0
  85. codegraph/plugins/php/extractor/composer.lock +76 -0
  86. codegraph/plugins/php/extractor/extract.php +743 -0
  87. codegraph/plugins/php/gating.py +573 -0
  88. codegraph/plugins/php/plugin.py +668 -0
  89. codegraph/plugins/php/strings.py +197 -0
  90. codegraph/plugins/python/__init__.py +0 -0
  91. codegraph/plugins/python/aitools.py +664 -0
  92. codegraph/plugins/python/external.py +245 -0
  93. codegraph/plugins/python/fields.py +107 -0
  94. codegraph/plugins/python/plugin.py +1733 -0
  95. codegraph/plugins/python/refs.py +485 -0
  96. codegraph/plugins/python/roots.py +412 -0
  97. codegraph/plugins/python/socketio.py +210 -0
  98. codegraph/plugins/python/subproc.py +864 -0
  99. codegraph/plugins/python/tests.py +1040 -0
  100. codegraph/plugins/python/values.py +179 -0
  101. codegraph/plugins/pyweb/__init__.py +0 -0
  102. codegraph/plugins/pyweb/plugin.py +1334 -0
  103. codegraph/plugins/pyweb/values.py +68 -0
  104. codegraph/plugins/rust/__init__.py +0 -0
  105. codegraph/plugins/rust/cargo.py +226 -0
  106. codegraph/plugins/rust/plugin.py +980 -0
  107. codegraph/plugins/rust/syntax.py +678 -0
  108. codegraph/plugins/scip/__init__.py +0 -0
  109. codegraph/plugins/scip/importer.py +129 -0
  110. codegraph/plugins/scip/scip.proto +962 -0
  111. codegraph/plugins/scip/scip_pb2.py +97 -0
  112. codegraph/plugins/stubs/__init__.py +0 -0
  113. codegraph/plugins/stubs/plugins.py +38 -0
  114. codegraph/plugins/swift/__init__.py +0 -0
  115. codegraph/plugins/swift/baseurl.py +109 -0
  116. codegraph/plugins/swift/exact.py +415 -0
  117. codegraph/plugins/swift/indexstore.py +209 -0
  118. codegraph/plugins/swift/packages.py +174 -0
  119. codegraph/plugins/swift/plugin.py +2890 -0
  120. codegraph/plugins/ts/__init__.py +0 -0
  121. codegraph/plugins/ts/baseurl.py +185 -0
  122. codegraph/plugins/ts/extractor/extract.mjs +2652 -0
  123. codegraph/plugins/ts/extractor/fw.mjs +685 -0
  124. codegraph/plugins/ts/extractor/package-lock.json +205 -0
  125. codegraph/plugins/ts/extractor/package.json +9 -0
  126. codegraph/plugins/ts/plugin.py +480 -0
  127. codegraph/plugins/tsweb/__init__.py +0 -0
  128. codegraph/plugins/tsweb/common.py +290 -0
  129. codegraph/plugins/tsweb/data.py +276 -0
  130. codegraph/presets/__init__.py +146 -0
  131. codegraph/presets/c_cpp.yaml +9 -0
  132. codegraph/presets/common.yaml +66 -0
  133. codegraph/presets/dart.yaml +9 -0
  134. codegraph/presets/django-ninja.yaml +15 -0
  135. codegraph/presets/django.yaml +25 -0
  136. codegraph/presets/djangorestframework.yaml +17 -0
  137. codegraph/presets/express.yaml +17 -0
  138. codegraph/presets/kotlin.yaml +11 -0
  139. codegraph/presets/laravel.yaml +40 -0
  140. codegraph/presets/nest.yaml +11 -0
  141. codegraph/presets/nextjs.yaml +15 -0
  142. codegraph/presets/nuxt.yaml +9 -0
  143. codegraph/presets/php.yaml +5 -0
  144. codegraph/presets/python.yaml +10 -0
  145. codegraph/presets/rust.yaml +5 -0
  146. codegraph/presets/swift.yaml +10 -0
  147. codegraph/presets/typescript.yaml +13 -0
  148. codegraph/process_runs.py +328 -0
  149. codegraph/protocols/__init__.py +299 -0
  150. codegraph/protocols/builtin.py +67 -0
  151. codegraph/protocols/matchers.py +144 -0
  152. codegraph/protocols/view.py +334 -0
  153. codegraph/query.py +2089 -0
  154. codegraph/realtime.py +260 -0
  155. codegraph/roundtrip.py +346 -0
  156. codegraph/routes.py +442 -0
  157. codegraph/starters.py +218 -0
  158. codegraph/tests_index.py +117 -0
  159. codegraph/viz/__init__.py +0 -0
  160. codegraph/viz/graph.py +369 -0
  161. codegraph/viz/server.py +198 -0
  162. codegraph/viz/static/app.css +148 -0
  163. codegraph/viz/static/app.js +1082 -0
  164. codegraph/viz/static/index.html +81 -0
  165. codegraph/viz/static/layered.js +237 -0
  166. codegraph/viz/static/vendor/VERSIONS.txt +4 -0
  167. codegraph/viz/static/vendor/cose-base.js +3214 -0
  168. codegraph/viz/static/vendor/cytoscape-fcose.js +1549 -0
  169. codegraph/viz/static/vendor/cytoscape.min.js +31 -0
  170. codegraph/viz/static/vendor/layout-base.js +5230 -0
  171. codegraph/viz/tools/package-lock.json +303 -0
  172. codegraph/viz/tools/package.json +7 -0
  173. codegraph/viz/tools/shoot.mjs +165 -0
  174. codegraph/xcode.py +251 -0
@@ -0,0 +1,980 @@
1
+ """Rust language plugin.
2
+
3
+ Two layers, merged onto the same node ids:
4
+ 1. syntactic (tree-sitter-rust, always): Cargo packages / targets / features, the module tree of every target,
5
+ items with keys, visibility, attributes, cfg gates, impls, unsafe, FFI, env keys, entry points, routes;
6
+ 2. semantic (rust-analyzer `scip`, when installed): every reference resolved by the compiler front-end.
7
+ Definitions are matched to syntactic items by (file, line, name), references become CALLS / USES_TYPE /
8
+ ACCESSES_FIELD / USES_VALUE / REFERENCES_FN edges from the innermost enclosing item.
9
+ Without rust-analyzer (or with CODEGRAPH_RUST_SCIP=0) a name-based resolver over the syntactic layer is used:
10
+ full-path matches are `resolved`, name-only matches `heuristic`.
11
+
12
+ Safety: rust-analyzer runs with build scripts and proc-macros disabled by default (they execute project code);
13
+ set CODEGRAPH_RUST_BUILD_SCRIPTS=1 to enable both (more macro-generated code gets resolved).
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import hashlib
18
+ import json
19
+ import os
20
+ import platform
21
+ import posixpath
22
+ import time
23
+ from collections import defaultdict
24
+ from pathlib import Path
25
+
26
+ from ...core.model import EXACT, HEURISTIC, RESOLVED
27
+ from ...core.fsutil import keep_file
28
+ from ...core.plugin import FrameworkPlugin, GraphBuilder, LanguagePlugin, Project
29
+ from ..native import gates as G
30
+ from ...core import cache
31
+ from ..native import runner, scipread
32
+ from ..native.ts import TreeSitterMissing
33
+ from ...core.paths import PathRules, rel_dir, rules as path_rules
34
+ from .cargo import SKIP_DIRS, discover
35
+ from .syntax import ENTRY_ATTRS, ROUTE_ATTRS, RFile, RItem, extract
36
+
37
+ CODE = {"function", "method", "ffi"}
38
+ TYPES = {"struct", "enum", "union", "trait", "type_alias"}
39
+ VALUES = {"const", "static"}
40
+ SCOPES = CODE | TYPES | VALUES | {"variant", "field"}
41
+ ENTRY_PRIORITY = ["main", "ffi_export", "test", "bench", "example", "build_script", "public_api"]
42
+ # std / prelude method names: a heuristic `.name()` match on these is almost always a std call
43
+ STD_METHODS = set("""new default clone to_string to_owned into from as_ref as_mut borrow len is_empty iter iter_mut into_iter
44
+ map map_err and_then ok ok_or unwrap expect unwrap_or unwrap_or_else unwrap_or_default get get_mut insert remove push pop
45
+ contains extend collect filter fold next take skip chain zip rev count sum min max sort sort_by dedup join split trim
46
+ starts_with ends_with parse fmt write write_all read read_to_string flush lock send recv spawn await poll clear
47
+ first last keys values entry or_insert or_default with_capacity capacity reserve truncate drain retain append
48
+ is_some is_none is_ok is_err as_str as_bytes as_ptr to_vec cmp eq ne partial_cmp hash drop deref deref_mut index
49
+ call build run close open start stop find position any all display debug copied cloned""".split())
50
+ def rust_targets_setting(project=None) -> tuple[str, str]:
51
+ """(value, source) of the per-target rust-analyzer runs: CODEGRAPH_RUST_TARGETS, else `.cg.yaml` rust.targets,
52
+ else `auto`."""
53
+ env = os.environ.get("CODEGRAPH_RUST_TARGETS")
54
+ if env is not None:
55
+ return env.strip().lower() if env.strip().lower() in ("auto", "off", "none", "0", "") else env.strip(), "CODEGRAPH_RUST_TARGETS"
56
+ cfg = (getattr(project, "options", None) or {}).get("config") or {}
57
+ v = (cfg.get("rust") or {}).get("targets")
58
+ if v:
59
+ return v, f"{cfg.get('file', '.cg.yaml')} rust.targets"
60
+ return "auto", "default"
61
+
62
+
63
+ # rust-analyzer `cargo.target` per platform (CODEGRAPH_RUST_TARGETS also takes triples)
64
+ TRIPLES = {"windows": "x86_64-pc-windows-msvc", "macos": "aarch64-apple-darwin", "linux": "x86_64-unknown-linux-gnu",
65
+ "ios": "aarch64-apple-ios", "android": "aarch64-linux-android", "web": "wasm32-unknown-unknown"}
66
+ TARGET_ORDER = ("windows", "macos", "linux", "android", "ios", "web")
67
+ SCIP_KIND = {17: "function", 26: "method", 70: "method", 66: "method", 80: "method", 49: "struct", 11: "enum",
68
+ 59: "union", 53: "trait", 55: "type_alias", 54: "type_alias", 8: "const", 82: "static", 25: "macro",
69
+ 15: "field", 12: "variant", 29: "mod"}
70
+
71
+
72
+ def _rust_files(root: Path, rules: PathRules | None = None):
73
+ rules = rules or PathRules(SKIP_DIRS)
74
+ for dp, dn, fn in os.walk(root):
75
+ rd = rel_dir(root, dp)
76
+ dn[:] = rules.prune(rd, dn, dot=True)
77
+ for f in sorted(fn):
78
+ rel = f"{rd}/{f}" if rd else f
79
+ if (f.endswith(".rs") or f in ("Cargo.toml", "Cargo.lock")) and keep_file(os.path.join(dp, f)) and not rules.excluded(rel):
80
+ yield rel
81
+
82
+
83
+ class RustPlugin(LanguagePlugin):
84
+ name = "rust"
85
+
86
+ def detect(self, project: Project) -> bool:
87
+ if project.exists("Cargo.toml"):
88
+ return True
89
+ from .cargo import tauri_crates
90
+ return bool(tauri_crates(project.root))
91
+
92
+ # ------------------------------------------------------------------ main
93
+ def index(self, project: Project, builder: GraphBuilder, frameworks: list[FrameworkPlugin]) -> dict:
94
+ t0 = time.time()
95
+ root = project.root
96
+ self.root, self.b = root, builder
97
+ self.targets_setting = rust_targets_setting(project)
98
+ self._tm = None
99
+ self.rules = path_rules(project, "rust")
100
+ stats: dict = defaultdict(int)
101
+ try:
102
+ from ..native.ts import parser
103
+ parser("rust")
104
+ except TreeSitterMissing as e:
105
+ return {"status": "error", "reason": str(e)}
106
+ pkgs, how = discover(root)
107
+ stats["workspace_from"] = how
108
+ stats["packages"] = len(pkgs)
109
+ self.pkgs = pkgs
110
+ self.pkg_features = {p.name: p.features for p in pkgs}
111
+ scen = (project.options.get("gates") or [None])[0]
112
+ self.scen = G.Scenario(scen, self.pkg_features)
113
+ self.files: dict[str, RFile] = {}
114
+ self.file_meta: dict[str, dict] = {}
115
+ self._module_trees(pkgs, stats)
116
+ stats["files"] = len(self.files)
117
+ orphans = [f for f in _rust_files(root, self.rules) if f.endswith(".rs") and f not in self.files
118
+ and "/target/" not in f and not f.startswith("target/")]
119
+ stats["orphan_rs_files"] = len(orphans)
120
+ # coverage: .rs files outside every crate's module tree are not in the graph
121
+ self.file_report = {"seen": list(self.files) + orphans, "unmapped": orphans,
122
+ "syntax_errors": {r: f.error_spans for r, f in self.files.items() if f.error_spans}}
123
+ self._indexes()
124
+ self._reexports()
125
+ self._emit_structure(stats)
126
+ # semantic layer
127
+ mode = "heuristic"
128
+ scip_info = None
129
+ if os.environ.get("CODEGRAPH_RUST_SCIP", "1") != "0":
130
+ scip_path, scip_info = self._run_scip()
131
+ if scip_path:
132
+ self._import_scip(scip_path, stats)
133
+ mode = "scip"
134
+ if self._scip_warning:
135
+ scip_info["warning"] = self._scip_warning
136
+ tinfo = self._target_scips(stats)
137
+ if tinfo:
138
+ scip_info["targets"] = tinfo
139
+ if mode == "heuristic":
140
+ self._heuristic_refs(stats)
141
+ else:
142
+ self._cfg_inactive_refs(stats)
143
+ self._impls(stats, mode)
144
+ self._facts(stats, mode)
145
+ self._entries(stats)
146
+ self._gating(stats)
147
+ self._platform_marks()
148
+ out = dict(stats)
149
+ out.update({"mode": mode, "seconds": round(time.time() - t0, 2)})
150
+ if scip_info:
151
+ out["scip"] = scip_info
152
+ return out
153
+
154
+ # ------------------------------------------------------------------ module trees
155
+ def _module_trees(self, pkgs, stats):
156
+ order = {"lib": 0, "bin": 1, "build": 2, "example": 3, "bench": 4, "test": 5}
157
+ for pkg in pkgs:
158
+ for tgt in sorted(pkg.targets, key=lambda t: (order.get(t.kind, 9), t.name)):
159
+ if not (self.root / tgt.src).exists():
160
+ stats["missing_target_roots"] += 1
161
+ continue
162
+ todo = [(tgt.src, tgt.crate, [], False, tgt.kind == "lib", True)]
163
+ while todo:
164
+ rel, module, cfgs, in_test, pub_chain, is_root = todo.pop(0)
165
+ if rel in self.files:
166
+ self.file_meta[rel].setdefault("also_in", []).append(tgt.crate)
167
+ continue
168
+ if self.rules.excluded(rel): # .cg.yaml exclude: the file and the modules it declares
169
+ stats["excluded_files"] += 1
170
+ continue
171
+ try:
172
+ src = (self.root / rel).read_bytes()
173
+ except OSError:
174
+ continue
175
+ rf = extract(rel, src, tgt.crate, module, cfgs, in_test or tgt.kind in ("test", "bench"), pub_chain)
176
+ self.files[rel] = rf
177
+ self.file_meta[rel] = {"pkg": pkg, "target": tgt, "is_root": is_root, "cfgs": cfgs}
178
+ p = Path(rel)
179
+ mod_rs = is_root or p.name in ("mod.rs", "lib.rs", "main.rs")
180
+ base = p.parent if mod_rs else p.parent / p.stem
181
+ for md in rf.mods:
182
+ if getattr(md, "inline", False):
183
+ continue
184
+ extra = md.module.split("::")[len(module.split("::")):-1]
185
+ if md.path_attr:
186
+ cand = [(p.parent if not extra else base.joinpath(*extra)) / md.path_attr]
187
+ else:
188
+ d = base.joinpath(*extra) if extra else base
189
+ cand = [d / f"{md.name}.rs", d / md.name / "mod.rs"]
190
+ hit = next((c for c in cand if (self.root / c).exists()), None)
191
+ if hit is None:
192
+ stats["unresolved_mod_decls"] += 1
193
+ continue
194
+ # `#[path = "../../x.rs"]`: one path per file (src/../../x.rs is x.rs); outside the root: skipped
195
+ hrel = posixpath.normpath(hit.as_posix())
196
+ if hrel == ".." or hrel.startswith("../"):
197
+ stats["mod_path_outside_root"] += 1
198
+ continue
199
+ todo.append((hrel, md.module, md.cfgs, md.in_test, md.pub_chain, False))
200
+
201
+ def _indexes(self):
202
+ self.items: list[RItem] = [it for rf in self.files.values() for it in rf.items]
203
+ self.by_key: dict[str, RItem] = {}
204
+ self.dupes = 0
205
+ for it in self.items:
206
+ if it.kind == "impl":
207
+ continue
208
+ if it.key in self.by_key:
209
+ prev = self.by_key[it.key]
210
+ if prev.kind == it.kind or {prev.kind, it.kind} <= CODE:
211
+ # cfg-alternative definitions (#[cfg(unix)] fn x / #[cfg(windows)] fn x) or macro-repeated names:
212
+ # the first keeps the plain key, later ones get @line
213
+ self.dupes += 1
214
+ it.key = f"{it.key}@{it.line}" if it.file == prev.file else f"{it.key}@{it.file}:{it.line}"
215
+ self.by_key.setdefault(it.key, it)
216
+ self.by_pos: dict[tuple, RItem] = {(it.file, it.line, it.name): it for it in self.items if it.kind != "impl"}
217
+ self.by_name: dict[str, list[RItem]] = defaultdict(list)
218
+ for it in self.items:
219
+ if it.kind != "impl":
220
+ self.by_name[it.name].append(it)
221
+ self.children: dict[str, dict[str, list[RItem]]] = defaultdict(lambda: defaultdict(list))
222
+ for it in self.items:
223
+ if it.kind == "impl":
224
+ continue
225
+ par = it.parent or it.module
226
+ if it.kind == "mod":
227
+ par = it.key.rsplit("::", 1)[0]
228
+ self.children[par][it.name].append(it)
229
+ self.lib_crates = {t.crate for p in self.pkgs for t in p.targets if t.kind == "lib"}
230
+ self.global_uses: dict[tuple, str] = {}
231
+ for rf in self.files.values():
232
+ for k, v in rf.use_map.items():
233
+ self.global_uses.setdefault(k, v)
234
+
235
+ def nid(self, it: RItem) -> str:
236
+ return f"{it.kind}:{it.key}"
237
+
238
+ def _reexports(self):
239
+ """`pub use a::b::C` / `pub use m::*` in a library makes the target public API even if its module is private."""
240
+ for _ in range(2):
241
+ for rf in self.files.values():
242
+ if self.file_meta[rf.path]["target"].kind != "lib":
243
+ continue
244
+ for module, path, _line in rf.pub_uses:
245
+ glob = path.endswith("::*")
246
+ full = self._abs_path(path[:-3] if glob else path, module, rf.crate)
247
+ if not full:
248
+ continue
249
+ if glob:
250
+ for lst in self.children.get(full, {}).values():
251
+ for it in lst:
252
+ if it.vis and it.vis.strip() == "pub":
253
+ self._mark_pub(it)
254
+ continue
255
+ it = self.by_key.get(full)
256
+ if it is not None:
257
+ self._mark_pub(it)
258
+
259
+ def _mark_pub(self, it: RItem):
260
+ if it.pub_chain:
261
+ return
262
+ it.pub_chain = True
263
+ if it.kind in TYPES or it.kind == "mod":
264
+ pre = it.key
265
+ mod = it.key.rsplit("::", 1)[0]
266
+ for x in self.items:
267
+ if x.kind in ("method", "const") and (x.parent == pre or (x.parent or "").startswith(f"{mod}::<{it.name} as ")):
268
+ if (x.vis and x.vis.strip() == "pub") or (x.parent or "").startswith(f"{mod}::<") or it.kind == "trait":
269
+ x.pub_chain = True
270
+ if it.kind == "mod" and x.module == it.key and x.vis and x.vis.strip() == "pub":
271
+ x.pub_chain = True
272
+
273
+ def _abs_path(self, path: str, module: str, crate: str) -> str | None:
274
+ """Resolve a path written in `module` to an item / module key (workspace crates only; follows `use`
275
+ re-exports a few levels)."""
276
+ segs = [s for s in path.split("::") if s]
277
+ if not segs:
278
+ return None
279
+ mod_parts = module.split("::")
280
+ root = mod_parts[0]
281
+ if segs[0] == "crate":
282
+ segs = [root] + segs[1:]
283
+ elif segs[0] == "self":
284
+ segs = mod_parts + segs[1:]
285
+ elif segs[0] == "super":
286
+ up = mod_parts[:]
287
+ while segs and segs[0] == "super":
288
+ up = up[:-1] if len(up) > 1 else up
289
+ segs = segs[1:]
290
+ segs = up + segs
291
+ elif segs[0] not in self.lib_crates:
292
+ for base in (mod_parts, [root]):
293
+ c = self._canon("::".join(base + segs))
294
+ if c:
295
+ return c
296
+ return None
297
+ return self._canon("::".join(segs)) or "::".join(segs)
298
+
299
+ def _canon(self, full: str, depth: int = 0) -> str | None:
300
+ if full in self.by_key or full in self.children:
301
+ return full
302
+ if depth > 4 or "::" not in full:
303
+ return None
304
+ mod, name = full.rsplit("::", 1)
305
+ base = mod if mod in self.children else self._canon(mod, depth + 1)
306
+ if base is None:
307
+ return None
308
+ if f"{base}::{name}" in self.by_key:
309
+ return f"{base}::{name}"
310
+ alias = self.global_uses.get((base, name))
311
+ if alias:
312
+ segs = [s for s in alias.split("::") if s]
313
+ mp = base.split("::")
314
+ if segs[0] == "crate":
315
+ segs = mp[:1] + segs[1:]
316
+ elif segs[0] == "self":
317
+ segs = mp + segs[1:]
318
+ elif segs[0] == "super":
319
+ segs = mp[:-1] + segs[1:]
320
+ elif segs[0] not in self.lib_crates:
321
+ segs = mp + segs
322
+ return self._canon("::".join(segs), depth + 1)
323
+ return None
324
+
325
+ # ------------------------------------------------------------------ structure nodes
326
+ def _emit_structure(self, stats):
327
+ b = self.b
328
+ for pkg in self.pkgs:
329
+ for f, enables in sorted(pkg.features.items()):
330
+ b.add_node("feature", f"{pkg.name}/{f}", name=f"{pkg.name}/{f}", file=pkg.manifest, lang="rust",
331
+ module=pkg.name, attrs={"enables": enables, "package": pkg.name})
332
+ for t in pkg.targets:
333
+ b.add_node("crate", t.crate, name=t.crate, file=t.src, line=1, lang="rust", module=t.crate,
334
+ attrs={"package": pkg.name, "target_kind": t.kind, "crate_types": t.crate_types,
335
+ "required_features": t.required_features, "version": pkg.version})
336
+ libs = {p.name: next((t.crate for t in p.targets if t.kind == "lib"), None) for p in self.pkgs}
337
+ for pkg in self.pkgs:
338
+ for t in pkg.targets:
339
+ lib_self = libs.get(pkg.name)
340
+ if lib_self and t.kind != "lib":
341
+ b.add_edge(f"crate:{t.crate}", f"crate:{lib_self}", "IMPORTS", pkg.manifest, None, EXACT)
342
+ for d in pkg.deps:
343
+ if libs.get(d):
344
+ b.add_edge(f"crate:{t.crate}", f"crate:{libs[d]}", "IMPORTS", pkg.manifest, None, EXACT)
345
+ for rf in self.files.values():
346
+ meta = self.file_meta[rf.path]
347
+ if meta["is_root"]:
348
+ b.add_node("crate", rf.crate, doc=rf.file_doc)
349
+ else:
350
+ b.add_node("mod", rf.module, name=rf.module, file=rf.path, line=1, lang="rust", module=rf.module,
351
+ doc=rf.file_doc, attrs={"crate": rf.crate})
352
+ for it in self.items:
353
+ if it.kind == "impl":
354
+ continue
355
+ attrs = {}
356
+ if it.vis:
357
+ attrs["vis"] = it.vis
358
+ if it.cfgs:
359
+ attrs["cfg"] = [c for c, _ in it.cfgs]
360
+ if it.unsafe:
361
+ attrs["unsafe"] = True
362
+ if it.is_async:
363
+ attrs["async"] = True
364
+ if it.abi:
365
+ attrs["abi"] = it.abi
366
+ if it.in_test:
367
+ attrs["test_only"] = True
368
+ if it.impl_trait and it.kind == "method" and it.impl_self:
369
+ attrs["trait"] = it.impl_trait
370
+ sel = [a for a in it.attrs if a in ("no_mangle", "export_name", "unsafe", "inline", "deprecated", "derive", "mut")
371
+ or a in ENTRY_ATTRS or a.split("::")[-1] in ROUTE_ATTRS]
372
+ if sel:
373
+ attrs["attributes"] = sel
374
+ if it.pub_chain:
375
+ attrs["public"] = True
376
+ fqn = it.key
377
+ n = self.b.add_node(it.kind, it.key, name=it.name, fqn=fqn, file=it.file, line=it.line if it.kind != "mod" else it.start,
378
+ end_line=it.end, module=it.module, doc=it.doc, lang="rust", attrs=attrs)
379
+ par = it.parent if it.parent and it.parent in self.by_key else None
380
+ if par:
381
+ self.b.add_edge(self.nid(self.by_key[par]), n, "CONTAINS", it.file, it.line, EXACT)
382
+ elif it.kind != "mod":
383
+ owner = f"mod:{it.module}" if not self.file_meta[it.file]["is_root"] or it.module != self.files[it.file].crate else f"crate:{it.module}"
384
+ if it.module == self.files[it.file].crate:
385
+ owner = f"crate:{it.module}"
386
+ elif f"mod:{it.module}" not in self.b.nodes:
387
+ owner = None
388
+ if owner:
389
+ self.b.add_edge(owner, n, "CONTAINS", it.file, it.line, EXACT)
390
+ stats[f"items_{it.kind}"] += 1
391
+ stats["duplicate_keys"] = self.dupes
392
+
393
+ # ------------------------------------------------------------------ SCIP
394
+ def _run_scip(self, triple: str | None = None):
395
+ ra = runner.find_tool("CODEGRAPH_RUST_ANALYZER", ["rust-analyzer"], [Path.home() / ".cargo" / "bin"])
396
+ pre = os.environ.get("CODEGRAPH_RUST_SCIP_FILE")
397
+ if pre and triple is None:
398
+ return (Path(pre) if Path(pre).exists() else None), {"source": "CODEGRAPH_RUST_SCIP_FILE", "path": pre}
399
+ if not ra:
400
+ return None, {"status": "rust-analyzer not installed; heuristic mode (see docs/native.md)"}
401
+ unsafe_ok = os.environ.get("CODEGRAPH_RUST_BUILD_SCRIPTS") == "1"
402
+ cfg = {"cargo": {"buildScripts": {"enable": unsafe_ok}, "features": "all"},
403
+ "procMacro": {"enable": unsafe_ok}}
404
+ if triple:
405
+ cfg["cargo"]["target"] = triple
406
+ cfg_text = json.dumps(cfg, sort_keys=True)
407
+ # named by content and written atomically: a concurrent run never reads a half-written config
408
+ cfg_path = (runner.cache_dir() / f"ra-config-{cache.root_key(self.root)}-"
409
+ f"{hashlib.sha256(cfg_text.encode()).hexdigest()[:12]}.json")
410
+ runner.write_atomic(cfg_path, cfg_text)
411
+ ver = runner.tool_version(ra)
412
+ files = list(_rust_files(self.root, getattr(self, "rules", None)))
413
+ key = runner.fingerprint(self.root, files, f"{ver}|{json.dumps(cfg, sort_keys=True)}")
414
+ timeout = int(os.environ.get("CODEGRAPH_INDEXER_TIMEOUT", "3600"))
415
+ env_path = os.environ.get("PATH", "")
416
+ cargo_bin = str(Path.home() / ".cargo" / "bin")
417
+ if cargo_bin not in env_path.split(os.pathsep) and Path(cargo_bin).is_dir():
418
+ os.environ["PATH"] = cargo_bin + os.pathsep + env_path
419
+ path, info = runner.run_cached("rust", key, [ra, "scip", str(self.root), "--config-path", str(cfg_path)],
420
+ self.root, "--output", timeout)
421
+ info.update({"indexer": ver, "build_scripts_and_proc_macros": unsafe_ok})
422
+ return path, info
423
+
424
+ def _target_scips(self, stats) -> dict:
425
+ """One more rust-analyzer run per target the cfg conditions name but the host does not build (Windows / macOS
426
+ code on a Linux host): the references under those conditions get exact edges instead of the syntactic
427
+ fallback (`via: cfg-inactive`). CODEGRAPH_RUST_TARGETS=0 turns it off, `windows,macos` (or target triples)
428
+ picks the targets; default `auto`: up to 3 targets named by the project's cfg conditions."""
429
+ want = getattr(self, "targets_setting", ("auto", "default"))[0]
430
+ if want in ("0", "", "off", "none") or os.environ.get("CODEGRAPH_RUST_SCIP_FILE"):
431
+ return {}
432
+ from ...platforms import KNOWN, cfg_cond, norm
433
+ host = norm(platform.system()) or "linux"
434
+ if want == "auto":
435
+ need = []
436
+ for rf in self.files.values():
437
+ preds = [p for p, _ in self.file_meta[rf.path]["cfgs"] + rf.inner_cfgs]
438
+ preds += [p for it in rf.items for p, _ in it.cfgs] + [r[2] for r in rf.cfg_regions]
439
+ for pred in preds:
440
+ try:
441
+ v = cfg_cond(pred).values()
442
+ except Exception: # pragma: no cover - a cfg the parser does not take
443
+ continue
444
+ if v.get(host) is False:
445
+ need += [p for p in KNOWN if v.get(p) is True and p != host and p not in need]
446
+ order = [p for p in TARGET_ORDER if p in need][:3]
447
+ else:
448
+ order = [x.strip() for x in want.split(",") if x.strip()]
449
+ out = {}
450
+ for p in order:
451
+ triple = TRIPLES.get(norm(p) or "", p if "-" in p else None)
452
+ name = norm(p) or p
453
+ if not triple or name == host:
454
+ continue
455
+ t0 = time.time()
456
+ path, info = self._run_scip(triple)
457
+ before = stats.get("target_scip_refs", 0)
458
+ if path:
459
+ try:
460
+ self._import_scip(path, stats, target=name)
461
+ except Exception as e: # pragma: no cover - a broken index: keep the host layer
462
+ info["error"] = f"import failed: {e}"
463
+ out[name] = {"triple": triple, "cache": info.get("cache"), "refs_added": stats.get("target_scip_refs", 0) - before,
464
+ "seconds": round(time.time() - t0, 2)}
465
+ if info.get("error"):
466
+ out[name]["error"] = info["error"]
467
+ return out
468
+
469
+ def _scope_painter(self, rel: str):
470
+ """line -> innermost item (by syntactic range) for a file."""
471
+ rf = self.files.get(rel)
472
+ if rf is None:
473
+ return None
474
+ n = len(rf.lines) + 2
475
+ paint = [None] * n
476
+ scopes = [it for it in rf.items if it.kind in SCOPES]
477
+ for it in sorted(scopes, key=lambda x: -(x.end - x.start)):
478
+ for ln in range(max(0, it.start), min(n, it.end + 1)):
479
+ paint[ln] = it
480
+ return paint
481
+
482
+ def _import_scip(self, path: Path, stats, target: str | None = None):
483
+ """Import a rust-analyzer SCIP index. `target` (a platform name): an index built for another target
484
+ (cargo.target); only its definitions missing from the host index and its references at positions the host
485
+ index did not resolve (code under a cfg that is false on the host) are added, tagged `exact_target`."""
486
+ idx = scipread.load(path)
487
+ if target is None:
488
+ stats["scip_documents"] = len(idx.docs)
489
+ self.sym_nodes = defaultdict(list)
490
+ self.pos_sym: dict[tuple, str] = {}
491
+ # symbol -> [(file, nid)]
492
+ sym_nodes = self.sym_nodes
493
+ host_pos = set(self.pos_sym) if target else ()
494
+ known = set(sym_nodes) if target else ()
495
+ for sym, defs in idx.defs.items():
496
+ if sym in known:
497
+ continue
498
+ for rel, o in defs:
499
+ d = scipread.descriptors(sym)
500
+ if not d or not d[1]:
501
+ continue
502
+ name = d[1][-1][0]
503
+ it = self.by_pos.get((rel, o.line + 1, name))
504
+ if it is None:
505
+ nid = self._synthetic_node(rel, o, sym, d, idx)
506
+ if nid is None:
507
+ continue
508
+ stats["scip_defs_synthetic" if target is None else "target_scip_defs_synthetic"] += 1
509
+ else:
510
+ nid = self.nid(it)
511
+ stats["scip_defs_matched" if target is None else "target_scip_defs_matched"] += 1
512
+ sym_nodes[sym].append((rel, nid))
513
+ if target is None:
514
+ self._scip_warning = scipread.native_warning(path, idx, stats)
515
+ attrs = {"exact_target": target} if target else {}
516
+ for rel, doc in idx.docs.items():
517
+ rf = self.files.get(rel)
518
+ if rf is None:
519
+ stats["scip_docs_outside_module_tree"] += 1
520
+ continue
521
+ paint = self._scope_painter(rel)
522
+ uses = rf.use_ranges
523
+ lines = rf.lines
524
+ target = self.file_meta[rel]["target"].crate
525
+ for o in doc.occs:
526
+ if target and (rel, o.line + 1, o.col) in host_pos:
527
+ continue
528
+ self.pos_sym[(rel, o.line + 1, o.col)] = o.symbol
529
+ if o.roles & scipread.DEFINITION:
530
+ continue
531
+ cands = sym_nodes.get(o.symbol)
532
+ if not cands:
533
+ continue
534
+ ln = o.line + 1
535
+ if any(a <= ln <= b for a, b in uses):
536
+ continue
537
+ owner = paint[ln] if ln < len(paint) else None
538
+ if owner is None:
539
+ stats["scip_refs_outside_items"] += 1
540
+ continue
541
+ if len(cands) > 1:
542
+ same = [c for c in cands if c[0] == rel] or [c for c in cands if self.file_meta.get(c[0], {}).get("target") and self.file_meta[c[0]]["target"].crate == target]
543
+ cands2 = same[:1] if same else cands[:5]
544
+ else:
545
+ cands2 = cands
546
+ line_txt = lines[o.line] if o.line < len(lines) else ""
547
+ before = line_txt[:o.col].rstrip()
548
+ after = line_txt[o.end_col:].lstrip()
549
+ for _, dst in cands2:
550
+ conf = HEURISTIC if len(cands2) > 1 else (RESOLVED if before.endswith(".") else EXACT)
551
+ self._ref_edge(self.nid(owner), dst, rel, ln, conf, after, stats, **attrs)
552
+ if target:
553
+ stats["target_scip_refs"] += 1
554
+
555
+ def _ref_edge(self, src: str, dst: str, rel: str, ln: int, conf: str, after: str, stats, **attrs):
556
+ kind = dst.split(":", 1)[0]
557
+ if kind in CODE:
558
+ called = after.startswith("(") or after.startswith("::<")
559
+ ek = "CALLS" if called else "REFERENCES_FN"
560
+ node = self.b.nodes.get(dst)
561
+ if node is not None and node.kind == "method" and node.attrs.get("trait") is None and dst in self._trait_methods:
562
+ attrs["dispatch"] = "trait"
563
+ stats["calls_trait_dispatch"] += 1
564
+ elif kind == "macro":
565
+ ek = "CALLS"
566
+ elif kind in TYPES:
567
+ ek = "USES_TYPE"
568
+ elif kind in ("field", "variant"):
569
+ ek = "ACCESSES_FIELD"
570
+ elif kind in VALUES:
571
+ ek = "USES_VALUE"
572
+ else:
573
+ return
574
+ if src == dst:
575
+ return
576
+ self.b.add_edge(src, dst, ek, rel, ln, conf, **attrs)
577
+ stats[f"edges_{ek}"] += 1
578
+
579
+ @property
580
+ def _trait_methods(self) -> set:
581
+ tm = getattr(self, "_tm", None)
582
+ if tm is None:
583
+ tm = self._tm = {self.nid(it) for it in self.items if it.kind == "method" and it.parent and
584
+ self.by_key.get(it.parent) is not None and self.by_key[it.parent].kind == "trait"}
585
+ return tm
586
+
587
+ def _synthetic_node(self, rel, o, sym, d, idx) -> str | None:
588
+ """A SCIP definition with no syntactic item (macro-generated, unusual syntax): derive a key from the
589
+ descriptor path under the file's crate."""
590
+ rf = self.files.get(rel)
591
+ if rf is None:
592
+ return None
593
+ si = idx.docs[rel].symbols.get(sym)
594
+ kind = SCIP_KIND.get(si.kind if si is not None else 0)
595
+ names = d[1]
596
+ last, suf = names[-1]
597
+ if kind is None:
598
+ kind = {"(": "function", "#": "struct", "!": "macro"}.get(suf)
599
+ if kind is None or kind == "mod":
600
+ return None
601
+ parts, i = [], 0
602
+ while i < len(names):
603
+ nm, s = names[i]
604
+ if s == "#" and nm == "impl":
605
+ tps = []
606
+ while i + 1 < len(names) and names[i + 1][1] == "[":
607
+ tps.append(names[i + 1][0])
608
+ i += 1
609
+ parts.append(f"<{tps[0]} as {tps[1]}>" if len(tps) > 1 else (tps[0] if tps else "impl"))
610
+ elif s != "[":
611
+ parts.append(nm)
612
+ i += 1
613
+ crate = rf.crate
614
+ key = "::".join([crate] + parts)
615
+ if kind == "function" and any(s == "#" for _, s in names[:-1]):
616
+ kind = "method"
617
+ nid = self.b.add_node(kind, key, name=last, fqn=key, file=rel, line=o.line + 1, module=rf.module, lang="rust",
618
+ attrs={"from": "scip", "macro_generated": True})
619
+ return nid
620
+
621
+ # ------------------------------------------------------------------ heuristic resolution
622
+ def _resolve_path(self, rf: RFile, owner: RItem, path: str) -> tuple[list[RItem], str]:
623
+ segs = [s.split("<")[0] for s in path.split("::") if s]
624
+ if not segs:
625
+ return [], HEURISTIC
626
+ module = owner.module
627
+ if segs[0] == "Self" and owner.impl_self:
628
+ segs = [owner.impl_self] + segs[1:]
629
+ alias = rf.use_map.get((module, segs[0]))
630
+ if alias:
631
+ segs = alias.split("::") + segs[1:]
632
+ full = self._abs_path("::".join(segs), module, rf.crate)
633
+ if full and full in self.by_key and self.by_key[full].kind in CODE | {"variant"}:
634
+ return [self.by_key[full]], RESOLVED
635
+ name = segs[-1]
636
+ cands = [c for c in self.by_name.get(name, []) if c.kind in CODE]
637
+ if len(segs) >= 2:
638
+ ty = segs[-2]
639
+ tc = [c for c in cands if c.parent and (c.parent.endswith(f"::{ty}") or f"<{ty} as " in c.parent)]
640
+ if tc:
641
+ return (tc if len(tc) <= 3 else []), HEURISTIC
642
+ if ty[:1].isupper():
643
+ return [], HEURISTIC
644
+ same_mod = [c for c in cands if c.module == module and c.kind != "method"]
645
+ if same_mod:
646
+ return same_mod[:1], RESOLVED
647
+ if len(segs) == 1:
648
+ # `use super::*;` / `use crate::m::*;` glob imports (typical in `mod tests`)
649
+ for (m, a), gpath in rf.use_map.items():
650
+ if m != module or not a.startswith("*"):
651
+ continue
652
+ gmod = self._abs_path(gpath, module, rf.crate)
653
+ hit = [c for c in cands if c.module == gmod and c.kind != "method"]
654
+ if hit:
655
+ return hit[:1], RESOLVED
656
+ free = [c for c in cands if c.kind in ("function", "ffi")]
657
+ if len(free) == 1:
658
+ return free, HEURISTIC
659
+ return [], HEURISTIC
660
+
661
+ def _resolve_call(self, rf: RFile, owner: RItem, form: str, path: str, name: str, stats) -> tuple[list[RItem], str]:
662
+ """Syntactic call resolution (heuristic mode, and code rust-analyzer does not analyse)."""
663
+ if form == "path":
664
+ return self._resolve_path(rf, owner, path)
665
+ if name in STD_METHODS:
666
+ stats["heuristic_skipped_std_method"] += 1
667
+ return None, HEURISTIC
668
+ cands = [c for c in self.by_name.get(name, []) if c.kind == "method"]
669
+ traits = {c.parent for c in cands if c.parent and self.by_key.get(c.parent) is not None and self.by_key[c.parent].kind == "trait"}
670
+ if len(cands) == 1:
671
+ return cands, HEURISTIC
672
+ if len(traits) == 1:
673
+ return [c for c in cands if c.parent in traits][:1], HEURISTIC
674
+ return [], HEURISTIC
675
+
676
+ def _call_edges(self, rf: RFile, owner: RItem, form, path, name, line, col, stats, **attrs) -> bool:
677
+ if form == "macro":
678
+ m = [c for c in self.by_name.get(name, []) if c.kind == "macro"]
679
+ if len(m) == 1:
680
+ self.b.add_edge(self.nid(owner), self.nid(m[0]), "CALLS", rf.path, line, HEURISTIC, **attrs)
681
+ return True
682
+ return False
683
+ targets, conf = self._resolve_call(rf, owner, form, path, name, stats)
684
+ if targets is None:
685
+ return None
686
+ if not targets:
687
+ return False
688
+ line_txt = rf.lines[line - 1] if line - 1 < len(rf.lines) else ""
689
+ for t in targets:
690
+ self._ref_edge(self.nid(owner), self.nid(t), rf.path, line, conf, line_txt[col + len(name):].lstrip() or "(", stats, **attrs)
691
+ return True
692
+
693
+ def _cfg_inactive_refs(self, stats):
694
+ """rust-analyzer analyses the host target: calls inside code compiled only for other targets (and calls to
695
+ items that exist only there) have no SCIP occurrence. Those call sites are resolved syntactically instead,
696
+ so `#[cfg(windows)]` code keeps its callees and callers on every target."""
697
+ pos = getattr(self, "pos_sym", {})
698
+ for rf in self.files.values():
699
+ regions = [(a, z) for a, z, _p, _l, _o in rf.cfg_regions]
700
+ file_cfg = bool(self.file_meta[rf.path]["cfgs"] or rf.inner_cfgs)
701
+ for owner_key, form, path, name, line, col in rf.calls:
702
+ if (rf.path, line, col) in pos or form == "macro" or (form == "method" and name in STD_METHODS):
703
+ continue
704
+ owner = self.by_key.get(owner_key)
705
+ if owner is None:
706
+ continue
707
+ site_cfg = file_cfg or bool(owner.cfgs) or any(a <= line <= z for a, z in regions)
708
+ if not site_cfg:
709
+ targets, _ = self._resolve_call(rf, owner, form, path, name, stats)
710
+ if not any(t.cfgs or self.file_meta.get(t.file, {}).get("cfgs") for t in targets or []):
711
+ continue
712
+ if self._call_edges(rf, owner, form, path, name, line, col, stats, via="cfg-inactive"):
713
+ stats["cfg_inactive_calls"] += 1
714
+
715
+ def _platform_marks(self):
716
+ """Platform conditions (codegraph/platforms.py): #[cfg] on items, mod declarations, #![cfg] and statements."""
717
+ from ...platforms import cfg_cond, mark
718
+ b = self.b
719
+ for rf in self.files.values():
720
+ for pred, line in self.file_meta[rf.path]["cfgs"] + rf.inner_cfgs:
721
+ mark(b, rf.path, 1, 10 ** 9, cfg_cond(pred), line=line)
722
+ for it in rf.items:
723
+ for pred, line in it.cfgs:
724
+ mark(b, rf.path, it.start, it.end, cfg_cond(pred), line=line)
725
+ for start, end, pred, aline, _owner in rf.cfg_regions:
726
+ mark(b, rf.path, start, end, cfg_cond(pred), line=aline, nodes=False)
727
+
728
+ def _heuristic_refs(self, stats):
729
+ for rf in self.files.values():
730
+ for owner_key, form, path, name, line, col in rf.calls:
731
+ owner = self.by_key.get(owner_key)
732
+ if owner is None:
733
+ continue
734
+ if self._call_edges(rf, owner, form, path, name, line, col, stats) is False and form != "macro":
735
+ stats["heuristic_unresolved_calls"] += 1
736
+ for owner_key, name, line, col in rf.type_refs:
737
+ owner = self.by_key.get(owner_key)
738
+ if owner is None:
739
+ continue
740
+ cands = [c for c in self.by_name.get(name, []) if c.kind in TYPES]
741
+ local = [c for c in cands if c.module == owner.module]
742
+ alias = rf.use_map.get((owner.module, name))
743
+ if alias:
744
+ full = self._abs_path(alias, owner.module, rf.crate)
745
+ if full in self.by_key and self.by_key[full].kind in TYPES:
746
+ local = [self.by_key[full]]
747
+ pick = local[:1] or (cands if len(cands) == 1 else [])
748
+ for t in pick:
749
+ if self.nid(t) != self.nid(owner):
750
+ self.b.add_edge(self.nid(owner), self.nid(t), "USES_TYPE", rf.path, line, RESOLVED if local else HEURISTIC)
751
+ stats["edges_USES_TYPE"] += 1
752
+
753
+ def _lookup_at(self, rel: str, line: int, col: int) -> list[str]:
754
+ sym = getattr(self, "pos_sym", {}).get((rel, line, col))
755
+ if sym:
756
+ return [n for _, n in self.sym_nodes.get(sym, [])]
757
+ return []
758
+
759
+ # ------------------------------------------------------------------ impls / dispatch
760
+ def _impls(self, stats, mode):
761
+ conf = RESOLVED if mode == "scip" else HEURISTIC
762
+ for rf in self.files.values():
763
+ for imp in (it for it in rf.items if it.kind == "impl"):
764
+ self_ids = self._lookup_at(rf.path, *imp.impl_self_pos) if imp.impl_self_pos else []
765
+ if not self_ids:
766
+ c = [x for x in self.by_name.get(imp.impl_self, []) if x.kind in TYPES - {"trait"}]
767
+ loc = [x for x in c if x.module == imp.module]
768
+ self_ids = [self.nid(x) for x in (loc[:1] or (c if len(c) == 1 else []))]
769
+ if not imp.impl_trait:
770
+ continue
771
+ stats["trait_impls"] += 1
772
+ tname = imp.impl_trait.split("<")[0]
773
+ trait_ids = [t for t in (self._lookup_at(rf.path, *imp.impl_trait_pos) if imp.impl_trait_pos else []) if t.startswith("trait:")]
774
+ tconf = conf
775
+ if not trait_ids:
776
+ alias = rf.use_map.get((imp.module, tname))
777
+ cands = [x for x in self.by_name.get(tname, []) if x.kind == "trait"]
778
+ if alias:
779
+ full = self._abs_path(alias, imp.module, rf.crate)
780
+ cands = [x for x in cands if x.key == full] or cands
781
+ loc = [x for x in cands if x.module == imp.module]
782
+ pick = loc[:1] or (cands if len(cands) == 1 else [])
783
+ trait_ids = [self.nid(x) for x in pick]
784
+ tconf = HEURISTIC if mode == "scip" and pick else conf
785
+ if not trait_ids:
786
+ # impl of a std / dependency trait (Default, From, Display, Drop, Iterator, a framework's Widget...):
787
+ # its methods are invoked implicitly by code outside the workspace, so link them from the Self type
788
+ # (heuristic): whatever uses the type may run them.
789
+ stats["trait_impls_external_trait"] += 1
790
+ for m in self.items:
791
+ if m.kind == "method" and m.parent == imp.key:
792
+ n = self.b.nodes.get(self.nid(m))
793
+ if n is not None:
794
+ n.attrs["external_trait"] = imp.impl_trait
795
+ for sid in self_ids:
796
+ self.b.add_edge(sid, self.nid(m), "IMPLEMENTED_BY", rf.path, m.line, HEURISTIC,
797
+ via="external_trait", trait=imp.impl_trait)
798
+ stats["external_trait_impl_edges"] += 1
799
+ continue
800
+ tid = trait_ids[0]
801
+ for sid in self_ids:
802
+ self.b.add_edge(sid, tid, "IMPLEMENTS", rf.path, imp.line, tconf)
803
+ tkey = tid.split(":", 1)[1]
804
+ for m in self.items:
805
+ if m.kind != "method" or m.parent != imp.key:
806
+ continue
807
+ tm = f"method:{tkey}::{m.name}"
808
+ if tm in self.b.nodes:
809
+ self.b.add_edge(tm, self.nid(m), "IMPLEMENTED_BY", rf.path, m.line, tconf)
810
+ stats["implemented_by_edges"] += 1
811
+ else:
812
+ stats["impl_methods_without_trait_decl"] += 1
813
+
814
+ # ------------------------------------------------------------------ facts: env, unsafe, ffi, routes, cfg
815
+ def _facts(self, stats, mode):
816
+ b = self.b
817
+ for rf in self.files.values():
818
+ pkg = self.file_meta[rf.path]["pkg"].name
819
+ for key, line, owner_key, how in rf.env:
820
+ owner = self.by_key.get(owner_key)
821
+ if owner is None:
822
+ continue
823
+ b.add_node("env", key, name=key, lang="rust", attrs={"source": "compile-time" if how.endswith("!") else "runtime"})
824
+ b.add_edge(self.nid(owner), f"env:{key}", "READS_ENV", rf.path, line, EXACT, how=how)
825
+ stats["env_reads"] += 1
826
+ for owner_key, path, line, how in rf.attr_refs:
827
+ owner = self.by_key.get(owner_key)
828
+ if owner is None:
829
+ continue
830
+ targets, conf = self._resolve_path(rf, owner, path)
831
+ if not targets and how == "with":
832
+ for fn in ("serialize", "deserialize"):
833
+ t, conf = self._resolve_path(rf, owner, f"{path}::{fn}")
834
+ targets += t
835
+ for t in targets:
836
+ b.add_edge(self.nid(owner), self.nid(t), "REFERENCES_FN", rf.path, line, HEURISTIC if conf == HEURISTIC else RESOLVED,
837
+ via=f"attribute {how}=")
838
+ stats["attribute_fn_refs"] += 1
839
+ for line, owner_key in rf.unsafe_blocks:
840
+ owner = self.by_key.get(owner_key)
841
+ if owner is None:
842
+ continue
843
+ crate = rf.crate
844
+ b.add_node("unsafe", crate, name=f"unsafe in {crate}", lang="rust", module=crate)
845
+ b.add_edge(self.nid(owner), f"unsafe:{crate}", "USES_UNSAFE", rf.path, line, EXACT, how="unsafe block")
846
+ stats["unsafe_blocks"] += 1
847
+ for it in rf.items:
848
+ if it.kind in ("function", "method") and it.unsafe:
849
+ b.add_node("unsafe", rf.crate, name=f"unsafe in {rf.crate}", lang="rust", module=rf.crate)
850
+ b.add_edge(self.nid(it), f"unsafe:{rf.crate}", "USES_UNSAFE", rf.path, it.line, EXACT, how="unsafe fn")
851
+ stats["unsafe_fns"] += 1
852
+ if it.kind == "impl" and it.unsafe:
853
+ stats["unsafe_impls"] += 1
854
+ if it.kind == "ffi":
855
+ stats["ffi_imports"] += 1
856
+ # cfg gates
857
+ own = it.cfgs
858
+ if own and it.kind in CODE | TYPES | VALUES | {"mod", "macro"}:
859
+ for pred, line in own:
860
+ self._gate_edges(self.nid(it), pred, rf.path, line, pkg)
861
+ # actix / rocket attribute routes
862
+ for a in it.attrs:
863
+ short = a.split("::")[-1]
864
+ if it.kind in ("function", "method") and short in ROUTE_ATTRS:
865
+ self._attr_route(rf, it, a)
866
+ for start, end, pred, aline, owner_key in rf.cfg_regions:
867
+ owner = self.by_key.get(owner_key)
868
+ if owner is not None:
869
+ self._gate_edges(self.nid(owner), pred, rf.path, aline, pkg)
870
+ for verb, path, handler, line, col, owner_key, fw in rf.routes:
871
+ owner = self.by_key.get(owner_key)
872
+ hid = self._lookup_at(rf.path, line, col)
873
+ conf = EXACT if hid else HEURISTIC
874
+ if not hid and owner is not None:
875
+ t, c = self._resolve_path(rf, owner, handler)
876
+ hid = [self.nid(x) for x in t]
877
+ conf = c
878
+ rid = b.add_node("route", f"{verb} {path}", name=f"{verb} {path}", file=rf.path, line=line, lang="rust",
879
+ module=rf.module, attrs={"framework": fw, "handler": handler})
880
+ b.nodes[rid].entry_kind = "http_route"
881
+ for h in hid[:1]:
882
+ b.add_edge(rid, h, "ROUTES_TO", rf.path, line, conf)
883
+ if owner is not None:
884
+ b.add_edge(self.nid(owner), rid, "REFERENCES_FN", rf.path, line, EXACT, how="router registration")
885
+ stats["routes"] += 1
886
+
887
+ def _attr_route(self, rf: RFile, it: RItem, attr: str):
888
+ line_txt = "\n".join(rf.lines[max(0, it.start - 1 - 6):it.line])
889
+ import re
890
+ m = None
891
+ for m in re.finditer(r"#\[\s*(?:[\w:]+::)?(get|post|put|delete|patch|head|options|route)\s*\(\s*\"([^\"]*)\"", line_txt):
892
+ pass
893
+ if not m:
894
+ return
895
+ verb = m.group(1).upper() if m.group(1) != "route" else "ANY"
896
+ rid = self.b.add_node("route", f"{verb} {m.group(2)}", name=f"{verb} {m.group(2)}", file=rf.path, line=it.line, lang="rust",
897
+ module=rf.module, attrs={"framework": "actix/rocket attribute", "handler": it.key})
898
+ self.b.nodes[rid].entry_kind = "http_route"
899
+ self.b.add_edge(rid, self.nid(it), "ROUTES_TO", rf.path, it.line, EXACT)
900
+
901
+ def _gate_edges(self, src: str, pred: str, rel: str, line: int, pkg: str):
902
+ p = G.parse_cfg(pred)
903
+ for key, val in G.cfg_atoms(p):
904
+ if key == "test":
905
+ continue
906
+ if key == "feature" and val is not None:
907
+ tgt = self.b.add_node("feature", f"{pkg}/{val}", name=f"{pkg}/{val}", lang="rust", module=pkg,
908
+ attrs={"package": pkg})
909
+ if val not in self.pkg_features.get(pkg, {}):
910
+ self.b.nodes[tgt].attrs["undeclared"] = True
911
+ else:
912
+ t = G.atom_text(key, val)
913
+ tgt = self.b.add_node("cfg", t, name=t, lang="rust")
914
+ self.b.add_edge(src, tgt, "GATED_BY", rel, line, EXACT, cfg=pred)
915
+
916
+ # ------------------------------------------------------------------ entry points
917
+ def _entries(self, stats):
918
+ b = self.b
919
+ for it in self.items:
920
+ if it.kind not in ("function", "method"):
921
+ continue
922
+ meta = self.file_meta[it.file]
923
+ tgt = meta["target"]
924
+ kinds = []
925
+ attrs = set(a.lstrip(":") for a in it.attrs)
926
+ for a in attrs:
927
+ if a in ENTRY_ATTRS:
928
+ k, rt = ENTRY_ATTRS[a]
929
+ if k == "main" and a == "main":
930
+ continue
931
+ kinds.append(k)
932
+ if rt:
933
+ b.nodes[self.nid(it)].attrs["runtime"] = rt
934
+ root_main = it.kind == "function" and it.name == "main" and it.key == f"{tgt.crate}::main"
935
+ if root_main:
936
+ kinds.append({"bin": "main", "example": "example", "bench": "bench", "build": "build_script",
937
+ "test": "test"}.get(tgt.kind, "main"))
938
+ if attrs & {"no_mangle", "export_name", "unsafe"} and it.kind == "function" and (it.abi or "no_mangle" in attrs or "export_name" in attrs):
939
+ if "no_mangle" in attrs or "export_name" in attrs or any("no_mangle" in x for x in it.attrs):
940
+ kinds.append("ffi_export")
941
+ if tgt.kind == "lib" and it.pub_chain and not it.in_test:
942
+ kinds.append("public_api")
943
+ if not kinds:
944
+ continue
945
+ k = min(kinds, key=ENTRY_PRIORITY.index)
946
+ n = b.nodes.get(self.nid(it))
947
+ if n is not None:
948
+ n.entry_kind = k
949
+ if len(set(kinds)) > 1:
950
+ n.attrs["entry_kinds"] = sorted(set(kinds), key=ENTRY_PRIORITY.index)
951
+ stats[f"entry_{k}"] += 1
952
+
953
+ # ------------------------------------------------------------------ gate scenario
954
+ def _gating(self, stats):
955
+ sc = self.scen
956
+ if not sc.active:
957
+ return
958
+ dead: dict[str, list] = defaultdict(list)
959
+ for rf in self.files.values():
960
+ pkg = self.file_meta[rf.path]["pkg"].name
961
+ for pred, line in self.file_meta[rf.path]["cfgs"] + rf.inner_cfgs:
962
+ if sc.eval_cfg(G.parse_cfg(pred), pkg) is False:
963
+ dead[rf.path].append((1, 10 ** 9, pred, line))
964
+ for it in rf.items:
965
+ for pred, line in it.cfgs:
966
+ if sc.eval_cfg(G.parse_cfg(pred), pkg) is False:
967
+ dead[rf.path].append((it.start, it.end, pred, line))
968
+ for start, end, pred, aline, _ in rf.cfg_regions:
969
+ if sc.eval_cfg(G.parse_cfg(pred), pkg) is False:
970
+ dead[rf.path].append((start, end, pred, aline))
971
+ n = 0
972
+ for e in self.b.edges.values():
973
+ if e.file in dead and e.line is not None and e.gate is None and e.kind != "GATED_BY":
974
+ for a, z, pred, line in dead[e.file]:
975
+ if a <= e.line <= z:
976
+ e.gate = sc.name
977
+ e.attrs = {**e.attrs, "guard": f"{e.file}:{line}", "guard_expr": f"cfg({pred})"}
978
+ n += 1
979
+ break
980
+ stats["gated_edges"] = n