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
|
@@ -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()">×</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()">×</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
|