rust-analyzer-db 0.2.0__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.
- rust_analyzer/__init__.py +1 -0
- rust_analyzer/cli.py +663 -0
- rust_analyzer/db.py +596 -0
- rust_analyzer/exceptions.py +41 -0
- rust_analyzer/extractor.py +640 -0
- rust_analyzer/graph.py +297 -0
- rust_analyzer/logging.py +52 -0
- rust_analyzer/mcp_server.py +516 -0
- rust_analyzer/static/style.css +573 -0
- rust_analyzer/static/vis-network.min.js +34 -0
- rust_analyzer/templates/api_surface.html +85 -0
- rust_analyzer/templates/base.html +82 -0
- rust_analyzer/templates/complexity.html +108 -0
- rust_analyzer/templates/dashboard.html +170 -0
- rust_analyzer/templates/deps.html +63 -0
- rust_analyzer/templates/graph.html +99 -0
- rust_analyzer/templates/items.html +117 -0
- rust_analyzer/templates/search.html +91 -0
- rust_analyzer/web.py +392 -0
- rust_analyzer_db-0.2.0.dist-info/METADATA +302 -0
- rust_analyzer_db-0.2.0.dist-info/RECORD +24 -0
- rust_analyzer_db-0.2.0.dist-info/WHEEL +4 -0
- rust_analyzer_db-0.2.0.dist-info/entry_points.txt +2 -0
- rust_analyzer_db-0.2.0.dist-info/licenses/LICENSE +201 -0
rust_analyzer/graph.py
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"""Builds a call graph (execution-flow graph) from the data stored in the
|
|
2
|
+
RustCodeDB and renders it as Graphviz DOT/SVG/PNG or an interactive HTML
|
|
3
|
+
page (vis-network).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
import re
|
|
9
|
+
import shutil
|
|
10
|
+
import subprocess
|
|
11
|
+
from collections import deque
|
|
12
|
+
from typing import Optional, Set, Tuple, List, Dict
|
|
13
|
+
|
|
14
|
+
from .db import RustCodeDB
|
|
15
|
+
|
|
16
|
+
NODE_COLORS = {
|
|
17
|
+
"function": "#4C72B0",
|
|
18
|
+
"method": "#55A868",
|
|
19
|
+
"external": "#B0B0B0",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
MAX_LABEL_LEN = 60
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _clean(s: Optional[str]) -> str:
|
|
26
|
+
"""Collapse whitespace/newlines and truncate. Anything downstream (DOT
|
|
27
|
+
labels, HTML/JSON) treats the result as plain text to be escaped there."""
|
|
28
|
+
if not s:
|
|
29
|
+
return ""
|
|
30
|
+
s = re.sub(r"\s+", " ", s).strip()
|
|
31
|
+
if len(s) > MAX_LABEL_LEN:
|
|
32
|
+
s = s[:MAX_LABEL_LEN - 1] + "\u2026"
|
|
33
|
+
return s
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _safe_id(*parts: Optional[str]) -> str:
|
|
37
|
+
"""Deterministic, DOT/HTML-safe node id built from arbitrary text."""
|
|
38
|
+
key = "\x1f".join(p or "" for p in parts)
|
|
39
|
+
return "n" + hashlib.md5(key.encode("utf-8", errors="ignore")).hexdigest()[:12]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _item_node_id(item_id: int) -> str:
|
|
43
|
+
return f"item{item_id}"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _external_node_id(name: str, receiver: Optional[str], is_method_call: bool) -> str:
|
|
47
|
+
# Method-call receivers are usually arbitrary local variable names
|
|
48
|
+
# (x.clone(), y.clone(), self.clone()...) so collapse purely by method
|
|
49
|
+
# name, or every unique variable would become its own node. Scoped
|
|
50
|
+
# calls (Type::method / module::func) have bounded, meaningful
|
|
51
|
+
# receivers, so keep those distinct.
|
|
52
|
+
if is_method_call:
|
|
53
|
+
return _safe_id("ext_method", name)
|
|
54
|
+
return _safe_id("ext_fn", receiver, name)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _node_label(name: str, target: Optional[str]) -> str:
|
|
58
|
+
name = _clean(name)
|
|
59
|
+
target = _clean(target) if target else None
|
|
60
|
+
return f"{target}::{name}" if target else name
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class CallGraph:
|
|
64
|
+
def __init__(self):
|
|
65
|
+
self.nodes: Dict[str, dict] = {} # node_id -> {label, kind, item_id}
|
|
66
|
+
self.edges: Set[Tuple[str, str]] = set()
|
|
67
|
+
|
|
68
|
+
def add_node(self, node_id: str, label: str, kind: str, item_id: Optional[int]):
|
|
69
|
+
if node_id not in self.nodes:
|
|
70
|
+
self.nodes[node_id] = {"label": label, "kind": kind, "item_id": item_id}
|
|
71
|
+
|
|
72
|
+
def add_edge(self, src: str, dst: str):
|
|
73
|
+
if src != dst:
|
|
74
|
+
self.edges.add((src, dst))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def build_whole_graph(db: RustCodeDB, include_unresolved: bool = False,
|
|
78
|
+
kinds: Optional[Set[str]] = None) -> CallGraph:
|
|
79
|
+
g = CallGraph()
|
|
80
|
+
for row in db.all_call_edges():
|
|
81
|
+
if not include_unresolved and row["callee_id"] is None:
|
|
82
|
+
continue
|
|
83
|
+
caller_kind = row["caller_kind"]
|
|
84
|
+
if kinds and caller_kind not in kinds:
|
|
85
|
+
continue
|
|
86
|
+
src_id = _item_node_id(row["caller_id"])
|
|
87
|
+
g.add_node(src_id, _node_label(row["caller_name"], row["caller_target"]), caller_kind, row["caller_id"])
|
|
88
|
+
|
|
89
|
+
if row["callee_id"] is not None:
|
|
90
|
+
dst_kind = row["callee_kind"]
|
|
91
|
+
dst_id = _item_node_id(row["callee_id"])
|
|
92
|
+
g.add_node(dst_id, _node_label(row["callee_name"], row["callee_target"]), dst_kind, row["callee_id"])
|
|
93
|
+
else:
|
|
94
|
+
dst_id = _external_node_id(row["callee_name"], row["receiver"], bool(row["is_method_call"]))
|
|
95
|
+
g.add_node(dst_id, _node_label(row["callee_name"], row["receiver"] if not row["is_method_call"] else None),
|
|
96
|
+
"external", None)
|
|
97
|
+
g.add_edge(src_id, dst_id)
|
|
98
|
+
return g
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def build_subgraph(db: RustCodeDB, root_item_ids: List[int], depth: int = 2,
|
|
102
|
+
direction: str = "both", include_unresolved: bool = True) -> CallGraph:
|
|
103
|
+
"""BFS out from the given root item id(s) following callee edges
|
|
104
|
+
('callees'), caller edges ('callers'), or both, up to `depth` hops."""
|
|
105
|
+
g = CallGraph()
|
|
106
|
+
|
|
107
|
+
def ensure_item_node(item_id: int):
|
|
108
|
+
row = db.get_item(item_id)
|
|
109
|
+
if row is None:
|
|
110
|
+
return
|
|
111
|
+
node_id = _item_node_id(item_id)
|
|
112
|
+
g.add_node(node_id, _node_label(row["name"], row["target"]), row["kind"], item_id)
|
|
113
|
+
|
|
114
|
+
visited = set()
|
|
115
|
+
queue = deque((rid, 0) for rid in root_item_ids)
|
|
116
|
+
for rid in root_item_ids:
|
|
117
|
+
ensure_item_node(rid)
|
|
118
|
+
visited.add(rid)
|
|
119
|
+
|
|
120
|
+
while queue:
|
|
121
|
+
item_id, d = queue.popleft()
|
|
122
|
+
if d >= depth:
|
|
123
|
+
continue
|
|
124
|
+
row = db.get_item(item_id)
|
|
125
|
+
if row is None:
|
|
126
|
+
continue
|
|
127
|
+
src_id = _item_node_id(item_id)
|
|
128
|
+
|
|
129
|
+
if direction in ("callees", "both"):
|
|
130
|
+
for call in db.get_calls_from(item_id):
|
|
131
|
+
if call["callee_id"] is not None:
|
|
132
|
+
ensure_item_node(call["callee_id"])
|
|
133
|
+
dst_id = _item_node_id(call["callee_id"])
|
|
134
|
+
g.add_edge(src_id, dst_id)
|
|
135
|
+
if call["callee_id"] not in visited:
|
|
136
|
+
visited.add(call["callee_id"])
|
|
137
|
+
queue.append((call["callee_id"], d + 1))
|
|
138
|
+
elif include_unresolved:
|
|
139
|
+
is_method = bool(call["is_method_call"])
|
|
140
|
+
dst_id = _external_node_id(call["callee_name"], call["receiver"], is_method)
|
|
141
|
+
g.add_node(dst_id, _node_label(call["callee_name"], call["receiver"] if not is_method else None),
|
|
142
|
+
"external", None)
|
|
143
|
+
g.add_edge(src_id, dst_id)
|
|
144
|
+
|
|
145
|
+
if direction in ("callers", "both"):
|
|
146
|
+
for call in db.get_calls_to(item_id):
|
|
147
|
+
ensure_item_node(call["caller_id"])
|
|
148
|
+
caller_node_id = _item_node_id(call["caller_id"])
|
|
149
|
+
g.add_edge(caller_node_id, src_id)
|
|
150
|
+
if call["caller_id"] not in visited:
|
|
151
|
+
visited.add(call["caller_id"])
|
|
152
|
+
queue.append((call["caller_id"], d + 1))
|
|
153
|
+
|
|
154
|
+
return g
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _dot_escape(s: str) -> str:
|
|
158
|
+
return s.replace("\\", "\\\\").replace('"', '\\"')
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def to_dot(g: CallGraph, title: str = "Call Graph") -> str:
|
|
162
|
+
lines = [
|
|
163
|
+
"digraph callgraph {",
|
|
164
|
+
f' label="{_dot_escape(title)}";',
|
|
165
|
+
" labelloc=t;",
|
|
166
|
+
" fontsize=18;",
|
|
167
|
+
" rankdir=LR;",
|
|
168
|
+
" node [shape=box, style=\"rounded,filled\", fontname=\"Helvetica\", fontsize=10];",
|
|
169
|
+
" edge [color=\"#888888\", arrowsize=0.7];",
|
|
170
|
+
]
|
|
171
|
+
for node_id, meta in g.nodes.items():
|
|
172
|
+
color = NODE_COLORS.get(meta["kind"], "#CCCCCC")
|
|
173
|
+
label = _dot_escape(meta["label"]) or "?"
|
|
174
|
+
shape = "ellipse" if meta["kind"] == "external" else "box"
|
|
175
|
+
lines.append(f' "{node_id}" [label="{label}", fillcolor="{color}", shape={shape}, fontcolor="white"];')
|
|
176
|
+
for src, dst in sorted(g.edges):
|
|
177
|
+
lines.append(f' "{src}" -> "{dst}";')
|
|
178
|
+
lines.append("}")
|
|
179
|
+
return "\n".join(lines)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def render_dot(dot_str: str, out_path: str, fmt: str) -> bool:
|
|
183
|
+
"""Render a DOT string to `fmt` (svg/png/pdf) via the `dot` binary.
|
|
184
|
+
Returns True on success, False if graphviz isn't installed."""
|
|
185
|
+
if shutil.which("dot") is None:
|
|
186
|
+
return False
|
|
187
|
+
proc = subprocess.run(
|
|
188
|
+
["dot", f"-T{fmt}", "-o", out_path],
|
|
189
|
+
input=dot_str.encode("utf-8"),
|
|
190
|
+
capture_output=True,
|
|
191
|
+
)
|
|
192
|
+
if proc.returncode != 0:
|
|
193
|
+
raise RuntimeError(proc.stderr.decode("utf-8", errors="replace"))
|
|
194
|
+
return True
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
VIS_HTML_TEMPLATE = """<!DOCTYPE html>
|
|
198
|
+
<html>
|
|
199
|
+
<head>
|
|
200
|
+
<meta charset="utf-8">
|
|
201
|
+
<title>{title}</title>
|
|
202
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/vis-network/9.1.9/vis-network.min.js"
|
|
203
|
+
onerror="document.getElementById('graph').innerHTML =
|
|
204
|
+
'<div style=\\'color:#eee;padding:24px;font-family:sans-serif\\'>' +
|
|
205
|
+
'Could not load vis-network from the CDN (cdnjs.cloudflare.com). ' +
|
|
206
|
+
'This page needs internet access in your browser to draw the graph. ' +
|
|
207
|
+
'Use <code>--format svg</code> or <code>--format png</code> instead if you are offline.</div>'"></script>
|
|
208
|
+
<style>
|
|
209
|
+
html, body {{ margin:0; height:100%; font-family: Helvetica, Arial, sans-serif; background:#1e1e1e; }}
|
|
210
|
+
#header {{ padding: 10px 16px; color: #eee; background:#252526; border-bottom: 1px solid #333; }}
|
|
211
|
+
#graph {{ width: 100%; height: calc(100% - 52px); background:#1e1e1e; }}
|
|
212
|
+
.legend span {{ display:inline-block; margin-right:16px; }}
|
|
213
|
+
.dot {{ display:inline-block; width:10px; height:10px; border-radius:50%; margin-right:4px; }}
|
|
214
|
+
#status {{ color:#999; font-size:12px; margin-left:12px; }}
|
|
215
|
+
</style>
|
|
216
|
+
</head>
|
|
217
|
+
<body>
|
|
218
|
+
<div id="header">
|
|
219
|
+
<strong>{title}</strong>
|
|
220
|
+
<span id="status">{node_count} nodes, {edge_count} edges</span>
|
|
221
|
+
<div class="legend" style="margin-top:4px; font-size:12px;">
|
|
222
|
+
<span><span class="dot" style="background:#4C72B0;"></span>function</span>
|
|
223
|
+
<span><span class="dot" style="background:#55A868;"></span>method</span>
|
|
224
|
+
<span><span class="dot" style="background:#B0B0B0;"></span>external/unresolved</span>
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
<div id="graph"></div>
|
|
228
|
+
<script>
|
|
229
|
+
try {{
|
|
230
|
+
const nodesData = {nodes_json};
|
|
231
|
+
const edgesData = {edges_json};
|
|
232
|
+
const nodes = new vis.DataSet(nodesData);
|
|
233
|
+
const edges = new vis.DataSet(edgesData);
|
|
234
|
+
const container = document.getElementById('graph');
|
|
235
|
+
const data = {{ nodes, edges }};
|
|
236
|
+
const nodeCount = nodesData.length;
|
|
237
|
+
|
|
238
|
+
// Large graphs: use a cheaper solver and stop physics as soon as it
|
|
239
|
+
// stabilizes (or after a bounded number of iterations) so the view
|
|
240
|
+
// doesn't sit spinning forever on a big project.
|
|
241
|
+
const big = nodeCount > 400;
|
|
242
|
+
const options = {{
|
|
243
|
+
nodes: {{ shape: 'box', margin: 8, font: {{ color: '#fff', size: 13 }}, borderWidth: 0 }},
|
|
244
|
+
edges: {{ arrows: 'to', color: {{ color: '#666', highlight: '#fff' }}, smooth: {{ type: 'dynamic' }} }},
|
|
245
|
+
physics: {{
|
|
246
|
+
solver: big ? 'barnesHut' : 'forceAtlas2Based',
|
|
247
|
+
stabilization: {{ enabled: true, iterations: big ? 150 : 250, fit: true }}
|
|
248
|
+
}},
|
|
249
|
+
interaction: {{ hover: true, tooltipDelay: 100 }}
|
|
250
|
+
}};
|
|
251
|
+
|
|
252
|
+
const network = new vis.Network(container, data, options);
|
|
253
|
+
const statusEl = document.getElementById('status');
|
|
254
|
+
|
|
255
|
+
network.once('stabilizationIterationsDone', function () {{
|
|
256
|
+
network.setOptions({{ physics: false }});
|
|
257
|
+
network.fit();
|
|
258
|
+
statusEl.textContent = nodeCount + ' nodes, ' + edgesData.length + ' edges (layout complete, physics off)';
|
|
259
|
+
}});
|
|
260
|
+
|
|
261
|
+
// Safety net: even if stabilization never fully completes on a huge
|
|
262
|
+
// graph, force it to stop spinning and fit the view after a timeout.
|
|
263
|
+
setTimeout(function () {{
|
|
264
|
+
network.setOptions({{ physics: false }});
|
|
265
|
+
network.fit();
|
|
266
|
+
}}, 8000);
|
|
267
|
+
}} catch (err) {{
|
|
268
|
+
document.getElementById('graph').innerHTML =
|
|
269
|
+
'<div style="color:#f66;padding:24px;font-family:monospace;white-space:pre-wrap">' +
|
|
270
|
+
'Error rendering graph: ' + err + '</div>';
|
|
271
|
+
console.error(err);
|
|
272
|
+
}}
|
|
273
|
+
</script>
|
|
274
|
+
</body>
|
|
275
|
+
</html>
|
|
276
|
+
"""
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def to_html(g: CallGraph, title: str = "Call Graph") -> str:
|
|
280
|
+
nodes = [
|
|
281
|
+
{
|
|
282
|
+
"id": nid,
|
|
283
|
+
"label": meta["label"] or "?",
|
|
284
|
+
"color": NODE_COLORS.get(meta["kind"], "#CCCCCC"),
|
|
285
|
+
"shape": "ellipse" if meta["kind"] == "external" else "box",
|
|
286
|
+
"title": f"{meta['kind']}: {meta['label']}",
|
|
287
|
+
}
|
|
288
|
+
for nid, meta in g.nodes.items()
|
|
289
|
+
]
|
|
290
|
+
edges = [{"from": s, "to": d} for s, d in sorted(g.edges)]
|
|
291
|
+
return VIS_HTML_TEMPLATE.format(
|
|
292
|
+
title=title,
|
|
293
|
+
node_count=len(nodes),
|
|
294
|
+
edge_count=len(edges),
|
|
295
|
+
nodes_json=json.dumps(nodes),
|
|
296
|
+
edges_json=json.dumps(edges),
|
|
297
|
+
)
|
rust_analyzer/logging.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Logging configuration for rust-analyzer-db."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
_configured = False
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class _SafeStreamHandler(logging.StreamHandler):
|
|
12
|
+
"""StreamHandler that silently drops writes if the stream is closed."""
|
|
13
|
+
|
|
14
|
+
def emit(self, record: logging.LogRecord) -> None:
|
|
15
|
+
try:
|
|
16
|
+
if self.stream.closed:
|
|
17
|
+
return
|
|
18
|
+
super().emit(record)
|
|
19
|
+
except ValueError:
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def setup_logging(*, verbose: bool = False, json_output: bool = False) -> None:
|
|
24
|
+
"""Configure the package-wide logger.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
verbose: Enable DEBUG level output.
|
|
28
|
+
json_output: Format logs as single-line key=value pairs.
|
|
29
|
+
"""
|
|
30
|
+
global _configured # noqa: PLW0603
|
|
31
|
+
if _configured:
|
|
32
|
+
return
|
|
33
|
+
_configured = True
|
|
34
|
+
|
|
35
|
+
level = logging.DEBUG if verbose else logging.INFO
|
|
36
|
+
|
|
37
|
+
if json_output:
|
|
38
|
+
fmt = '{"level":"%(levelname)s","logger":"%(name)s","msg":"%(message)s"}'
|
|
39
|
+
else:
|
|
40
|
+
fmt = "%(levelname)s: %(message)s"
|
|
41
|
+
|
|
42
|
+
handler = _SafeStreamHandler(sys.stderr)
|
|
43
|
+
handler.setFormatter(logging.Formatter(fmt))
|
|
44
|
+
|
|
45
|
+
root = logging.getLogger("rust_analyzer")
|
|
46
|
+
root.setLevel(level)
|
|
47
|
+
root.addHandler(handler)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_logger(name: str) -> logging.Logger:
|
|
51
|
+
"""Get a child logger under the ``rust_analyzer`` namespace."""
|
|
52
|
+
return logging.getLogger(f"rust_analyzer.{name}")
|