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 @@
|
|
|
1
|
+
__version__ = "0.2.0"
|
rust_analyzer/cli.py
ADDED
|
@@ -0,0 +1,663 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""rust-analyzer-db: Professional Rust code analysis tool.
|
|
3
|
+
|
|
4
|
+
Scans Rust source with tree-sitter, extracts structs/impls/functions/etc.
|
|
5
|
+
into a SQLite DB, and provides query, search, call-graph, complexity
|
|
6
|
+
analysis, and public-API surface commands.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import hashlib
|
|
13
|
+
import json
|
|
14
|
+
import sys
|
|
15
|
+
import time
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from .db import RustCodeDB
|
|
20
|
+
from .exceptions import AnalysisError, ParseError
|
|
21
|
+
from .extractor import ExtractedItem, FileExtraction, extract_file, extract_calls
|
|
22
|
+
from . import graph as graphmod
|
|
23
|
+
from .logging import get_logger, setup_logging
|
|
24
|
+
|
|
25
|
+
log = get_logger("cli")
|
|
26
|
+
|
|
27
|
+
DEFAULT_DB = "rust_code.db"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ------------------------------------------------------------------
|
|
31
|
+
# Helpers
|
|
32
|
+
# ------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
def _hash_bytes(data: bytes) -> str:
|
|
35
|
+
return hashlib.sha256(data).hexdigest()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _store_item(
|
|
39
|
+
db: RustCodeDB,
|
|
40
|
+
file_id: int,
|
|
41
|
+
item: ExtractedItem,
|
|
42
|
+
src: bytes,
|
|
43
|
+
parent_id: int | None = None,
|
|
44
|
+
) -> int:
|
|
45
|
+
complexity = item.complexity
|
|
46
|
+
item_id = db.insert_item(
|
|
47
|
+
file_id=file_id,
|
|
48
|
+
kind=item.kind,
|
|
49
|
+
name=item.name,
|
|
50
|
+
start_line=item.start_line,
|
|
51
|
+
end_line=item.end_line,
|
|
52
|
+
source=item.source,
|
|
53
|
+
target=item.target,
|
|
54
|
+
trait_name=item.trait_name,
|
|
55
|
+
visibility=item.visibility,
|
|
56
|
+
signature=item.signature,
|
|
57
|
+
doc=item.doc,
|
|
58
|
+
attributes=item.attributes,
|
|
59
|
+
parent_id=parent_id,
|
|
60
|
+
is_pub=item.is_pub,
|
|
61
|
+
is_const_fn=item.is_const_fn,
|
|
62
|
+
is_async=item.is_async,
|
|
63
|
+
is_unsafe=item.is_unsafe,
|
|
64
|
+
cyclomatic_complexity=complexity.cyclomatic if complexity else 1,
|
|
65
|
+
cognitive_complexity=complexity.cognitive if complexity else 0,
|
|
66
|
+
nesting_depth=complexity.nesting_depth if complexity else 0,
|
|
67
|
+
num_branches=complexity.num_branches if complexity else 0,
|
|
68
|
+
num_function_calls=complexity.num_function_calls if complexity else 0,
|
|
69
|
+
lines_of_code=complexity.lines_of_code if complexity else 0,
|
|
70
|
+
)
|
|
71
|
+
# store generic params
|
|
72
|
+
if item.generic_params:
|
|
73
|
+
db.insert_generic_params(
|
|
74
|
+
item_id,
|
|
75
|
+
[(g.name, ", ".join(g.bounds), g.default) for g in item.generic_params],
|
|
76
|
+
)
|
|
77
|
+
# store lifetime params
|
|
78
|
+
if item.lifetime_params:
|
|
79
|
+
db.insert_lifetime_params(
|
|
80
|
+
item_id,
|
|
81
|
+
[(l.name, ", ".join(l.bounds)) for l in item.lifetime_params],
|
|
82
|
+
)
|
|
83
|
+
# extract calls from function/method bodies
|
|
84
|
+
if item.body_node is not None and item.kind in ("function", "method"):
|
|
85
|
+
for edge in extract_calls(item.body_node, src):
|
|
86
|
+
db.insert_call(
|
|
87
|
+
caller_id=item_id,
|
|
88
|
+
callee_name=edge.callee_name,
|
|
89
|
+
line=edge.line,
|
|
90
|
+
receiver=edge.receiver,
|
|
91
|
+
is_method_call=edge.is_method_call,
|
|
92
|
+
)
|
|
93
|
+
for child in item.children:
|
|
94
|
+
_store_item(db, file_id, child, src, parent_id=item_id)
|
|
95
|
+
return item_id
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _output_json(data: Any) -> None:
|
|
99
|
+
"""Print data as JSON to stdout."""
|
|
100
|
+
print(json.dumps(data, indent=2, default=str))
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _output_rows(rows: list[Any], fields: list[str] | None = None) -> list[dict[str, Any]]:
|
|
104
|
+
"""Convert rows to list of dicts."""
|
|
105
|
+
return [dict(r) for r in rows]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# ------------------------------------------------------------------
|
|
109
|
+
# Commands
|
|
110
|
+
# ------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
def cmd_scan(args: argparse.Namespace) -> int:
|
|
113
|
+
"""Scan Rust source files and populate the database."""
|
|
114
|
+
root = Path(args.path)
|
|
115
|
+
if not root.exists():
|
|
116
|
+
log.error("Path not found: %s", root)
|
|
117
|
+
return 1
|
|
118
|
+
|
|
119
|
+
rs_files = [root] if root.is_file() else sorted(root.rglob("*.rs"))
|
|
120
|
+
if not rs_files:
|
|
121
|
+
log.info("No .rs files found.")
|
|
122
|
+
return 0
|
|
123
|
+
|
|
124
|
+
skip_dirs = {"target", ".git", "node_modules", ".cargo"}
|
|
125
|
+
rs_files = [
|
|
126
|
+
f for f in rs_files
|
|
127
|
+
if not any(part in skip_dirs for part in f.parts)
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
db = RustCodeDB(args.db)
|
|
131
|
+
total_items = 0
|
|
132
|
+
total_uses = 0
|
|
133
|
+
total_externs = 0
|
|
134
|
+
scanned = 0
|
|
135
|
+
skipped = 0
|
|
136
|
+
errors = 0
|
|
137
|
+
start_time = time.monotonic()
|
|
138
|
+
|
|
139
|
+
for i, f in enumerate(rs_files):
|
|
140
|
+
if not args.quiet:
|
|
141
|
+
progress = f"[{i+1}/{len(rs_files)}]" if len(rs_files) > 1 else ""
|
|
142
|
+
print(f"\r {progress} Scanning {f}...", end="", flush=True) if len(rs_files) > 10 else None
|
|
143
|
+
|
|
144
|
+
data = f.read_bytes()
|
|
145
|
+
file_hash = _hash_bytes(data)
|
|
146
|
+
abs_path = str(f.resolve())
|
|
147
|
+
if not args.force and db.file_unchanged(abs_path, file_hash):
|
|
148
|
+
skipped += 1
|
|
149
|
+
continue
|
|
150
|
+
|
|
151
|
+
file_id = db.upsert_file(abs_path, f.stat().st_mtime, file_hash)
|
|
152
|
+
try:
|
|
153
|
+
extraction = extract_file(data)
|
|
154
|
+
except ParseError as e:
|
|
155
|
+
log.warning("Parse error in %s: %s", f, e.reason)
|
|
156
|
+
errors += 1
|
|
157
|
+
continue
|
|
158
|
+
except Exception as e:
|
|
159
|
+
log.error("Failed to parse %s: %s", f, e)
|
|
160
|
+
errors += 1
|
|
161
|
+
continue
|
|
162
|
+
|
|
163
|
+
for item in extraction.items:
|
|
164
|
+
_store_item(db, file_id, item, data)
|
|
165
|
+
|
|
166
|
+
db.update_file_lines(file_id, extraction.total_lines)
|
|
167
|
+
|
|
168
|
+
for use in extraction.use_declarations:
|
|
169
|
+
db.insert_use_decl(
|
|
170
|
+
file_id, use.path, use.alias, use.is_glob,
|
|
171
|
+
use.start_line, use.end_line,
|
|
172
|
+
)
|
|
173
|
+
total_uses += 1
|
|
174
|
+
|
|
175
|
+
for ext in extraction.extern_crates:
|
|
176
|
+
db.insert_extern_crate(
|
|
177
|
+
file_id, ext.name, ext.alias, ext.start_line, ext.end_line,
|
|
178
|
+
)
|
|
179
|
+
total_externs += 1
|
|
180
|
+
|
|
181
|
+
db.commit()
|
|
182
|
+
count = _count_items(extraction.items)
|
|
183
|
+
total_items += count
|
|
184
|
+
scanned += 1
|
|
185
|
+
if not args.quiet and len(rs_files) <= 10:
|
|
186
|
+
log.info(" %s -> %d items (%d LOC)", f, count, extraction.total_lines)
|
|
187
|
+
|
|
188
|
+
elapsed = time.monotonic() - start_time
|
|
189
|
+
|
|
190
|
+
if scanned:
|
|
191
|
+
total, resolved = db.resolve_calls()
|
|
192
|
+
log.info("Resolved %d/%d call edges.", resolved, total)
|
|
193
|
+
|
|
194
|
+
if not args.quiet:
|
|
195
|
+
print() # newline after progress
|
|
196
|
+
log.info(
|
|
197
|
+
"Scanned %d file(s), skipped %d unchanged, errors %d, "
|
|
198
|
+
"extracted %d items, %d use decls, %d extern crates. "
|
|
199
|
+
"DB: %s (%.1fs)",
|
|
200
|
+
scanned, skipped, errors, total_items, total_uses, total_externs,
|
|
201
|
+
args.db, elapsed,
|
|
202
|
+
)
|
|
203
|
+
db.close()
|
|
204
|
+
return 0
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _count_items(items: list[ExtractedItem]) -> int:
|
|
208
|
+
"""Recursively count items including children."""
|
|
209
|
+
count = len(items)
|
|
210
|
+
for item in items:
|
|
211
|
+
count += _count_items(item.children)
|
|
212
|
+
return count
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _print_row(row: Any, show_source: bool = False) -> None:
|
|
216
|
+
loc = f"{row['file_path']}:{row['start_line']}-{row['end_line']}"
|
|
217
|
+
header = f"[{row['id']}] {row['kind']:<12} {row['name']}"
|
|
218
|
+
if row["target"] and row["kind"] not in ("impl",):
|
|
219
|
+
header += f" (impl for {row['target']})"
|
|
220
|
+
vis = row["visibility"] or ""
|
|
221
|
+
cx = ""
|
|
222
|
+
if row["cyclomatic_complexity"] and row["cyclomatic_complexity"] > 1:
|
|
223
|
+
cx = f" CC={row['cyclomatic_complexity']}"
|
|
224
|
+
print(f"{header} {vis}{cx} {loc}")
|
|
225
|
+
if show_source:
|
|
226
|
+
print("-" * 70)
|
|
227
|
+
print(row["source"])
|
|
228
|
+
print("-" * 70)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def cmd_list(args: argparse.Namespace) -> int:
|
|
232
|
+
"""List items with optional filters."""
|
|
233
|
+
db = RustCodeDB(args.db)
|
|
234
|
+
rows = db.list_items(
|
|
235
|
+
kind=args.kind, name=args.name, target=args.target,
|
|
236
|
+
file_like=args.file, limit=args.limit,
|
|
237
|
+
)
|
|
238
|
+
if not rows:
|
|
239
|
+
log.info("No matching items.")
|
|
240
|
+
return 0
|
|
241
|
+
|
|
242
|
+
if args.json:
|
|
243
|
+
_output_json(_output_rows(rows))
|
|
244
|
+
else:
|
|
245
|
+
for row in rows:
|
|
246
|
+
_print_row(row)
|
|
247
|
+
log.info("%d item(s).", len(rows))
|
|
248
|
+
db.close()
|
|
249
|
+
return 0
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def cmd_show(args: argparse.Namespace) -> int:
|
|
253
|
+
"""Show full source code of matching items."""
|
|
254
|
+
db = RustCodeDB(args.db)
|
|
255
|
+
rows = db.list_items(kind=args.kind, name=args.name, limit=args.limit)
|
|
256
|
+
if not rows:
|
|
257
|
+
log.info("No matching items.")
|
|
258
|
+
return 0
|
|
259
|
+
|
|
260
|
+
if args.json:
|
|
261
|
+
_output_json(_output_rows(rows))
|
|
262
|
+
else:
|
|
263
|
+
for row in rows:
|
|
264
|
+
if row["doc"]:
|
|
265
|
+
print(row["doc"])
|
|
266
|
+
if row["attributes"]:
|
|
267
|
+
print(row["attributes"])
|
|
268
|
+
_print_row(row, show_source=True)
|
|
269
|
+
print()
|
|
270
|
+
db.close()
|
|
271
|
+
return 0
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def cmd_methods(args: argparse.Namespace) -> int:
|
|
275
|
+
"""List methods on a type or trait."""
|
|
276
|
+
db = RustCodeDB(args.db)
|
|
277
|
+
rows = db.get_methods_of(args.target)
|
|
278
|
+
if not rows:
|
|
279
|
+
log.info("No methods found for '%s'.", args.target)
|
|
280
|
+
return 0
|
|
281
|
+
|
|
282
|
+
if args.json:
|
|
283
|
+
_output_json(_output_rows(rows))
|
|
284
|
+
else:
|
|
285
|
+
for row in rows:
|
|
286
|
+
_print_row(row, show_source=args.full)
|
|
287
|
+
log.info("%d method(s) on types matching '%s'.", len(rows), args.target)
|
|
288
|
+
db.close()
|
|
289
|
+
return 0
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def cmd_search(args: argparse.Namespace) -> int:
|
|
293
|
+
"""Full-text search over name/signature/doc/source."""
|
|
294
|
+
db = RustCodeDB(args.db)
|
|
295
|
+
try:
|
|
296
|
+
rows = db.search(args.query, kind=args.kind, limit=args.limit)
|
|
297
|
+
except Exception as e:
|
|
298
|
+
log.error("Search error (try quoting the query): %s", e)
|
|
299
|
+
return 1
|
|
300
|
+
if not rows:
|
|
301
|
+
log.info("No matches.")
|
|
302
|
+
return 0
|
|
303
|
+
|
|
304
|
+
if args.json:
|
|
305
|
+
_output_json(_output_rows(rows))
|
|
306
|
+
else:
|
|
307
|
+
for row in rows:
|
|
308
|
+
_print_row(row, show_source=args.full)
|
|
309
|
+
log.info("%d match(es).", len(rows))
|
|
310
|
+
db.close()
|
|
311
|
+
return 0
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def cmd_serve(args: argparse.Namespace) -> int:
|
|
315
|
+
"""Start the web frontend server."""
|
|
316
|
+
try:
|
|
317
|
+
import uvicorn
|
|
318
|
+
except ImportError:
|
|
319
|
+
log.error("uvicorn is required for the web server. Install with: pip install uvicorn")
|
|
320
|
+
return 1
|
|
321
|
+
|
|
322
|
+
from .web import create_app
|
|
323
|
+
|
|
324
|
+
if not Path(args.db).exists():
|
|
325
|
+
log.error("Database not found: %s. Run 'scan' first.", args.db)
|
|
326
|
+
return 1
|
|
327
|
+
|
|
328
|
+
app = create_app(args.db)
|
|
329
|
+
log.info("Starting web server at http://%s:%d", args.host, args.port)
|
|
330
|
+
log.info("Database: %s", args.db)
|
|
331
|
+
uvicorn.run(app, host=args.host, port=args.port, log_level="info")
|
|
332
|
+
return 0
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def cmd_mcp(args: argparse.Namespace) -> int:
|
|
336
|
+
"""Start the MCP server over stdio transport."""
|
|
337
|
+
from .mcp_server import run_server
|
|
338
|
+
|
|
339
|
+
if not Path(args.db).exists():
|
|
340
|
+
log.error("Database not found: %s. Run 'scan' first.", args.db)
|
|
341
|
+
return 1
|
|
342
|
+
|
|
343
|
+
log.info("Starting MCP server (db: %s)", args.db)
|
|
344
|
+
run_server(args.db)
|
|
345
|
+
return 0
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def cmd_graph(args: argparse.Namespace) -> int:
|
|
349
|
+
"""Render a call graph."""
|
|
350
|
+
db = RustCodeDB(args.db)
|
|
351
|
+
|
|
352
|
+
if args.root:
|
|
353
|
+
rows = db.list_items(name=args.root, limit=50)
|
|
354
|
+
rows = [r for r in rows if r["kind"] in ("function", "method")]
|
|
355
|
+
if not rows:
|
|
356
|
+
log.error("No function/method found matching '%s'.", args.root)
|
|
357
|
+
return 1
|
|
358
|
+
root_ids = [r["id"] for r in rows]
|
|
359
|
+
names = ", ".join(f"{r['name']} ({r['file_path']}:{r['start_line']})" for r in rows)
|
|
360
|
+
log.info("Root node(s): %s", names)
|
|
361
|
+
g = graphmod.build_subgraph(
|
|
362
|
+
db, root_ids, depth=args.depth, direction=args.direction,
|
|
363
|
+
include_unresolved=not args.no_unresolved,
|
|
364
|
+
)
|
|
365
|
+
title = f"Call graph: {args.root} (depth {args.depth}, {args.direction})"
|
|
366
|
+
else:
|
|
367
|
+
kinds = set(args.kind.split(",")) if args.kind else None
|
|
368
|
+
g = graphmod.build_whole_graph(db, include_unresolved=not args.no_unresolved, kinds=kinds)
|
|
369
|
+
title = "Call graph (whole project)"
|
|
370
|
+
|
|
371
|
+
if not g.nodes:
|
|
372
|
+
log.info("No call edges found. Did you run `scan` on this project?")
|
|
373
|
+
return 0
|
|
374
|
+
|
|
375
|
+
log.info("Graph: %d node(s), %d edge(s).", len(g.nodes), len(g.edges))
|
|
376
|
+
if len(g.nodes) > 400:
|
|
377
|
+
log.warning(
|
|
378
|
+
"Large graph — use --root <function> to focus or --no-unresolved "
|
|
379
|
+
"to drop external calls."
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
fmt = args.format
|
|
383
|
+
out = args.output
|
|
384
|
+
|
|
385
|
+
if args.json:
|
|
386
|
+
_output_json({
|
|
387
|
+
"title": title,
|
|
388
|
+
"node_count": len(g.nodes),
|
|
389
|
+
"edge_count": len(g.edges),
|
|
390
|
+
"nodes": [{"id": nid, **meta} for nid, meta in g.nodes.items()],
|
|
391
|
+
"edges": [{"from": s, "to": d} for s, d in sorted(g.edges)],
|
|
392
|
+
})
|
|
393
|
+
db.close()
|
|
394
|
+
return 0
|
|
395
|
+
|
|
396
|
+
if fmt == "dot":
|
|
397
|
+
dot_str = graphmod.to_dot(g, title)
|
|
398
|
+
Path(out).write_text(dot_str)
|
|
399
|
+
log.info("Wrote DOT file: %s", out)
|
|
400
|
+
elif fmt == "html":
|
|
401
|
+
html_str = graphmod.to_html(g, title)
|
|
402
|
+
Path(out).write_text(html_str)
|
|
403
|
+
log.info("Wrote interactive HTML: %s (open in a browser)", out)
|
|
404
|
+
elif fmt in ("svg", "png", "pdf"):
|
|
405
|
+
dot_str = graphmod.to_dot(g, title)
|
|
406
|
+
try:
|
|
407
|
+
ok = graphmod.render_dot(dot_str, out, fmt)
|
|
408
|
+
except RuntimeError as e:
|
|
409
|
+
log.error("Graphviz render failed: %s", e)
|
|
410
|
+
return 1
|
|
411
|
+
if not ok:
|
|
412
|
+
fallback = str(Path(out).with_suffix(".dot"))
|
|
413
|
+
Path(fallback).write_text(dot_str)
|
|
414
|
+
log.warning("Graphviz 'dot' not found; wrote DOT to %s", fallback)
|
|
415
|
+
else:
|
|
416
|
+
log.info("Wrote %s: %s", fmt.upper(), out)
|
|
417
|
+
db.close()
|
|
418
|
+
return 0
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def cmd_stats(args: argparse.Namespace) -> int:
|
|
422
|
+
"""Show project statistics."""
|
|
423
|
+
db = RustCodeDB(args.db)
|
|
424
|
+
n_files, rows = db.stats()
|
|
425
|
+
total, resolved = db.call_graph_stats()
|
|
426
|
+
|
|
427
|
+
if args.json:
|
|
428
|
+
data = {
|
|
429
|
+
"files": n_files,
|
|
430
|
+
"items_by_kind": {r["kind"]: r["n"] for r in rows},
|
|
431
|
+
"total_calls": total,
|
|
432
|
+
"resolved_calls": resolved,
|
|
433
|
+
}
|
|
434
|
+
_output_json(data)
|
|
435
|
+
else:
|
|
436
|
+
total_items = sum(r["n"] for r in rows)
|
|
437
|
+
print(f"Files scanned: {n_files}")
|
|
438
|
+
print(f"Total items: {total_items}")
|
|
439
|
+
print(f"Call edges: {total} ({resolved} resolved)")
|
|
440
|
+
print()
|
|
441
|
+
print("Items by kind:")
|
|
442
|
+
for row in rows:
|
|
443
|
+
bar = "█" * min(row["n"], 50)
|
|
444
|
+
print(f" {row['kind']:<14} {row['n']:>6} {bar}")
|
|
445
|
+
db.close()
|
|
446
|
+
return 0
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def cmd_complexity(args: argparse.Namespace) -> int:
|
|
450
|
+
"""Show complexity report for functions/methods."""
|
|
451
|
+
db = RustCodeDB(args.db)
|
|
452
|
+
rows = db.complexity_report(min_complexity=args.min_complexity)
|
|
453
|
+
|
|
454
|
+
if not rows:
|
|
455
|
+
log.info("No functions exceed complexity threshold %d.", args.min_complexity)
|
|
456
|
+
return 0
|
|
457
|
+
|
|
458
|
+
if args.json:
|
|
459
|
+
_output_json(_output_rows(rows))
|
|
460
|
+
else:
|
|
461
|
+
print(f"{'Kind':<10} {'Name':<35} {'CC':>4} {'Cog':>4} {'Nest':>5} {'LOC':>5} Location")
|
|
462
|
+
print("-" * 90)
|
|
463
|
+
for row in rows:
|
|
464
|
+
loc = f"{row['file_path']}:{row['start_line']}"
|
|
465
|
+
print(
|
|
466
|
+
f"{row['kind']:<10} {row['name']:<35} "
|
|
467
|
+
f"{row['cyclomatic_complexity']:>4} {row['cognitive_complexity']:>4} "
|
|
468
|
+
f"{row['nesting_depth']:>5} {row['lines_of_code']:>5} {loc}"
|
|
469
|
+
)
|
|
470
|
+
log.info("%d function(s) above complexity threshold.", len(rows))
|
|
471
|
+
db.close()
|
|
472
|
+
return 0
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def cmd_api(args: argparse.Namespace) -> int:
|
|
476
|
+
"""Show the public API surface of the project."""
|
|
477
|
+
db = RustCodeDB(args.db)
|
|
478
|
+
rows = db.api_surface()
|
|
479
|
+
|
|
480
|
+
if not rows:
|
|
481
|
+
log.info("No public items found.")
|
|
482
|
+
return 0
|
|
483
|
+
|
|
484
|
+
if args.json:
|
|
485
|
+
_output_json(_output_rows(rows))
|
|
486
|
+
else:
|
|
487
|
+
by_kind: dict[str, list[Any]] = {}
|
|
488
|
+
for row in rows:
|
|
489
|
+
by_kind.setdefault(row["kind"], []).append(row)
|
|
490
|
+
for kind in ("struct", "enum", "trait", "function", "method", "const", "type_alias", "union"):
|
|
491
|
+
items = by_kind.get(kind, [])
|
|
492
|
+
if not items:
|
|
493
|
+
continue
|
|
494
|
+
print(f"\n{kind.upper()} ({len(items)}):")
|
|
495
|
+
for row in items:
|
|
496
|
+
vis = row["visibility"] or ""
|
|
497
|
+
loc = f"{row['file_path']}:{row['start_line']}"
|
|
498
|
+
sig = f" {row['signature']}" if row["signature"] else ""
|
|
499
|
+
print(f" {vis} {row['name']}{sig} -- {loc}")
|
|
500
|
+
log.info("%d public item(s) total.", len(rows))
|
|
501
|
+
db.close()
|
|
502
|
+
return 0
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def cmd_deps(args: argparse.Namespace) -> int:
|
|
506
|
+
"""Show external dependencies (use/extern crate)."""
|
|
507
|
+
db = RustCodeDB(args.db)
|
|
508
|
+
|
|
509
|
+
externs = db.all_extern_crates()
|
|
510
|
+
uses = db.get_use_declarations()
|
|
511
|
+
|
|
512
|
+
if args.json:
|
|
513
|
+
_output_json({
|
|
514
|
+
"extern_crates": [dict(r) for r in externs],
|
|
515
|
+
"use_declarations": [dict(r) for r in uses],
|
|
516
|
+
})
|
|
517
|
+
else:
|
|
518
|
+
if externs:
|
|
519
|
+
print("External crates:")
|
|
520
|
+
for r in externs:
|
|
521
|
+
alias = f" as {r['alias']}" if r["alias"] else ""
|
|
522
|
+
print(f" {r['name']}{alias}")
|
|
523
|
+
else:
|
|
524
|
+
print("No extern crate declarations found.")
|
|
525
|
+
|
|
526
|
+
# Group use paths by top-level crate
|
|
527
|
+
crate_uses: dict[str, list[str]] = {}
|
|
528
|
+
for r in uses:
|
|
529
|
+
path = r["path"]
|
|
530
|
+
top = path.split("::")[0]
|
|
531
|
+
crate_uses.setdefault(top, []).append(path)
|
|
532
|
+
|
|
533
|
+
if crate_uses:
|
|
534
|
+
print(f"\nUse declarations ({len(crate_uses)} top-level crates):")
|
|
535
|
+
for crate_name in sorted(crate_uses):
|
|
536
|
+
paths = crate_uses[crate_name]
|
|
537
|
+
print(f" {crate_name} ({len(paths)} imports)")
|
|
538
|
+
if args.full:
|
|
539
|
+
for p in sorted(paths):
|
|
540
|
+
print(f" use {p};")
|
|
541
|
+
db.close()
|
|
542
|
+
return 0
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
# ------------------------------------------------------------------
|
|
546
|
+
# CLI parser
|
|
547
|
+
# ------------------------------------------------------------------
|
|
548
|
+
|
|
549
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
550
|
+
db_parent = argparse.ArgumentParser(add_help=False)
|
|
551
|
+
db_parent.add_argument("--db", default=DEFAULT_DB, help=f"SQLite DB path (default: {DEFAULT_DB})")
|
|
552
|
+
|
|
553
|
+
output_parent = argparse.ArgumentParser(add_help=False)
|
|
554
|
+
output_parent.add_argument("--json", action="store_true", help="Output as JSON")
|
|
555
|
+
|
|
556
|
+
p = argparse.ArgumentParser(
|
|
557
|
+
prog="rust-analyzer-db",
|
|
558
|
+
description=__doc__,
|
|
559
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
560
|
+
parents=[db_parent],
|
|
561
|
+
)
|
|
562
|
+
p.add_argument("-v", "--verbose", action="store_true", help="Enable debug logging")
|
|
563
|
+
sub = p.add_subparsers(dest="command", required=True)
|
|
564
|
+
|
|
565
|
+
# scan
|
|
566
|
+
s = sub.add_parser("scan", help="Scan a file or directory of .rs files", parents=[db_parent])
|
|
567
|
+
s.add_argument("path", help="File or directory to scan")
|
|
568
|
+
s.add_argument("--force", action="store_true", help="Re-parse even unchanged files")
|
|
569
|
+
s.add_argument("-q", "--quiet", action="store_true", help="Suppress progress output")
|
|
570
|
+
s.set_defaults(func=cmd_scan)
|
|
571
|
+
|
|
572
|
+
# list
|
|
573
|
+
l = sub.add_parser("list", help="List items, optionally filtered", parents=[db_parent, output_parent])
|
|
574
|
+
l.add_argument("--kind", help="struct|enum|trait|impl|function|method|const|static|mod|...")
|
|
575
|
+
l.add_argument("--name", help="Substring match on name")
|
|
576
|
+
l.add_argument("--target", help="Substring match on impl/method target type")
|
|
577
|
+
l.add_argument("--file", help="Substring match on file path")
|
|
578
|
+
l.add_argument("--limit", type=int, default=200)
|
|
579
|
+
l.set_defaults(func=cmd_list)
|
|
580
|
+
|
|
581
|
+
# show
|
|
582
|
+
sh = sub.add_parser("show", help="Show full source of matching item(s)", parents=[db_parent, output_parent])
|
|
583
|
+
sh.add_argument("name", help="Exact-ish name (substring match)")
|
|
584
|
+
sh.add_argument("--kind", help="Restrict to a kind")
|
|
585
|
+
sh.add_argument("--limit", type=int, default=20)
|
|
586
|
+
sh.set_defaults(func=cmd_show)
|
|
587
|
+
|
|
588
|
+
# methods
|
|
589
|
+
m = sub.add_parser("methods", help="List methods on a type/trait", parents=[db_parent, output_parent])
|
|
590
|
+
m.add_argument("target", help="Type name, e.g. MyStruct")
|
|
591
|
+
m.add_argument("--full", action="store_true", help="Print full source of each method")
|
|
592
|
+
m.set_defaults(func=cmd_methods)
|
|
593
|
+
|
|
594
|
+
# search
|
|
595
|
+
se = sub.add_parser("search", help="Full-text search over name/signature/doc/source", parents=[db_parent, output_parent])
|
|
596
|
+
se.add_argument("query", help="FTS5 query, e.g. 'parse OR tokenize'")
|
|
597
|
+
se.add_argument("--kind", help="Restrict to a kind")
|
|
598
|
+
se.add_argument("--limit", type=int, default=50)
|
|
599
|
+
se.add_argument("--full", action="store_true", help="Print full source of each match")
|
|
600
|
+
se.set_defaults(func=cmd_search)
|
|
601
|
+
|
|
602
|
+
# stats
|
|
603
|
+
st = sub.add_parser("stats", help="Show summary counts", parents=[db_parent, output_parent])
|
|
604
|
+
st.set_defaults(func=cmd_stats)
|
|
605
|
+
|
|
606
|
+
# complexity
|
|
607
|
+
cx = sub.add_parser("complexity", help="Show cyclomatic complexity report", parents=[db_parent, output_parent])
|
|
608
|
+
cx.add_argument("--min", dest="min_complexity", type=int, default=5,
|
|
609
|
+
help="Minimum complexity to report (default 5)")
|
|
610
|
+
cx.set_defaults(func=cmd_complexity)
|
|
611
|
+
|
|
612
|
+
# api
|
|
613
|
+
api = sub.add_parser("api", help="Show public API surface", parents=[db_parent, output_parent])
|
|
614
|
+
api.set_defaults(func=cmd_api)
|
|
615
|
+
|
|
616
|
+
# deps
|
|
617
|
+
dp = sub.add_parser("deps", help="Show external dependencies (use/extern crate)", parents=[db_parent, output_parent])
|
|
618
|
+
dp.add_argument("--full", action="store_true", help="Show all use paths (not just crate summary)")
|
|
619
|
+
dp.set_defaults(func=cmd_deps)
|
|
620
|
+
|
|
621
|
+
# graph
|
|
622
|
+
g = sub.add_parser("graph", help="Render a call graph (execution flow)", parents=[db_parent, output_parent])
|
|
623
|
+
g.add_argument("--root", help="Function/method name to center on (substring match)")
|
|
624
|
+
g.add_argument("--depth", type=int, default=2, help="BFS depth from --root (default 2)")
|
|
625
|
+
g.add_argument("--direction", choices=["callees", "callers", "both"], default="both",
|
|
626
|
+
help="With --root: callees, callers, or both (default both)")
|
|
627
|
+
g.add_argument("--kind", help="Whole-graph only: comma-separated caller kinds")
|
|
628
|
+
g.add_argument("--no-unresolved", action="store_true",
|
|
629
|
+
help="Omit calls that couldn't be resolved")
|
|
630
|
+
g.add_argument("--format", choices=["svg", "png", "pdf", "dot", "html"], default="svg",
|
|
631
|
+
help="Output format (default svg)")
|
|
632
|
+
g.add_argument("-o", "--output", default="callgraph.svg", help="Output file path")
|
|
633
|
+
g.set_defaults(func=cmd_graph)
|
|
634
|
+
|
|
635
|
+
# serve
|
|
636
|
+
sv = sub.add_parser("serve", help="Start the web frontend server", parents=[db_parent])
|
|
637
|
+
sv.add_argument("--host", default="127.0.0.1", help="Bind host (default: 127.0.0.1)")
|
|
638
|
+
sv.add_argument("--port", type=int, default=8000, help="Bind port (default: 8000)")
|
|
639
|
+
sv.set_defaults(func=cmd_serve)
|
|
640
|
+
|
|
641
|
+
# mcp
|
|
642
|
+
mc = sub.add_parser("mcp", help="Start the MCP server (Model Context Protocol)", parents=[db_parent])
|
|
643
|
+
mc.set_defaults(func=cmd_mcp)
|
|
644
|
+
|
|
645
|
+
return p
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def main(argv: list[str] | None = None) -> int:
|
|
649
|
+
parser = build_parser()
|
|
650
|
+
args = parser.parse_args(argv)
|
|
651
|
+
setup_logging(verbose=getattr(args, "verbose", False))
|
|
652
|
+
try:
|
|
653
|
+
return args.func(args)
|
|
654
|
+
except AnalysisError as e:
|
|
655
|
+
log.error("%s", e)
|
|
656
|
+
return 1
|
|
657
|
+
except KeyboardInterrupt:
|
|
658
|
+
log.warning("Interrupted.")
|
|
659
|
+
return 130
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
if __name__ == "__main__":
|
|
663
|
+
sys.exit(main())
|