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.
- cg_code_graph-0.10.1.dist-info/METADATA +678 -0
- cg_code_graph-0.10.1.dist-info/RECORD +174 -0
- cg_code_graph-0.10.1.dist-info/WHEEL +5 -0
- cg_code_graph-0.10.1.dist-info/entry_points.txt +3 -0
- cg_code_graph-0.10.1.dist-info/licenses/LICENSE +21 -0
- cg_code_graph-0.10.1.dist-info/top_level.txt +1 -0
- codegraph/__init__.py +2 -0
- codegraph/aitools.py +129 -0
- codegraph/apps.py +76 -0
- codegraph/blindspots.py +428 -0
- codegraph/bridges.py +1701 -0
- codegraph/cli.py +725 -0
- codegraph/concepts.py +362 -0
- codegraph/config.py +559 -0
- codegraph/core/__init__.py +0 -0
- codegraph/core/cache.py +375 -0
- codegraph/core/detect.py +80 -0
- codegraph/core/extractors.py +187 -0
- codegraph/core/fsutil.py +61 -0
- codegraph/core/generated.py +575 -0
- codegraph/core/model.py +174 -0
- codegraph/core/paths.py +175 -0
- codegraph/core/plugin.py +160 -0
- codegraph/core/store.py +80 -0
- codegraph/core/syntax_errors.py +132 -0
- codegraph/coverage.py +928 -0
- codegraph/doctor.py +453 -0
- codegraph/external.py +613 -0
- codegraph/indexer.py +336 -0
- codegraph/link.py +434 -0
- codegraph/lint_async.py +524 -0
- codegraph/mcp_server.py +1303 -0
- codegraph/parity.py +473 -0
- codegraph/parity_structure.py +307 -0
- codegraph/payload.py +321 -0
- codegraph/plans.py +1285 -0
- codegraph/platform_scan.py +643 -0
- codegraph/platforms.py +1369 -0
- codegraph/plugins/__init__.py +0 -0
- codegraph/plugins/cfamily/__init__.py +0 -0
- codegraph/plugins/cfamily/plugin.py +930 -0
- codegraph/plugins/cfamily/syntax.py +881 -0
- codegraph/plugins/dart/__init__.py +0 -0
- codegraph/plugins/dart/bridges.py +345 -0
- codegraph/plugins/dart/extractor/bin/extract.dart +717 -0
- codegraph/plugins/dart/extractor/pubspec.lock +149 -0
- codegraph/plugins/dart/extractor/pubspec.yaml +7 -0
- codegraph/plugins/dart/http.py +904 -0
- codegraph/plugins/dart/models.py +308 -0
- codegraph/plugins/dart/plugin.py +625 -0
- codegraph/plugins/dart/program.py +907 -0
- codegraph/plugins/django/__init__.py +0 -0
- codegraph/plugins/django/extras.py +378 -0
- codegraph/plugins/django/models.py +508 -0
- codegraph/plugins/django/plugin.py +728 -0
- codegraph/plugins/django/schemas.py +339 -0
- codegraph/plugins/django/shapes.py +216 -0
- codegraph/plugins/django/urls.py +603 -0
- codegraph/plugins/express/__init__.py +0 -0
- codegraph/plugins/express/plugin.py +428 -0
- codegraph/plugins/flutter/__init__.py +0 -0
- codegraph/plugins/flutter/plugin.py +538 -0
- codegraph/plugins/kotlin/__init__.py +0 -0
- codegraph/plugins/kotlin/exact.py +457 -0
- codegraph/plugins/kotlin/plugin.py +1961 -0
- codegraph/plugins/kotlin/reparse.py +234 -0
- codegraph/plugins/laravel/__init__.py +0 -0
- codegraph/plugins/laravel/broadcast.py +351 -0
- codegraph/plugins/laravel/plugin.py +863 -0
- codegraph/plugins/laravel/tests.py +262 -0
- codegraph/plugins/laravel/values.py +728 -0
- codegraph/plugins/native/__init__.py +0 -0
- codegraph/plugins/native/gates.py +286 -0
- codegraph/plugins/native/runner.py +183 -0
- codegraph/plugins/native/scipread.py +194 -0
- codegraph/plugins/native/ts.py +54 -0
- codegraph/plugins/nest/__init__.py +0 -0
- codegraph/plugins/nest/plugin.py +654 -0
- codegraph/plugins/nextjs/__init__.py +0 -0
- codegraph/plugins/nextjs/plugin.py +336 -0
- codegraph/plugins/nuxt/__init__.py +0 -0
- codegraph/plugins/nuxt/plugin.py +308 -0
- codegraph/plugins/php/__init__.py +0 -0
- codegraph/plugins/php/extractor/composer.json +5 -0
- codegraph/plugins/php/extractor/composer.lock +76 -0
- codegraph/plugins/php/extractor/extract.php +743 -0
- codegraph/plugins/php/gating.py +573 -0
- codegraph/plugins/php/plugin.py +668 -0
- codegraph/plugins/php/strings.py +197 -0
- codegraph/plugins/python/__init__.py +0 -0
- codegraph/plugins/python/aitools.py +664 -0
- codegraph/plugins/python/external.py +245 -0
- codegraph/plugins/python/fields.py +107 -0
- codegraph/plugins/python/plugin.py +1733 -0
- codegraph/plugins/python/refs.py +485 -0
- codegraph/plugins/python/roots.py +412 -0
- codegraph/plugins/python/socketio.py +210 -0
- codegraph/plugins/python/subproc.py +864 -0
- codegraph/plugins/python/tests.py +1040 -0
- codegraph/plugins/python/values.py +179 -0
- codegraph/plugins/pyweb/__init__.py +0 -0
- codegraph/plugins/pyweb/plugin.py +1334 -0
- codegraph/plugins/pyweb/values.py +68 -0
- codegraph/plugins/rust/__init__.py +0 -0
- codegraph/plugins/rust/cargo.py +226 -0
- codegraph/plugins/rust/plugin.py +980 -0
- codegraph/plugins/rust/syntax.py +678 -0
- codegraph/plugins/scip/__init__.py +0 -0
- codegraph/plugins/scip/importer.py +129 -0
- codegraph/plugins/scip/scip.proto +962 -0
- codegraph/plugins/scip/scip_pb2.py +97 -0
- codegraph/plugins/stubs/__init__.py +0 -0
- codegraph/plugins/stubs/plugins.py +38 -0
- codegraph/plugins/swift/__init__.py +0 -0
- codegraph/plugins/swift/baseurl.py +109 -0
- codegraph/plugins/swift/exact.py +415 -0
- codegraph/plugins/swift/indexstore.py +209 -0
- codegraph/plugins/swift/packages.py +174 -0
- codegraph/plugins/swift/plugin.py +2890 -0
- codegraph/plugins/ts/__init__.py +0 -0
- codegraph/plugins/ts/baseurl.py +185 -0
- codegraph/plugins/ts/extractor/extract.mjs +2652 -0
- codegraph/plugins/ts/extractor/fw.mjs +685 -0
- codegraph/plugins/ts/extractor/package-lock.json +205 -0
- codegraph/plugins/ts/extractor/package.json +9 -0
- codegraph/plugins/ts/plugin.py +480 -0
- codegraph/plugins/tsweb/__init__.py +0 -0
- codegraph/plugins/tsweb/common.py +290 -0
- codegraph/plugins/tsweb/data.py +276 -0
- codegraph/presets/__init__.py +146 -0
- codegraph/presets/c_cpp.yaml +9 -0
- codegraph/presets/common.yaml +66 -0
- codegraph/presets/dart.yaml +9 -0
- codegraph/presets/django-ninja.yaml +15 -0
- codegraph/presets/django.yaml +25 -0
- codegraph/presets/djangorestframework.yaml +17 -0
- codegraph/presets/express.yaml +17 -0
- codegraph/presets/kotlin.yaml +11 -0
- codegraph/presets/laravel.yaml +40 -0
- codegraph/presets/nest.yaml +11 -0
- codegraph/presets/nextjs.yaml +15 -0
- codegraph/presets/nuxt.yaml +9 -0
- codegraph/presets/php.yaml +5 -0
- codegraph/presets/python.yaml +10 -0
- codegraph/presets/rust.yaml +5 -0
- codegraph/presets/swift.yaml +10 -0
- codegraph/presets/typescript.yaml +13 -0
- codegraph/process_runs.py +328 -0
- codegraph/protocols/__init__.py +299 -0
- codegraph/protocols/builtin.py +67 -0
- codegraph/protocols/matchers.py +144 -0
- codegraph/protocols/view.py +334 -0
- codegraph/query.py +2089 -0
- codegraph/realtime.py +260 -0
- codegraph/roundtrip.py +346 -0
- codegraph/routes.py +442 -0
- codegraph/starters.py +218 -0
- codegraph/tests_index.py +117 -0
- codegraph/viz/__init__.py +0 -0
- codegraph/viz/graph.py +369 -0
- codegraph/viz/server.py +198 -0
- codegraph/viz/static/app.css +148 -0
- codegraph/viz/static/app.js +1082 -0
- codegraph/viz/static/index.html +81 -0
- codegraph/viz/static/layered.js +237 -0
- codegraph/viz/static/vendor/VERSIONS.txt +4 -0
- codegraph/viz/static/vendor/cose-base.js +3214 -0
- codegraph/viz/static/vendor/cytoscape-fcose.js +1549 -0
- codegraph/viz/static/vendor/cytoscape.min.js +31 -0
- codegraph/viz/static/vendor/layout-base.js +5230 -0
- codegraph/viz/tools/package-lock.json +303 -0
- codegraph/viz/tools/package.json +7 -0
- codegraph/viz/tools/shoot.mjs +165 -0
- 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
|