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/db.py
ADDED
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
"""SQLite storage layer for extracted Rust code items.
|
|
2
|
+
|
|
3
|
+
Schema version 2: adds use_declarations, extern_crates, generic_params,
|
|
4
|
+
lifetime_params, and complexity_metrics tables for deeper analysis.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import sqlite3
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Optional
|
|
12
|
+
|
|
13
|
+
from .exceptions import DatabaseError
|
|
14
|
+
from .logging import get_logger
|
|
15
|
+
|
|
16
|
+
log = get_logger("db")
|
|
17
|
+
|
|
18
|
+
SCHEMA_VERSION = 2
|
|
19
|
+
|
|
20
|
+
SCHEMA = """
|
|
21
|
+
-- Files table
|
|
22
|
+
CREATE TABLE IF NOT EXISTS files (
|
|
23
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
24
|
+
path TEXT UNIQUE NOT NULL,
|
|
25
|
+
mtime REAL,
|
|
26
|
+
hash TEXT,
|
|
27
|
+
total_lines INTEGER DEFAULT 0,
|
|
28
|
+
scanned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
-- Items table (structs, enums, functions, methods, etc.)
|
|
32
|
+
CREATE TABLE IF NOT EXISTS items (
|
|
33
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
34
|
+
file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
|
|
35
|
+
kind TEXT NOT NULL,
|
|
36
|
+
name TEXT NOT NULL,
|
|
37
|
+
target TEXT,
|
|
38
|
+
trait_name TEXT,
|
|
39
|
+
visibility TEXT,
|
|
40
|
+
signature TEXT,
|
|
41
|
+
doc TEXT,
|
|
42
|
+
attributes TEXT,
|
|
43
|
+
start_line INTEGER NOT NULL,
|
|
44
|
+
end_line INTEGER NOT NULL,
|
|
45
|
+
source TEXT NOT NULL,
|
|
46
|
+
parent_id INTEGER REFERENCES items(id) ON DELETE CASCADE,
|
|
47
|
+
-- new analysis columns
|
|
48
|
+
is_pub INTEGER DEFAULT 0,
|
|
49
|
+
is_const_fn INTEGER DEFAULT 0,
|
|
50
|
+
is_async INTEGER DEFAULT 0,
|
|
51
|
+
is_unsafe INTEGER DEFAULT 0,
|
|
52
|
+
cyclomatic_complexity INTEGER DEFAULT 1,
|
|
53
|
+
cognitive_complexity INTEGER DEFAULT 0,
|
|
54
|
+
nesting_depth INTEGER DEFAULT 0,
|
|
55
|
+
num_branches INTEGER DEFAULT 0,
|
|
56
|
+
num_function_calls INTEGER DEFAULT 0,
|
|
57
|
+
lines_of_code INTEGER DEFAULT 0
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
CREATE INDEX IF NOT EXISTS idx_items_name ON items(name);
|
|
61
|
+
CREATE INDEX IF NOT EXISTS idx_items_kind ON items(kind);
|
|
62
|
+
CREATE INDEX IF NOT EXISTS idx_items_target ON items(target);
|
|
63
|
+
CREATE INDEX IF NOT EXISTS idx_items_file ON items(file_id);
|
|
64
|
+
CREATE INDEX IF NOT EXISTS idx_items_parent ON items(parent_id);
|
|
65
|
+
|
|
66
|
+
-- Call edges (function/method call resolution)
|
|
67
|
+
CREATE TABLE IF NOT EXISTS calls (
|
|
68
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
69
|
+
caller_id INTEGER NOT NULL REFERENCES items(id) ON DELETE CASCADE,
|
|
70
|
+
callee_name TEXT NOT NULL,
|
|
71
|
+
receiver TEXT,
|
|
72
|
+
is_method_call INTEGER NOT NULL,
|
|
73
|
+
line INTEGER,
|
|
74
|
+
callee_id INTEGER REFERENCES items(id) ON DELETE SET NULL
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
CREATE INDEX IF NOT EXISTS idx_calls_caller ON calls(caller_id);
|
|
78
|
+
CREATE INDEX IF NOT EXISTS idx_calls_callee ON calls(callee_id);
|
|
79
|
+
CREATE INDEX IF NOT EXISTS idx_calls_callee_name ON calls(callee_name);
|
|
80
|
+
|
|
81
|
+
-- Use declarations
|
|
82
|
+
CREATE TABLE IF NOT EXISTS use_declarations (
|
|
83
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
84
|
+
file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
|
|
85
|
+
path TEXT NOT NULL,
|
|
86
|
+
alias TEXT,
|
|
87
|
+
is_glob INTEGER DEFAULT 0,
|
|
88
|
+
start_line INTEGER NOT NULL,
|
|
89
|
+
end_line INTEGER NOT NULL
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
CREATE INDEX IF NOT EXISTS idx_uses_file ON use_declarations(file_id);
|
|
93
|
+
CREATE INDEX IF NOT EXISTS idx_uses_path ON use_declarations(path);
|
|
94
|
+
|
|
95
|
+
-- Extern crate declarations
|
|
96
|
+
CREATE TABLE IF NOT EXISTS extern_crates (
|
|
97
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
98
|
+
file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
|
|
99
|
+
name TEXT NOT NULL,
|
|
100
|
+
alias TEXT,
|
|
101
|
+
start_line INTEGER NOT NULL,
|
|
102
|
+
end_line INTEGER NOT NULL
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
CREATE INDEX IF NOT EXISTS idx_extern_file ON extern_crates(file_id);
|
|
106
|
+
|
|
107
|
+
-- Generic type parameters
|
|
108
|
+
CREATE TABLE IF NOT EXISTS generic_params (
|
|
109
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
110
|
+
item_id INTEGER NOT NULL REFERENCES items(id) ON DELETE CASCADE,
|
|
111
|
+
name TEXT NOT NULL,
|
|
112
|
+
bounds TEXT,
|
|
113
|
+
default_val TEXT,
|
|
114
|
+
kind TEXT DEFAULT 'type' -- 'type' or 'const'
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
CREATE INDEX IF NOT EXISTS idx_generics_item ON generic_params(item_id);
|
|
118
|
+
|
|
119
|
+
-- Lifetime parameters
|
|
120
|
+
CREATE TABLE IF NOT EXISTS lifetime_params (
|
|
121
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
122
|
+
item_id INTEGER NOT NULL REFERENCES items(id) ON DELETE CASCADE,
|
|
123
|
+
name TEXT NOT NULL,
|
|
124
|
+
bounds TEXT
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
CREATE INDEX IF NOT EXISTS idx_lifetimes_item ON lifetime_params(item_id);
|
|
128
|
+
|
|
129
|
+
-- FTS5 index for full-text search
|
|
130
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS items_fts USING fts5(
|
|
131
|
+
name, target, signature, doc, source, content='items', content_rowid='id'
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
CREATE TRIGGER IF NOT EXISTS items_ai AFTER INSERT ON items BEGIN
|
|
135
|
+
INSERT INTO items_fts(rowid, name, target, signature, doc, source)
|
|
136
|
+
VALUES (new.id, new.name, new.target, new.signature, new.doc, new.source);
|
|
137
|
+
END;
|
|
138
|
+
|
|
139
|
+
CREATE TRIGGER IF NOT EXISTS items_ad AFTER DELETE ON items BEGIN
|
|
140
|
+
INSERT INTO items_fts(items_fts, rowid, name, target, signature, doc, source)
|
|
141
|
+
VALUES ('delete', old.id, old.name, old.target, old.signature, old.doc, old.source);
|
|
142
|
+
END;
|
|
143
|
+
|
|
144
|
+
CREATE TRIGGER IF NOT EXISTS items_au AFTER UPDATE ON items BEGIN
|
|
145
|
+
INSERT INTO items_fts(items_fts, rowid, name, target, signature, doc, source)
|
|
146
|
+
VALUES ('delete', old.id, old.name, old.target, old.signature, old.doc, old.source);
|
|
147
|
+
INSERT INTO items_fts(rowid, name, target, signature, doc, source)
|
|
148
|
+
VALUES (new.id, new.name, new.target, new.signature, new.doc, new.source);
|
|
149
|
+
END;
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class RustCodeDB:
|
|
154
|
+
"""SQLite-backed storage for Rust code analysis data."""
|
|
155
|
+
|
|
156
|
+
def __init__(self, db_path: str) -> None:
|
|
157
|
+
self.db_path = db_path
|
|
158
|
+
try:
|
|
159
|
+
self.conn = sqlite3.connect(db_path)
|
|
160
|
+
except sqlite3.Error as e:
|
|
161
|
+
raise DatabaseError(f"Cannot open database {db_path}: {e}") from e
|
|
162
|
+
self.conn.row_factory = sqlite3.Row
|
|
163
|
+
self.conn.execute("PRAGMA foreign_keys = ON")
|
|
164
|
+
self.conn.execute("PRAGMA journal_mode = WAL")
|
|
165
|
+
self.conn.executescript(SCHEMA)
|
|
166
|
+
self.conn.commit()
|
|
167
|
+
|
|
168
|
+
def close(self) -> None:
|
|
169
|
+
self.conn.close()
|
|
170
|
+
|
|
171
|
+
def __enter__(self) -> RustCodeDB:
|
|
172
|
+
return self
|
|
173
|
+
|
|
174
|
+
def __exit__(self, *exc: Any) -> None:
|
|
175
|
+
self.close()
|
|
176
|
+
|
|
177
|
+
# ------------------------------------------------------------------
|
|
178
|
+
# File management
|
|
179
|
+
# ------------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
def upsert_file(self, path: str, mtime: float, file_hash: str) -> int:
|
|
182
|
+
cur = self.conn.cursor()
|
|
183
|
+
row = cur.execute("SELECT id, hash FROM files WHERE path = ?", (path,)).fetchone()
|
|
184
|
+
if row is None:
|
|
185
|
+
cur.execute(
|
|
186
|
+
"INSERT INTO files(path, mtime, hash) VALUES (?, ?, ?)",
|
|
187
|
+
(path, mtime, file_hash),
|
|
188
|
+
)
|
|
189
|
+
self.conn.commit()
|
|
190
|
+
return cur.lastrowid
|
|
191
|
+
file_id: int = row["id"]
|
|
192
|
+
if row["hash"] != file_hash:
|
|
193
|
+
cur.execute("DELETE FROM items WHERE file_id = ?", (file_id,))
|
|
194
|
+
cur.execute(
|
|
195
|
+
"UPDATE files SET mtime = ?, hash = ? WHERE id = ?",
|
|
196
|
+
(mtime, file_hash, file_id),
|
|
197
|
+
)
|
|
198
|
+
self.conn.commit()
|
|
199
|
+
return file_id
|
|
200
|
+
|
|
201
|
+
def update_file_lines(self, file_id: int, total_lines: int) -> None:
|
|
202
|
+
self.conn.execute(
|
|
203
|
+
"UPDATE files SET total_lines = ? WHERE id = ?",
|
|
204
|
+
(total_lines, file_id),
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
def file_unchanged(self, path: str, file_hash: str) -> bool:
|
|
208
|
+
row = self.conn.execute(
|
|
209
|
+
"SELECT hash FROM files WHERE path = ?", (path,)
|
|
210
|
+
).fetchone()
|
|
211
|
+
return row is not None and row["hash"] == file_hash
|
|
212
|
+
|
|
213
|
+
# ------------------------------------------------------------------
|
|
214
|
+
# Item management
|
|
215
|
+
# ------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
def insert_item(
|
|
218
|
+
self,
|
|
219
|
+
file_id: int,
|
|
220
|
+
kind: str,
|
|
221
|
+
name: str,
|
|
222
|
+
start_line: int,
|
|
223
|
+
end_line: int,
|
|
224
|
+
source: str,
|
|
225
|
+
*,
|
|
226
|
+
target: Optional[str] = None,
|
|
227
|
+
trait_name: Optional[str] = None,
|
|
228
|
+
visibility: Optional[str] = None,
|
|
229
|
+
signature: Optional[str] = None,
|
|
230
|
+
doc: Optional[str] = None,
|
|
231
|
+
attributes: Optional[str] = None,
|
|
232
|
+
parent_id: Optional[int] = None,
|
|
233
|
+
is_pub: bool = False,
|
|
234
|
+
is_const_fn: bool = False,
|
|
235
|
+
is_async: bool = False,
|
|
236
|
+
is_unsafe: bool = False,
|
|
237
|
+
cyclomatic_complexity: int = 1,
|
|
238
|
+
cognitive_complexity: int = 0,
|
|
239
|
+
nesting_depth: int = 0,
|
|
240
|
+
num_branches: int = 0,
|
|
241
|
+
num_function_calls: int = 0,
|
|
242
|
+
lines_of_code: int = 0,
|
|
243
|
+
) -> int:
|
|
244
|
+
cur = self.conn.cursor()
|
|
245
|
+
cur.execute(
|
|
246
|
+
"""INSERT INTO items
|
|
247
|
+
(file_id, kind, name, target, trait_name, visibility, signature,
|
|
248
|
+
doc, attributes, start_line, end_line, source, parent_id,
|
|
249
|
+
is_pub, is_const_fn, is_async, is_unsafe,
|
|
250
|
+
cyclomatic_complexity, cognitive_complexity, nesting_depth,
|
|
251
|
+
num_branches, num_function_calls, lines_of_code)
|
|
252
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
|
253
|
+
(
|
|
254
|
+
file_id, kind, name, target, trait_name, visibility, signature,
|
|
255
|
+
doc, attributes, start_line, end_line, source, parent_id,
|
|
256
|
+
int(is_pub), int(is_const_fn), int(is_async), int(is_unsafe),
|
|
257
|
+
cyclomatic_complexity, cognitive_complexity, nesting_depth,
|
|
258
|
+
num_branches, num_function_calls, lines_of_code,
|
|
259
|
+
),
|
|
260
|
+
)
|
|
261
|
+
return cur.lastrowid
|
|
262
|
+
|
|
263
|
+
def insert_generic_params(
|
|
264
|
+
self, item_id: int, generics: list[tuple[str, str, Optional[str]]]
|
|
265
|
+
) -> None:
|
|
266
|
+
"""Insert generic parameters. Each tuple: (name, bounds, default)."""
|
|
267
|
+
cur = self.conn.cursor()
|
|
268
|
+
for name, bounds, default in generics:
|
|
269
|
+
cur.execute(
|
|
270
|
+
"INSERT INTO generic_params (item_id, name, bounds, default_val) VALUES (?,?,?,?)",
|
|
271
|
+
(item_id, name, bounds, default),
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
def insert_lifetime_params(
|
|
275
|
+
self, item_id: int, lifetimes: list[tuple[str, str]]
|
|
276
|
+
) -> None:
|
|
277
|
+
"""Insert lifetime parameters. Each tuple: (name, bounds)."""
|
|
278
|
+
cur = self.conn.cursor()
|
|
279
|
+
for name, bounds in lifetimes:
|
|
280
|
+
cur.execute(
|
|
281
|
+
"INSERT INTO lifetime_params (item_id, name, bounds) VALUES (?,?,?)",
|
|
282
|
+
(item_id, name, bounds),
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
def commit(self) -> None:
|
|
286
|
+
self.conn.commit()
|
|
287
|
+
|
|
288
|
+
# ------------------------------------------------------------------
|
|
289
|
+
# Use declarations / extern crates
|
|
290
|
+
# ------------------------------------------------------------------
|
|
291
|
+
|
|
292
|
+
def insert_use_decl(
|
|
293
|
+
self, file_id: int, path: str, alias: Optional[str],
|
|
294
|
+
is_glob: bool, start_line: int, end_line: int,
|
|
295
|
+
) -> int:
|
|
296
|
+
cur = self.conn.cursor()
|
|
297
|
+
cur.execute(
|
|
298
|
+
"INSERT INTO use_declarations (file_id, path, alias, is_glob, start_line, end_line) "
|
|
299
|
+
"VALUES (?,?,?,?,?,?)",
|
|
300
|
+
(file_id, path, alias, int(is_glob), start_line, end_line),
|
|
301
|
+
)
|
|
302
|
+
return cur.lastrowid
|
|
303
|
+
|
|
304
|
+
def insert_extern_crate(
|
|
305
|
+
self, file_id: int, name: str, alias: Optional[str],
|
|
306
|
+
start_line: int, end_line: int,
|
|
307
|
+
) -> int:
|
|
308
|
+
cur = self.conn.cursor()
|
|
309
|
+
cur.execute(
|
|
310
|
+
"INSERT INTO extern_crates (file_id, name, alias, start_line, end_line) "
|
|
311
|
+
"VALUES (?,?,?,?,?)",
|
|
312
|
+
(file_id, name, alias, start_line, end_line),
|
|
313
|
+
)
|
|
314
|
+
return cur.lastrowid
|
|
315
|
+
|
|
316
|
+
def get_use_declarations(self, file_id: Optional[int] = None) -> list[sqlite3.Row]:
|
|
317
|
+
if file_id is not None:
|
|
318
|
+
return self.conn.execute(
|
|
319
|
+
"SELECT u.*, f.path AS file_path FROM use_declarations u "
|
|
320
|
+
"JOIN files f ON u.file_id = f.id WHERE u.file_id = ? ORDER BY u.start_line",
|
|
321
|
+
(file_id,),
|
|
322
|
+
).fetchall()
|
|
323
|
+
return self.conn.execute(
|
|
324
|
+
"SELECT u.*, f.path AS file_path FROM use_declarations u "
|
|
325
|
+
"JOIN files f ON u.file_id = f.id ORDER BY f.path, u.start_line"
|
|
326
|
+
).fetchall()
|
|
327
|
+
|
|
328
|
+
def get_extern_crates(self, file_id: Optional[int] = None) -> list[sqlite3.Row]:
|
|
329
|
+
if file_id is not None:
|
|
330
|
+
return self.conn.execute(
|
|
331
|
+
"SELECT e.*, f.path AS file_path FROM extern_crates e "
|
|
332
|
+
"JOIN files f ON e.file_id = f.id WHERE e.file_id = ? ORDER BY e.start_line",
|
|
333
|
+
(file_id,),
|
|
334
|
+
).fetchall()
|
|
335
|
+
return self.conn.execute(
|
|
336
|
+
"SELECT e.*, f.path AS file_path FROM extern_crates e "
|
|
337
|
+
"JOIN files f ON e.file_id = f.id ORDER BY f.path, e.start_line"
|
|
338
|
+
).fetchall()
|
|
339
|
+
|
|
340
|
+
def all_extern_crates(self) -> list[sqlite3.Row]:
|
|
341
|
+
return self.conn.execute(
|
|
342
|
+
"SELECT DISTINCT name, alias FROM extern_crates ORDER BY name"
|
|
343
|
+
).fetchall()
|
|
344
|
+
|
|
345
|
+
# ------------------------------------------------------------------
|
|
346
|
+
# Call graph
|
|
347
|
+
# ------------------------------------------------------------------
|
|
348
|
+
|
|
349
|
+
def clear_calls_for_file(self, file_id: int) -> None:
|
|
350
|
+
self.conn.execute(
|
|
351
|
+
"DELETE FROM calls WHERE caller_id IN (SELECT id FROM items WHERE file_id = ?)",
|
|
352
|
+
(file_id,),
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
def insert_call(
|
|
356
|
+
self,
|
|
357
|
+
caller_id: int,
|
|
358
|
+
callee_name: str,
|
|
359
|
+
line: int,
|
|
360
|
+
receiver: Optional[str] = None,
|
|
361
|
+
is_method_call: bool = False,
|
|
362
|
+
) -> int:
|
|
363
|
+
cur = self.conn.cursor()
|
|
364
|
+
cur.execute(
|
|
365
|
+
"INSERT INTO calls (caller_id, callee_name, receiver, is_method_call, line) "
|
|
366
|
+
"VALUES (?, ?, ?, ?, ?)",
|
|
367
|
+
(caller_id, callee_name, receiver, int(is_method_call), line),
|
|
368
|
+
)
|
|
369
|
+
return cur.lastrowid
|
|
370
|
+
|
|
371
|
+
def resolve_calls(self) -> tuple[int, int]:
|
|
372
|
+
"""Best-effort static resolution of call edges.
|
|
373
|
+
|
|
374
|
+
Returns (total, resolved) counts.
|
|
375
|
+
"""
|
|
376
|
+
cur = self.conn.cursor()
|
|
377
|
+
calls = cur.execute(
|
|
378
|
+
"""SELECT calls.id, calls.callee_name, calls.receiver, calls.is_method_call,
|
|
379
|
+
items.target AS caller_target
|
|
380
|
+
FROM calls JOIN items ON calls.caller_id = items.id
|
|
381
|
+
WHERE calls.callee_id IS NULL"""
|
|
382
|
+
).fetchall()
|
|
383
|
+
|
|
384
|
+
resolved = 0
|
|
385
|
+
for call in calls:
|
|
386
|
+
callee_id = None
|
|
387
|
+
if call["is_method_call"]:
|
|
388
|
+
if call["receiver"] == "self" and call["caller_target"]:
|
|
389
|
+
row = cur.execute(
|
|
390
|
+
"SELECT id FROM items "
|
|
391
|
+
"WHERE kind IN ('method') AND name = ? AND target = ? LIMIT 1",
|
|
392
|
+
(call["callee_name"], call["caller_target"]),
|
|
393
|
+
).fetchone()
|
|
394
|
+
if row:
|
|
395
|
+
callee_id = row["id"]
|
|
396
|
+
if callee_id is None:
|
|
397
|
+
row = cur.execute(
|
|
398
|
+
"SELECT id FROM items WHERE kind = 'method' AND name = ? LIMIT 1",
|
|
399
|
+
(call["callee_name"],),
|
|
400
|
+
).fetchone()
|
|
401
|
+
if row:
|
|
402
|
+
callee_id = row["id"]
|
|
403
|
+
else:
|
|
404
|
+
if call["receiver"]:
|
|
405
|
+
short_receiver = call["receiver"].split("::")[-1]
|
|
406
|
+
row = cur.execute(
|
|
407
|
+
"SELECT id FROM items "
|
|
408
|
+
"WHERE kind = 'method' AND name = ? AND target = ? LIMIT 1",
|
|
409
|
+
(call["callee_name"], short_receiver),
|
|
410
|
+
).fetchone()
|
|
411
|
+
if row:
|
|
412
|
+
callee_id = row["id"]
|
|
413
|
+
if callee_id is None:
|
|
414
|
+
row = cur.execute(
|
|
415
|
+
"SELECT id FROM items WHERE kind = 'function' AND name = ? LIMIT 1",
|
|
416
|
+
(call["callee_name"],),
|
|
417
|
+
).fetchone()
|
|
418
|
+
if row:
|
|
419
|
+
callee_id = row["id"]
|
|
420
|
+
if callee_id is not None:
|
|
421
|
+
cur.execute("UPDATE calls SET callee_id = ? WHERE id = ?", (callee_id, call["id"]))
|
|
422
|
+
resolved += 1
|
|
423
|
+
self.conn.commit()
|
|
424
|
+
total = len(calls) + self.conn.execute(
|
|
425
|
+
"SELECT COUNT(*) AS n FROM calls WHERE callee_id IS NOT NULL"
|
|
426
|
+
).fetchone()["n"]
|
|
427
|
+
return total, resolved
|
|
428
|
+
|
|
429
|
+
def get_calls_from(self, item_id: int) -> list[sqlite3.Row]:
|
|
430
|
+
return self.conn.execute(
|
|
431
|
+
"""SELECT calls.*, callee.name AS callee_resolved_name, callee.kind AS callee_kind,
|
|
432
|
+
callee.target AS callee_target
|
|
433
|
+
FROM calls LEFT JOIN items AS callee ON calls.callee_id = callee.id
|
|
434
|
+
WHERE calls.caller_id = ?""",
|
|
435
|
+
(item_id,),
|
|
436
|
+
).fetchall()
|
|
437
|
+
|
|
438
|
+
def get_calls_to(self, item_id: int) -> list[sqlite3.Row]:
|
|
439
|
+
return self.conn.execute(
|
|
440
|
+
"""SELECT calls.*, caller.name AS caller_name, caller.kind AS caller_kind,
|
|
441
|
+
caller.target AS caller_target
|
|
442
|
+
FROM calls JOIN items AS caller ON calls.caller_id = caller.id
|
|
443
|
+
WHERE calls.callee_id = ?""",
|
|
444
|
+
(item_id,),
|
|
445
|
+
).fetchall()
|
|
446
|
+
|
|
447
|
+
def find_callable(self, name: str, target: Optional[str] = None) -> list[sqlite3.Row]:
|
|
448
|
+
q = "SELECT * FROM items WHERE kind IN ('function','method') AND name = ?"
|
|
449
|
+
params: list[Any] = [name]
|
|
450
|
+
if target:
|
|
451
|
+
q += " AND (target = ? OR target IS NULL)"
|
|
452
|
+
params.append(target)
|
|
453
|
+
return self.conn.execute(q, params).fetchall()
|
|
454
|
+
|
|
455
|
+
def all_call_edges(self) -> list[sqlite3.Row]:
|
|
456
|
+
return self.conn.execute(
|
|
457
|
+
"""SELECT calls.id, calls.caller_id, caller.name AS caller_name,
|
|
458
|
+
caller.target AS caller_target, caller.kind AS caller_kind,
|
|
459
|
+
calls.callee_id, calls.callee_name,
|
|
460
|
+
callee.target AS callee_target, callee.kind AS callee_kind,
|
|
461
|
+
calls.is_method_call, calls.receiver
|
|
462
|
+
FROM calls
|
|
463
|
+
JOIN items AS caller ON calls.caller_id = caller.id
|
|
464
|
+
LEFT JOIN items AS callee ON calls.callee_id = callee.id"""
|
|
465
|
+
).fetchall()
|
|
466
|
+
|
|
467
|
+
def call_graph_stats(self) -> tuple[int, int]:
|
|
468
|
+
total = self.conn.execute("SELECT COUNT(*) AS n FROM calls").fetchone()["n"]
|
|
469
|
+
resolved = self.conn.execute(
|
|
470
|
+
"SELECT COUNT(*) AS n FROM calls WHERE callee_id IS NOT NULL"
|
|
471
|
+
).fetchone()["n"]
|
|
472
|
+
return total, resolved
|
|
473
|
+
|
|
474
|
+
# ------------------------------------------------------------------
|
|
475
|
+
# Queries
|
|
476
|
+
# ------------------------------------------------------------------
|
|
477
|
+
|
|
478
|
+
def list_items(
|
|
479
|
+
self,
|
|
480
|
+
kind: Optional[str] = None,
|
|
481
|
+
name: Optional[str] = None,
|
|
482
|
+
target: Optional[str] = None,
|
|
483
|
+
file_like: Optional[str] = None,
|
|
484
|
+
limit: int = 200,
|
|
485
|
+
) -> list[sqlite3.Row]:
|
|
486
|
+
q = """SELECT items.*, files.path AS file_path
|
|
487
|
+
FROM items JOIN files ON items.file_id = files.id
|
|
488
|
+
WHERE 1=1"""
|
|
489
|
+
params: list[Any] = []
|
|
490
|
+
if kind:
|
|
491
|
+
q += " AND items.kind = ?"
|
|
492
|
+
params.append(kind)
|
|
493
|
+
if name:
|
|
494
|
+
q += " AND items.name LIKE ?"
|
|
495
|
+
params.append(f"%{name}%")
|
|
496
|
+
if target:
|
|
497
|
+
q += " AND items.target LIKE ?"
|
|
498
|
+
params.append(f"%{target}%")
|
|
499
|
+
if file_like:
|
|
500
|
+
q += " AND files.path LIKE ?"
|
|
501
|
+
params.append(f"%{file_like}%")
|
|
502
|
+
q += " ORDER BY files.path, items.start_line LIMIT ?"
|
|
503
|
+
params.append(limit)
|
|
504
|
+
return self.conn.execute(q, params).fetchall()
|
|
505
|
+
|
|
506
|
+
def get_item(self, item_id: int) -> Optional[sqlite3.Row]:
|
|
507
|
+
return self.conn.execute(
|
|
508
|
+
"""SELECT items.*, files.path AS file_path
|
|
509
|
+
FROM items JOIN files ON items.file_id = files.id
|
|
510
|
+
WHERE items.id = ?""",
|
|
511
|
+
(item_id,),
|
|
512
|
+
).fetchone()
|
|
513
|
+
|
|
514
|
+
def get_methods_of(self, target: str) -> list[sqlite3.Row]:
|
|
515
|
+
return self.conn.execute(
|
|
516
|
+
"""SELECT items.*, files.path AS file_path
|
|
517
|
+
FROM items JOIN files ON items.file_id = files.id
|
|
518
|
+
WHERE items.kind = 'method' AND items.target LIKE ?
|
|
519
|
+
ORDER BY files.path, items.start_line""",
|
|
520
|
+
(f"%{target}%",),
|
|
521
|
+
).fetchall()
|
|
522
|
+
|
|
523
|
+
def search(self, query: str, kind: Optional[str] = None, limit: int = 100) -> list[sqlite3.Row]:
|
|
524
|
+
q = """SELECT items.*, files.path AS file_path
|
|
525
|
+
FROM items_fts
|
|
526
|
+
JOIN items ON items.id = items_fts.rowid
|
|
527
|
+
JOIN files ON items.file_id = files.id
|
|
528
|
+
WHERE items_fts MATCH ?"""
|
|
529
|
+
params: list[Any] = [query]
|
|
530
|
+
if kind:
|
|
531
|
+
q += " AND items.kind = ?"
|
|
532
|
+
params.append(kind)
|
|
533
|
+
q += " ORDER BY rank LIMIT ?"
|
|
534
|
+
params.append(limit)
|
|
535
|
+
return self.conn.execute(q, params).fetchall()
|
|
536
|
+
|
|
537
|
+
def stats(self) -> tuple[int, list[sqlite3.Row]]:
|
|
538
|
+
rows = self.conn.execute(
|
|
539
|
+
"SELECT kind, COUNT(*) as n FROM items GROUP BY kind ORDER BY n DESC"
|
|
540
|
+
).fetchall()
|
|
541
|
+
n_files = self.conn.execute("SELECT COUNT(*) AS n FROM files").fetchone()["n"]
|
|
542
|
+
return n_files, rows
|
|
543
|
+
|
|
544
|
+
def complexity_report(self, min_complexity: int = 5) -> list[sqlite3.Row]:
|
|
545
|
+
"""Return functions/methods with cyclomatic complexity >= threshold."""
|
|
546
|
+
return self.conn.execute(
|
|
547
|
+
"""SELECT items.*, files.path AS file_path
|
|
548
|
+
FROM items JOIN files ON items.file_id = files.id
|
|
549
|
+
WHERE items.kind IN ('function', 'method')
|
|
550
|
+
AND items.cyclomatic_complexity >= ?
|
|
551
|
+
ORDER BY items.cyclomatic_complexity DESC""",
|
|
552
|
+
(min_complexity,),
|
|
553
|
+
).fetchall()
|
|
554
|
+
|
|
555
|
+
def api_surface(self) -> list[sqlite3.Row]:
|
|
556
|
+
"""Return all public items (the public API surface)."""
|
|
557
|
+
return self.conn.execute(
|
|
558
|
+
"""SELECT items.*, files.path AS file_path
|
|
559
|
+
FROM items JOIN files ON items.file_id = files.id
|
|
560
|
+
WHERE items.is_pub = 1
|
|
561
|
+
ORDER BY items.kind, items.name"""
|
|
562
|
+
).fetchall()
|
|
563
|
+
|
|
564
|
+
def get_generics(self, item_id: int) -> list[sqlite3.Row]:
|
|
565
|
+
return self.conn.execute(
|
|
566
|
+
"SELECT * FROM generic_params WHERE item_id = ? ORDER BY name",
|
|
567
|
+
(item_id,),
|
|
568
|
+
).fetchall()
|
|
569
|
+
|
|
570
|
+
def get_lifetimes(self, item_id: int) -> list[sqlite3.Row]:
|
|
571
|
+
return self.conn.execute(
|
|
572
|
+
"SELECT * FROM lifetime_params WHERE item_id = ? ORDER BY name",
|
|
573
|
+
(item_id,),
|
|
574
|
+
).fetchall()
|
|
575
|
+
|
|
576
|
+
def most_complex_functions(self, limit: int = 20) -> list[sqlite3.Row]:
|
|
577
|
+
return self.conn.execute(
|
|
578
|
+
"""SELECT items.*, files.path AS file_path
|
|
579
|
+
FROM items JOIN files ON items.file_id = files.id
|
|
580
|
+
WHERE items.kind IN ('function', 'method')
|
|
581
|
+
ORDER BY items.cyclomatic_complexity DESC
|
|
582
|
+
LIMIT ?""",
|
|
583
|
+
(limit,),
|
|
584
|
+
).fetchall()
|
|
585
|
+
|
|
586
|
+
def largest_files(self, limit: int = 20) -> list[sqlite3.Row]:
|
|
587
|
+
return self.conn.execute(
|
|
588
|
+
"""SELECT files.*, COUNT(items.id) AS item_count,
|
|
589
|
+
SUM(items.lines_of_code) AS total_loc
|
|
590
|
+
FROM files
|
|
591
|
+
LEFT JOIN items ON items.file_id = files.id
|
|
592
|
+
GROUP BY files.id
|
|
593
|
+
ORDER BY total_loc DESC
|
|
594
|
+
LIMIT ?""",
|
|
595
|
+
(limit,),
|
|
596
|
+
).fetchall()
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Exception hierarchy for rust-analyzer-db."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AnalysisError(Exception):
|
|
7
|
+
"""Base exception for all analysis errors."""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ParseError(AnalysisError):
|
|
11
|
+
"""Raised when tree-sitter fails to parse a Rust source file."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, file_path: str, reason: str) -> None:
|
|
14
|
+
self.file_path = file_path
|
|
15
|
+
self.reason = reason
|
|
16
|
+
super().__init__(f"Failed to parse {file_path}: {reason}")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DatabaseError(AnalysisError):
|
|
20
|
+
"""Raised on database operation failures."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class QueryError(DatabaseError):
|
|
24
|
+
"""Raised when a database query is malformed or fails."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, query: str, reason: str) -> None:
|
|
27
|
+
self.query = query
|
|
28
|
+
self.reason = reason
|
|
29
|
+
super().__init__(f"Query failed: {reason}")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class GraphError(AnalysisError):
|
|
33
|
+
"""Raised when graph construction or rendering fails."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class RenderError(GraphError):
|
|
37
|
+
"""Raised when Graphviz rendering fails."""
|
|
38
|
+
|
|
39
|
+
def __init__(self, reason: str) -> None:
|
|
40
|
+
self.reason = reason
|
|
41
|
+
super().__init__(f"Render failed: {reason}")
|