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.
@@ -0,0 +1,117 @@
1
+ {% extends "base.html" %}
2
+ {% block title %}Items — Rust Analyzer{% endblock %}
3
+ {% block topbar_title %}Code Items{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="panel">
7
+ <div class="panel-header">
8
+ <span class="panel-title">All Items ({{ total }})</span>
9
+ <form method="get" action="/items" class="controls">
10
+ <select name="kind" onchange="this.form.submit()">
11
+ <option value="">All kinds</option>
12
+ {% for k in all_kinds %}
13
+ <option value="{{ k }}" {% if kind_filter == k %}selected{% endif %}>{{ k }}</option>
14
+ {% endfor %}
15
+ </select>
16
+ <input type="text" name="name" placeholder="Filter by name..." value="{{ name_filter }}" style="width:200px">
17
+ <input type="text" name="file" placeholder="Filter by file..." value="{{ file_filter }}" style="width:200px">
18
+ <button type="submit" class="btn btn-primary">Filter</button>
19
+ {% if kind_filter or name_filter or file_filter %}
20
+ <a href="/items" class="btn">Clear</a>
21
+ {% endif %}
22
+ </form>
23
+ </div>
24
+ <div class="panel-body no-pad">
25
+ <div class="table-wrap">
26
+ <table>
27
+ <thead>
28
+ <tr>
29
+ <th>ID</th>
30
+ <th>Kind</th>
31
+ <th>Name</th>
32
+ <th>Visibility</th>
33
+ <th>Target</th>
34
+ <th>Signature</th>
35
+ <th>CC</th>
36
+ <th>Location</th>
37
+ <th></th>
38
+ </tr>
39
+ </thead>
40
+ <tbody>
41
+ {% for row in items %}
42
+ <tr>
43
+ <td class="muted">{{ row.id }}</td>
44
+ <td><span class="badge badge-{{ row.kind }}">{{ row.kind }}</span></td>
45
+ <td class="mono">{{ row.name }}</td>
46
+ <td class="muted">{{ row.visibility or '' }}</td>
47
+ <td class="muted mono" style="font-size:11px">{{ row.target or '' }}</td>
48
+ <td class="mono" style="font-size:11px;max-width:250px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{ row.signature or '' }}</td>
49
+ <td class="mono {% if row.cyclomatic_complexity and row.cyclomatic_complexity >= 10 %}cx-high{% elif row.cyclomatic_complexity and row.cyclomatic_complexity >= 5 %}cx-medium{% else %}cx-low{% endif %}">
50
+ {{ row.cyclomatic_complexity or '' }}
51
+ </td>
52
+ <td class="muted mono" style="font-size:11px">{{ row.file_path }}:{{ row.start_line }}</td>
53
+ <td>
54
+ <button class="btn" onclick="showSource({{ row.id }})" title="View source">{'{{ '{' }}</button>
55
+ </td>
56
+ </tr>
57
+ {% endfor %}
58
+ {% if not items %}
59
+ <tr><td colspan="9" class="muted" style="text-align:center;padding:24px">No items found</td></tr>
60
+ {% endif %}
61
+ </tbody>
62
+ </table>
63
+ </div>
64
+ </div>
65
+ {% if total_pages > 1 %}
66
+ <div class="pagination">
67
+ {% if page > 1 %}
68
+ <a href="?page={{ page-1 }}&kind={{ kind_filter }}&name={{ name_filter }}&file={{ file_filter }}">Prev</a>
69
+ {% endif %}
70
+ {% for p in range(1, total_pages+1) %}
71
+ {% if p == page %}
72
+ <button class="active">{{ p }}</button>
73
+ {% elif p <= 3 or p > total_pages - 3 or (p >= page - 1 and p <= page + 1) %}
74
+ <a href="?page={{ p }}&kind={{ kind_filter }}&name={{ name_filter }}&file={{ file_filter }}"><button>{{ p }}</button></a>
75
+ {% elif p == 4 or p == total_pages - 3 %}
76
+ <button disabled>...</button>
77
+ {% endif %}
78
+ {% endfor %}
79
+ {% if page < total_pages %}
80
+ <a href="?page={{ page+1 }}&kind={{ kind_filter }}&name={{ name_filter }}&file={{ file_filter }}">Next</a>
81
+ {% endif %}
82
+ </div>
83
+ {% endif %}
84
+ </div>
85
+
86
+ <!-- Source modal -->
87
+ <div class="modal-overlay" id="source-modal" style="display:none" onclick="if(event.target===this)closeModal()">
88
+ <div class="modal">
89
+ <div class="modal-header">
90
+ <h3 id="modal-title">Source</h3>
91
+ <button class="modal-close" onclick="closeModal()">&times;</button>
92
+ </div>
93
+ <div class="modal-body">
94
+ <div id="modal-meta" style="margin-bottom:12px;font-size:12px;color:var(--text-secondary)"></div>
95
+ <pre class="source-viewer" id="modal-source"></pre>
96
+ </div>
97
+ </div>
98
+ </div>
99
+ {% endblock %}
100
+
101
+ {% block scripts %}
102
+ <script>
103
+ async function showSource(id) {
104
+ const resp = await fetch('/api/item/' + id);
105
+ if (!resp.ok) return;
106
+ const item = await resp.json();
107
+ document.getElementById('modal-title').textContent = item.kind + ' ' + item.name;
108
+ document.getElementById('modal-meta').textContent = item.file_path + ':' + item.start_line + '-' + item.end_line;
109
+ document.getElementById('modal-source').textContent = item.source || '(no source)';
110
+ document.getElementById('source-modal').style.display = 'flex';
111
+ }
112
+ function closeModal() {
113
+ document.getElementById('source-modal').style.display = 'none';
114
+ }
115
+ document.addEventListener('keydown', e => { if (e.key === 'Escape') closeModal(); });
116
+ </script>
117
+ {% endblock %}
@@ -0,0 +1,91 @@
1
+ {% extends "base.html" %}
2
+ {% block title %}Search — Rust Analyzer{% endblock %}
3
+ {% block topbar_title %}Search{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="panel">
7
+ <div class="panel-header">
8
+ <span class="panel-title">Full-Text Search</span>
9
+ </div>
10
+ <div class="panel-body">
11
+ <form method="get" action="/search" class="controls">
12
+ <input type="text" name="q" placeholder="Search name, signature, doc, source..." value="{{ query }}" style="width:400px" autofocus>
13
+ <select name="kind">
14
+ <option value="">All kinds</option>
15
+ {% for k in all_kinds %}
16
+ <option value="{{ k }}" {% if kind_filter == k %}selected{% endif %}>{{ k }}</option>
17
+ {% endfor %}
18
+ </select>
19
+ <button type="submit" class="btn btn-primary">Search</button>
20
+ </form>
21
+ </div>
22
+ </div>
23
+
24
+ {% if query %}
25
+ <div class="panel">
26
+ <div class="panel-header">
27
+ <span class="panel-title">Results for "{{ query }}" ({{ rows|length }})</span>
28
+ </div>
29
+ <div class="panel-body no-pad">
30
+ <div class="table-wrap">
31
+ <table>
32
+ <thead>
33
+ <tr>
34
+ <th>Kind</th>
35
+ <th>Name</th>
36
+ <th>Signature</th>
37
+ <th>File</th>
38
+ <th></th>
39
+ </tr>
40
+ </thead>
41
+ <tbody>
42
+ {% for row in rows %}
43
+ <tr>
44
+ <td><span class="badge badge-{{ row.kind }}">{{ row.kind }}</span></td>
45
+ <td class="mono">{{ row.name }}</td>
46
+ <td class="mono" style="font-size:11px;max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{ row.signature or '' }}</td>
47
+ <td class="muted mono" style="font-size:11px">{{ row.file_path }}:{{ row.start_line }}</td>
48
+ <td>
49
+ <button class="btn" onclick="showSource({{ row.id }})" title="View source">{'{{ '{' }}</button>
50
+ </td>
51
+ </tr>
52
+ {% endfor %}
53
+ {% if not rows %}
54
+ <tr><td colspan="5" class="muted" style="text-align:center;padding:24px">No results</td></tr>
55
+ {% endif %}
56
+ </tbody>
57
+ </table>
58
+ </div>
59
+ </div>
60
+ </div>
61
+ {% endif %}
62
+
63
+ <div class="modal-overlay" id="source-modal" style="display:none" onclick="if(event.target===this)closeModal()">
64
+ <div class="modal">
65
+ <div class="modal-header">
66
+ <h3 id="modal-title">Source</h3>
67
+ <button class="modal-close" onclick="closeModal()">&times;</button>
68
+ </div>
69
+ <div class="modal-body">
70
+ <div id="modal-meta" style="margin-bottom:12px;font-size:12px;color:var(--text-secondary)"></div>
71
+ <pre class="source-viewer" id="modal-source"></pre>
72
+ </div>
73
+ </div>
74
+ </div>
75
+ {% endblock %}
76
+
77
+ {% block scripts %}
78
+ <script>
79
+ async function showSource(id) {
80
+ const resp = await fetch('/api/item/' + id);
81
+ if (!resp.ok) return;
82
+ const item = await resp.json();
83
+ document.getElementById('modal-title').textContent = item.kind + ' ' + item.name;
84
+ document.getElementById('modal-meta').textContent = item.file_path + ':' + item.start_line + '-' + item.end_line;
85
+ document.getElementById('modal-source').textContent = item.source || '(no source)';
86
+ document.getElementById('source-modal').style.display = 'flex';
87
+ }
88
+ function closeModal() { document.getElementById('source-modal').style.display = 'none'; }
89
+ document.addEventListener('keydown', e => { if (e.key === 'Escape') closeModal(); });
90
+ </script>
91
+ {% endblock %}
rust_analyzer/web.py ADDED
@@ -0,0 +1,392 @@
1
+ """FastAPI web frontend for rust-analyzer-db.
2
+
3
+ Provides a master-panel style web UI with dashboard, item browser,
4
+ complexity report, call graph, API surface, dependency analysis, and search.
5
+
6
+ Usage:
7
+ rust-analyzer-db serve --db rust_code.db
8
+ rust-analyzer-db serve --db rust_code.db --port 8080 --host 0.0.0.0
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import math
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+ from fastapi import FastAPI, Query, Request
19
+ from fastapi.responses import HTMLResponse
20
+ from fastapi.staticfiles import StaticFiles
21
+ from fastapi.templating import Jinja2Templates
22
+
23
+ from . import __version__
24
+ from .db import RustCodeDB
25
+ from .logging import get_logger
26
+
27
+ log = get_logger("web")
28
+
29
+ STATIC_DIR = Path(__file__).parent / "static"
30
+ TEMPLATE_DIR = Path(__file__).parent / "templates"
31
+
32
+ app = FastAPI(title="Rust Analyzer DB", version=__version__)
33
+ app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
34
+ templates = Jinja2Templates(directory=str(TEMPLATE_DIR))
35
+
36
+ _db_path: str = "rust_code.db"
37
+
38
+
39
+ def _db() -> RustCodeDB:
40
+ return RustCodeDB(_db_path)
41
+
42
+
43
+ def _r(row: Any) -> dict[str, Any]:
44
+ if row is None:
45
+ return {}
46
+ return dict(row)
47
+
48
+
49
+ def _rs(rows: Any) -> list[dict[str, Any]]:
50
+ return [dict(r) for r in rows]
51
+
52
+
53
+ def _tr(request: Request, name: str, ctx: dict[str, Any]) -> HTMLResponse:
54
+ ctx["request"] = request
55
+ return templates.TemplateResponse(request, name, ctx)
56
+
57
+
58
+ # ── Dashboard ────────────────────────────────────────────────────────────────
59
+
60
+ @app.get("/", response_class=HTMLResponse)
61
+ def dashboard(request: Request) -> HTMLResponse:
62
+ db = _db()
63
+ try:
64
+ n_files, kind_rows_raw = db.stats()
65
+ kind_rows = _rs(kind_rows_raw)
66
+ total_items = sum(r["n"] for r in kind_rows)
67
+ total_calls, resolved_calls = db.call_graph_stats()
68
+ pub_count = len(db.api_surface())
69
+ top_complex = _rs(db.most_complex_functions(limit=10))
70
+ files = _rs(db.largest_files(limit=10))
71
+ extern_crates = _rs(db.all_extern_crates())
72
+
73
+ avg_complexity = 0.0
74
+ if top_complex:
75
+ avg_complexity = sum(r["cyclomatic_complexity"] for r in top_complex) / len(top_complex)
76
+
77
+ kind_labels = [r["kind"] for r in kind_rows]
78
+ kind_counts = [r["n"] for r in kind_rows]
79
+
80
+ cx_buckets: dict[str, int] = {"1": 0, "2-3": 0, "4-5": 0, "6-9": 0, "10-14": 0, "15+": 0}
81
+ all_fns = _rs(db.list_items(kind="function", limit=10000))
82
+ all_fns += _rs(db.list_items(kind="method", limit=10000))
83
+ for fn in all_fns:
84
+ cc = fn["cyclomatic_complexity"] or 1
85
+ if cc <= 1:
86
+ cx_buckets["1"] += 1
87
+ elif cc <= 3:
88
+ cx_buckets["2-3"] += 1
89
+ elif cc <= 5:
90
+ cx_buckets["4-5"] += 1
91
+ elif cc <= 9:
92
+ cx_buckets["6-9"] += 1
93
+ elif cc <= 14:
94
+ cx_buckets["10-14"] += 1
95
+ else:
96
+ cx_buckets["15+"] += 1
97
+
98
+ file_labels: list[str] = []
99
+ file_counts: list[int] = []
100
+ for f in files:
101
+ name_str = Path(f["path"]).name
102
+ if len(name_str) > 25:
103
+ name_str = name_str[:22] + "..."
104
+ file_labels.append(name_str)
105
+ file_counts.append(f["total_loc"] or 0)
106
+
107
+ return _tr(request, "dashboard.html", {
108
+ "active": "dashboard",
109
+ "n_files": n_files,
110
+ "total_items": total_items,
111
+ "total_calls": total_calls,
112
+ "resolved_calls": resolved_calls,
113
+ "pub_count": pub_count,
114
+ "avg_complexity": avg_complexity,
115
+ "n_extern_crates": len(extern_crates),
116
+ "top_complex": top_complex,
117
+ "kind_labels": kind_labels,
118
+ "kind_counts": kind_counts,
119
+ "cx_labels": list(cx_buckets.keys()),
120
+ "cx_counts": list(cx_buckets.values()),
121
+ "file_labels": file_labels,
122
+ "file_counts": file_counts,
123
+ })
124
+ finally:
125
+ db.close()
126
+
127
+
128
+ # ── Items Browser ────────────────────────────────────────────────────────────
129
+
130
+ @app.get("/items", response_class=HTMLResponse)
131
+ def items_page(
132
+ request: Request,
133
+ kind: str = "",
134
+ name: str = "",
135
+ file: str = "",
136
+ page: int = Query(1, ge=1),
137
+ per_page: int = 50,
138
+ ) -> HTMLResponse:
139
+ db = _db()
140
+ try:
141
+ all_kind_rows = _rs(db.stats()[1])
142
+ all_kinds = [r["kind"] for r in all_kind_rows]
143
+
144
+ all_rows = _rs(db.list_items(
145
+ kind=kind or None,
146
+ name=name or None,
147
+ file_like=file or None,
148
+ limit=10000,
149
+ ))
150
+ total = len(all_rows)
151
+ total_pages = max(1, math.ceil(total / per_page))
152
+ start = (page - 1) * per_page
153
+ items = all_rows[start:start + per_page]
154
+
155
+ return _tr(request, "items.html", {
156
+ "active": "items",
157
+ "items": items,
158
+ "total": total,
159
+ "page": page,
160
+ "total_pages": total_pages,
161
+ "kind_filter": kind,
162
+ "name_filter": name,
163
+ "file_filter": file,
164
+ "all_kinds": all_kinds,
165
+ })
166
+ finally:
167
+ db.close()
168
+
169
+
170
+ # ── Complexity ───────────────────────────────────────────────────────────────
171
+
172
+ @app.get("/complexity", response_class=HTMLResponse)
173
+ def complexity_page(
174
+ request: Request,
175
+ min: int = Query(5, ge=1, le=100),
176
+ ) -> HTMLResponse:
177
+ db = _db()
178
+ try:
179
+ rows = _rs(db.complexity_report(min_complexity=min))
180
+ max_cc = max((r["cyclomatic_complexity"] for r in rows), default=0)
181
+ max_cog = max((r["cognitive_complexity"] for r in rows), default=0)
182
+
183
+ return _tr(request, "complexity.html", {
184
+ "active": "complexity",
185
+ "rows": rows,
186
+ "min_cx": min,
187
+ "max_cc": max_cc,
188
+ "max_cog": max_cog,
189
+ })
190
+ finally:
191
+ db.close()
192
+
193
+
194
+ # ── Call Graph ───────────────────────────────────────────────────────────────
195
+
196
+ @app.get("/graph", response_class=HTMLResponse)
197
+ def graph_page(
198
+ request: Request,
199
+ root: str = "",
200
+ direction: str = "both",
201
+ depth: int = Query(2, ge=1, le=10),
202
+ unresolved: bool = True,
203
+ ) -> HTMLResponse:
204
+ from . import graph as graphmod
205
+
206
+ db = _db()
207
+ try:
208
+ title = "Call graph (whole project)"
209
+ node_count = 0
210
+ edge_count = 0
211
+
212
+ if root:
213
+ rows = _rs(db.list_items(name=root, limit=50))
214
+ rows = [r for r in rows if r["kind"] in ("function", "method")]
215
+ if rows:
216
+ root_ids = [r["id"] for r in rows]
217
+ names = ", ".join(r["name"] for r in rows[:3])
218
+ title = f"Call graph: {names} (depth {depth}, {direction})"
219
+ g = graphmod.build_subgraph(
220
+ db, root_ids, depth=depth, direction=direction,
221
+ include_unresolved=unresolved,
222
+ )
223
+ else:
224
+ g = graphmod.CallGraph()
225
+ title = f"No function found matching '{root}'"
226
+ else:
227
+ g = graphmod.build_whole_graph(db, include_unresolved=unresolved)
228
+
229
+ nodes_data = [{"id": nid, **meta} for nid, meta in g.nodes.items()]
230
+ edges_data = [{"from": s, "to": d} for s, d in sorted(g.edges)]
231
+ node_count = len(nodes_data)
232
+ edge_count = len(edges_data)
233
+
234
+ return _tr(request, "graph.html", {
235
+ "active": "graph",
236
+ "nodes_json": json.dumps(nodes_data),
237
+ "edges_json": json.dumps(edges_data),
238
+ "title": title,
239
+ "node_count": node_count,
240
+ "edge_count": edge_count,
241
+ "root_name": root,
242
+ "direction": direction,
243
+ "depth": depth,
244
+ "no_unresolved": not unresolved,
245
+ })
246
+ finally:
247
+ db.close()
248
+
249
+
250
+ # ── API Surface ──────────────────────────────────────────────────────────────
251
+
252
+ @app.get("/api-surface", response_class=HTMLResponse)
253
+ def api_surface_page(request: Request) -> HTMLResponse:
254
+ db = _db()
255
+ try:
256
+ rows = _rs(db.api_surface())
257
+ by_kind: dict[str, list[dict[str, Any]]] = {}
258
+ for row in rows:
259
+ by_kind.setdefault(row["kind"], []).append(row)
260
+
261
+ return _tr(request, "api_surface.html", {
262
+ "active": "api",
263
+ "by_kind": by_kind,
264
+ "total": len(rows),
265
+ })
266
+ finally:
267
+ db.close()
268
+
269
+
270
+ # ── Dependencies ─────────────────────────────────────────────────────────────
271
+
272
+ @app.get("/deps", response_class=HTMLResponse)
273
+ def deps_page(request: Request) -> HTMLResponse:
274
+ db = _db()
275
+ try:
276
+ extern_crates = _rs(db.all_extern_crates())
277
+ uses = _rs(db.get_use_declarations())
278
+
279
+ crate_groups: dict[str, list[str]] = {}
280
+ for r in uses:
281
+ path = r["path"]
282
+ top = path.split("::")[0] if path else "(unknown)"
283
+ crate_groups.setdefault(top, []).append(path)
284
+
285
+ return _tr(request, "deps.html", {
286
+ "active": "deps",
287
+ "extern_crates": extern_crates,
288
+ "crate_groups": crate_groups,
289
+ })
290
+ finally:
291
+ db.close()
292
+
293
+
294
+ # ── Search ───────────────────────────────────────────────────────────────────
295
+
296
+ @app.get("/search", response_class=HTMLResponse)
297
+ def search_page(
298
+ request: Request,
299
+ q: str = "",
300
+ kind: str = "",
301
+ ) -> HTMLResponse:
302
+ db = _db()
303
+ try:
304
+ all_kind_rows = _rs(db.stats()[1])
305
+ all_kinds = [r["kind"] for r in all_kind_rows]
306
+
307
+ rows: list[dict[str, Any]] = []
308
+ if q:
309
+ try:
310
+ rows = _rs(db.search(q, kind=kind or None, limit=200))
311
+ except Exception:
312
+ rows = []
313
+
314
+ return _tr(request, "search.html", {
315
+ "active": "search",
316
+ "rows": rows,
317
+ "query": q,
318
+ "kind_filter": kind,
319
+ "all_kinds": all_kinds,
320
+ })
321
+ finally:
322
+ db.close()
323
+
324
+
325
+ # ── JSON API ─────────────────────────────────────────────────────────────────
326
+
327
+ @app.get("/api/item/{item_id}")
328
+ def api_item(item_id: int) -> dict[str, Any]:
329
+ db = _db()
330
+ try:
331
+ row = db.get_item(item_id)
332
+ if row is None:
333
+ return {"error": "not found"}
334
+ return _r(row)
335
+ finally:
336
+ db.close()
337
+
338
+
339
+ @app.get("/api/stats")
340
+ def api_stats() -> dict[str, Any]:
341
+ db = _db()
342
+ try:
343
+ n_files, rows = db.stats()
344
+ total, resolved = db.call_graph_stats()
345
+ return {
346
+ "files": n_files,
347
+ "items_by_kind": {r["kind"]: r["n"] for r in rows},
348
+ "total_calls": total,
349
+ "resolved_calls": resolved,
350
+ }
351
+ finally:
352
+ db.close()
353
+
354
+
355
+ @app.get("/api/graph-data")
356
+ def api_graph_data(
357
+ root: str = "",
358
+ direction: str = "both",
359
+ depth: int = Query(2, ge=1, le=10),
360
+ unresolved: bool = True,
361
+ ) -> dict[str, Any]:
362
+ from . import graph as graphmod
363
+
364
+ db = _db()
365
+ try:
366
+ if root:
367
+ rows = _rs(db.list_items(name=root, limit=50))
368
+ rows = [r for r in rows if r["kind"] in ("function", "method")]
369
+ if rows:
370
+ g = graphmod.build_subgraph(
371
+ db, [r["id"] for r in rows], depth=depth,
372
+ direction=direction, include_unresolved=unresolved,
373
+ )
374
+ else:
375
+ g = graphmod.CallGraph()
376
+ else:
377
+ g = graphmod.build_whole_graph(db, include_unresolved=unresolved)
378
+
379
+ return {
380
+ "nodes": [{"id": nid, **meta} for nid, meta in g.nodes.items()],
381
+ "edges": [{"from": s, "to": d} for s, d in sorted(g.edges)],
382
+ }
383
+ finally:
384
+ db.close()
385
+
386
+
387
+ # ── Serve helper ─────────────────────────────────────────────────────────────
388
+
389
+ def create_app(db_path: str) -> FastAPI:
390
+ global _db_path
391
+ _db_path = db_path
392
+ return app