aja-codeintel 0.1.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.
- aja_codeintel-0.1.0.dist-info/METADATA +436 -0
- aja_codeintel-0.1.0.dist-info/RECORD +68 -0
- aja_codeintel-0.1.0.dist-info/WHEEL +5 -0
- aja_codeintel-0.1.0.dist-info/entry_points.txt +3 -0
- aja_codeintel-0.1.0.dist-info/licenses/LICENSE +21 -0
- aja_codeintel-0.1.0.dist-info/top_level.txt +1 -0
- codeintel_cli/__init__.py +1 -0
- codeintel_cli/__main__.py +4 -0
- codeintel_cli/cli.py +41 -0
- codeintel_cli/commands/__init__.py +1 -0
- codeintel_cli/commands/graph/__init__.py +18 -0
- codeintel_cli/commands/graph/deps_cmd.py +35 -0
- codeintel_cli/commands/graph/related_cmd.py +121 -0
- codeintel_cli/commands/graph/relsymbols_cmd.py +347 -0
- codeintel_cli/commands/graph/reverse_related_cmd.py +54 -0
- codeintel_cli/commands/nav/__init__.py +12 -0
- codeintel_cli/commands/nav/copy_cmd.py +101 -0
- codeintel_cli/commands/nav/open_cmd.py +18 -0
- codeintel_cli/commands/nav/where_cmd.py +21 -0
- codeintel_cli/commands/project/__init__.py +26 -0
- codeintel_cli/commands/project/context_cmd.py +326 -0
- codeintel_cli/commands/project/folder_cmd.py +51 -0
- codeintel_cli/commands/project/imports_cmd.py +90 -0
- codeintel_cli/commands/project/models_cmd.py +98 -0
- codeintel_cli/commands/project/modeltree_cmd.py +476 -0
- codeintel_cli/commands/project/new.py +0 -0
- codeintel_cli/commands/project/resolve_cmd.py +29 -0
- codeintel_cli/commands/project/scan_cmd.py +51 -0
- codeintel_cli/commands/project/servicemap_cmd.py +180 -0
- codeintel_cli/commands/project/tree_cmd.py +203 -0
- codeintel_cli/commands/project/version_cmd.py +14 -0
- codeintel_cli/context/java_context.py +180 -0
- codeintel_cli/context/java_rel.py +299 -0
- codeintel_cli/context/java_service.py +291 -0
- codeintel_cli/context/python_context.py +91 -0
- codeintel_cli/context/python_rel.py +251 -0
- codeintel_cli/context/python_service.py +205 -0
- codeintel_cli/core/fuzzy.py +72 -0
- codeintel_cli/core/opener.py +37 -0
- codeintel_cli/core/project.py +34 -0
- codeintel_cli/core/resolve_folder.py +68 -0
- codeintel_cli/core/resolve_model_target.py +92 -0
- codeintel_cli/core/resolve_target.py +53 -0
- codeintel_cli/core/timing.py +13 -0
- codeintel_cli/core/where.py +77 -0
- codeintel_cli/db/__init__.py +7 -0
- codeintel_cli/db/cache.py +224 -0
- codeintel_cli/db/operations.py +333 -0
- codeintel_cli/db/schema.py +102 -0
- codeintel_cli/errors.py +78 -0
- codeintel_cli/graph/__init__.py +1 -0
- codeintel_cli/graph/builder.py +149 -0
- codeintel_cli/graph/query.py +30 -0
- codeintel_cli/graph/traverse.py +49 -0
- codeintel_cli/lang/__init__.py +0 -0
- codeintel_cli/lang/java/__init__.py +0 -0
- codeintel_cli/lang/java/engine.py +18 -0
- codeintel_cli/lang/java/models.py +105 -0
- codeintel_cli/lang/java/resolve.py +49 -0
- codeintel_cli/lang/python/__init__.py +0 -0
- codeintel_cli/lang/python/engine.py +8 -0
- codeintel_cli/lang/python/models.py +86 -0
- codeintel_cli/lang/router.py +24 -0
- codeintel_cli/parser/imports.py +26 -0
- codeintel_cli/parser/resolve.py +49 -0
- codeintel_cli/parser/symbols.py +92 -0
- codeintel_cli/scanner/__init__.py +0 -0
- codeintel_cli/scanner/scanner.py +41 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import sqlite3
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _hash_content(content: str) -> str:
|
|
11
|
+
return hashlib.md5(content.encode("utf-8")).hexdigest()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_file_modified_time(path: Path) -> float:
|
|
15
|
+
try:
|
|
16
|
+
return path.stat().st_mtime
|
|
17
|
+
except Exception:
|
|
18
|
+
return 0.0
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def is_file_cached(conn: sqlite3.Connection, path: Path) -> bool:
|
|
22
|
+
cursor = conn.execute("SELECT modified_time FROM files WHERE path = ?", (str(path.resolve()),))
|
|
23
|
+
row = cursor.fetchone()
|
|
24
|
+
if not row:
|
|
25
|
+
return False
|
|
26
|
+
cached_mtime = float(row[0])
|
|
27
|
+
current_mtime = float(get_file_modified_time(path))
|
|
28
|
+
return abs(cached_mtime - current_mtime) < 0.01
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_file_id(conn: sqlite3.Connection, path: Path) -> int | None:
|
|
32
|
+
cursor = conn.execute("SELECT id FROM files WHERE path = ?", (str(path.resolve()),))
|
|
33
|
+
row = cursor.fetchone()
|
|
34
|
+
return int(row[0]) if row else None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def upsert_file(conn: sqlite3.Connection, path: Path, rel_path: str, language: str, content: str = "") -> int:
|
|
38
|
+
path_str = str(path.resolve())
|
|
39
|
+
file_id = get_file_id(conn, Path(path_str))
|
|
40
|
+
modified_time = get_file_modified_time(Path(path_str))
|
|
41
|
+
scanned_at = datetime.now().timestamp()
|
|
42
|
+
content_hash = _hash_content(content) if content else None
|
|
43
|
+
|
|
44
|
+
if file_id:
|
|
45
|
+
conn.execute(
|
|
46
|
+
"UPDATE files SET rel_path = ?, language = ?, modified_time = ?, scanned_at = ?, content_hash = ? WHERE id = ?",
|
|
47
|
+
(rel_path, language, modified_time, scanned_at, content_hash, file_id),
|
|
48
|
+
)
|
|
49
|
+
return file_id
|
|
50
|
+
|
|
51
|
+
cur = conn.execute(
|
|
52
|
+
"INSERT INTO files (path, rel_path, language, modified_time, scanned_at, content_hash) VALUES (?, ?, ?, ?, ?, ?)",
|
|
53
|
+
(path_str, rel_path, language, modified_time, scanned_at, content_hash),
|
|
54
|
+
)
|
|
55
|
+
return int(cur.lastrowid)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def clear_file_data(conn: sqlite3.Connection, file_id: int) -> None:
|
|
59
|
+
conn.execute("DELETE FROM imports WHERE file_id = ?", (file_id,))
|
|
60
|
+
conn.execute("DELETE FROM symbols WHERE file_id = ?", (file_id,))
|
|
61
|
+
cur = conn.execute("SELECT id FROM models WHERE file_id = ?", (file_id,))
|
|
62
|
+
mids = [int(r[0]) for r in cur.fetchall()]
|
|
63
|
+
for mid in mids:
|
|
64
|
+
conn.execute("DELETE FROM model_fields WHERE model_id = ?", (mid,))
|
|
65
|
+
conn.execute("DELETE FROM relationships WHERE model_id = ?", (mid,))
|
|
66
|
+
conn.execute("DELETE FROM models WHERE file_id = ?", (file_id,))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def insert_import(conn: sqlite3.Connection, file_id: int, import_path: str, resolved_file_id: int | None = None) -> None:
|
|
70
|
+
conn.execute(
|
|
71
|
+
"INSERT INTO imports (file_id, import_path, resolved_file_id) VALUES (?, ?, ?)",
|
|
72
|
+
(file_id, import_path, resolved_file_id),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def update_import_resolved(conn: sqlite3.Connection, import_row_id: int, resolved_file_id: int) -> None:
|
|
77
|
+
conn.execute("UPDATE imports SET resolved_file_id = ? WHERE id = ?", (resolved_file_id, import_row_id))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def insert_symbol(conn: sqlite3.Connection, file_id: int, name: str, symbol_type: str, line_number: int | None = None) -> None:
|
|
81
|
+
conn.execute(
|
|
82
|
+
"INSERT INTO symbols (file_id, name, type, line_number) VALUES (?, ?, ?, ?)",
|
|
83
|
+
(file_id, name, symbol_type, line_number),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def insert_model(
|
|
88
|
+
conn: sqlite3.Connection,
|
|
89
|
+
file_id: int,
|
|
90
|
+
model_name: str,
|
|
91
|
+
fields: list[tuple[str, str, bool]],
|
|
92
|
+
relationships: list[dict[str, Any]],
|
|
93
|
+
) -> int:
|
|
94
|
+
cur = conn.execute("INSERT INTO models (file_id, name) VALUES (?, ?)", (file_id, model_name))
|
|
95
|
+
model_id = int(cur.lastrowid)
|
|
96
|
+
|
|
97
|
+
for field_name, field_type, is_pk in fields:
|
|
98
|
+
conn.execute(
|
|
99
|
+
"INSERT INTO model_fields (model_id, name, type, is_primary_key) VALUES (?, ?, ?, ?)",
|
|
100
|
+
(model_id, field_name, field_type, 1 if is_pk else 0),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
for rel in relationships:
|
|
104
|
+
conn.execute(
|
|
105
|
+
"INSERT INTO relationships (model_id, kind, target, field) VALUES (?, ?, ?, ?)",
|
|
106
|
+
(model_id, str(rel.get("kind", "")), str(rel.get("target", "")), str(rel.get("field", ""))),
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
return model_id
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def get_all_files(conn: sqlite3.Connection) -> list[Path]:
|
|
113
|
+
cur = conn.execute("SELECT path FROM files ORDER BY path")
|
|
114
|
+
return [Path(r[0]) for r in cur.fetchall()]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def get_file_imports(conn: sqlite3.Connection, file_id: int) -> list[str]:
|
|
118
|
+
cur = conn.execute("SELECT import_path FROM imports WHERE file_id = ? ORDER BY id", (file_id,))
|
|
119
|
+
return [str(r[0]) for r in cur.fetchall()]
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def get_file_symbols(conn: sqlite3.Connection, file_id: int) -> list[dict]:
|
|
123
|
+
cur = conn.execute(
|
|
124
|
+
"SELECT name, type, line_number FROM symbols WHERE file_id = ? ORDER BY COALESCE(line_number, 1000000)",
|
|
125
|
+
(file_id,),
|
|
126
|
+
)
|
|
127
|
+
return [{"name": r[0], "type": r[1], "line": r[2]} for r in cur.fetchall()]
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def get_models(conn: sqlite3.Connection) -> list[dict]:
|
|
131
|
+
cur = conn.execute(
|
|
132
|
+
"SELECT m.id, m.name, f.path FROM models m JOIN files f ON m.file_id = f.id ORDER BY m.name"
|
|
133
|
+
)
|
|
134
|
+
out: list[dict] = []
|
|
135
|
+
for model_id, name, path in cur.fetchall():
|
|
136
|
+
fcur = conn.execute("SELECT name, type, is_primary_key FROM model_fields WHERE model_id = ?", (model_id,))
|
|
137
|
+
fields = [(r[0], r[1], bool(r[2])) for r in fcur.fetchall()]
|
|
138
|
+
out.append({"name": name, "path": Path(path), "fields": fields})
|
|
139
|
+
return out
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def needs_rescan(conn: sqlite3.Connection, project_root: Path) -> bool:
|
|
143
|
+
from ..scanner.scanner import find_all_supported_files
|
|
144
|
+
|
|
145
|
+
current_files = {str(p.resolve()) for p in find_all_supported_files(project_root)}
|
|
146
|
+
cur = conn.execute("SELECT path FROM files")
|
|
147
|
+
cached_files = {str(r[0]) for r in cur.fetchall()}
|
|
148
|
+
if current_files != cached_files:
|
|
149
|
+
return True
|
|
150
|
+
|
|
151
|
+
cur = conn.execute("SELECT path, modified_time FROM files")
|
|
152
|
+
for path_str, cached_mtime in cur.fetchall():
|
|
153
|
+
p = Path(path_str)
|
|
154
|
+
if not p.exists():
|
|
155
|
+
return True
|
|
156
|
+
now_mtime = get_file_modified_time(p)
|
|
157
|
+
if abs(float(now_mtime) - float(cached_mtime)) > 0.01:
|
|
158
|
+
return True
|
|
159
|
+
|
|
160
|
+
return False
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def get_imports_map_for_paths(conn: sqlite3.Connection, paths: list[Path]) -> dict[str, list[str]]:
|
|
164
|
+
if not paths:
|
|
165
|
+
return {}
|
|
166
|
+
|
|
167
|
+
path_strs = [str(p.resolve()) for p in paths]
|
|
168
|
+
CHUNK = 800
|
|
169
|
+
out: dict[str, list[str]] = {}
|
|
170
|
+
|
|
171
|
+
for i in range(0, len(path_strs), CHUNK):
|
|
172
|
+
chunk = path_strs[i : i + CHUNK]
|
|
173
|
+
placeholders = ",".join("?" for _ in chunk)
|
|
174
|
+
rows = conn.execute(
|
|
175
|
+
f"""
|
|
176
|
+
SELECT f.path, imp.import_path
|
|
177
|
+
FROM imports imp
|
|
178
|
+
JOIN files f ON f.id = imp.file_id
|
|
179
|
+
WHERE f.path IN ({placeholders})
|
|
180
|
+
ORDER BY imp.id
|
|
181
|
+
""",
|
|
182
|
+
chunk,
|
|
183
|
+
).fetchall()
|
|
184
|
+
|
|
185
|
+
for pstr, imp in rows:
|
|
186
|
+
out.setdefault(str(pstr), []).append(str(imp))
|
|
187
|
+
|
|
188
|
+
for pstr in chunk:
|
|
189
|
+
out.setdefault(pstr, out.get(pstr, []))
|
|
190
|
+
|
|
191
|
+
return out
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def get_deps_map_for_paths(conn: sqlite3.Connection, paths: list[Path]) -> dict[str, list[str]]:
|
|
195
|
+
if not paths:
|
|
196
|
+
return {}
|
|
197
|
+
|
|
198
|
+
path_strs = [str(p.resolve()) for p in paths]
|
|
199
|
+
CHUNK = 600
|
|
200
|
+
out: dict[str, list[str]] = {}
|
|
201
|
+
|
|
202
|
+
for i in range(0, len(path_strs), CHUNK):
|
|
203
|
+
chunk = path_strs[i : i + CHUNK]
|
|
204
|
+
placeholders = ",".join("?" for _ in chunk)
|
|
205
|
+
rows = conn.execute(
|
|
206
|
+
f"""
|
|
207
|
+
SELECT f1.path, f2.path
|
|
208
|
+
FROM imports i
|
|
209
|
+
JOIN files f1 ON f1.id = i.file_id
|
|
210
|
+
JOIN files f2 ON f2.id = i.resolved_file_id
|
|
211
|
+
WHERE f1.path IN ({placeholders})
|
|
212
|
+
ORDER BY f1.path, f2.path
|
|
213
|
+
""",
|
|
214
|
+
chunk,
|
|
215
|
+
).fetchall()
|
|
216
|
+
|
|
217
|
+
for src, dep in rows:
|
|
218
|
+
out.setdefault(str(src), []).append(str(dep))
|
|
219
|
+
|
|
220
|
+
for src in chunk:
|
|
221
|
+
out.setdefault(src, out.get(src, []))
|
|
222
|
+
|
|
223
|
+
return out
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def get_all_file_id_map(conn: sqlite3.Connection) -> dict[str, int]:
|
|
227
|
+
cur = conn.execute("SELECT id, path FROM files")
|
|
228
|
+
return {str(path): int(fid) for fid, path in cur.fetchall()}
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def get_unresolved_import_rows(conn: sqlite3.Connection) -> list[tuple[int, int, str]]:
|
|
232
|
+
cur = conn.execute(
|
|
233
|
+
"SELECT id, file_id, import_path FROM imports WHERE resolved_file_id IS NULL ORDER BY id"
|
|
234
|
+
)
|
|
235
|
+
return [(int(r[0]), int(r[1]), str(r[2])) for r in cur.fetchall()]
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def get_file_path_and_lang(conn: sqlite3.Connection, file_id: int) -> tuple[str, str] | None:
|
|
239
|
+
cur = conn.execute("SELECT path, language FROM files WHERE id = ?", (file_id,))
|
|
240
|
+
row = cur.fetchone()
|
|
241
|
+
if not row:
|
|
242
|
+
return None
|
|
243
|
+
return str(row[0]), str(row[1])
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def get_deps_for_path(conn: sqlite3.Connection, path: Path) -> list[Path]:
|
|
247
|
+
cur = conn.execute(
|
|
248
|
+
"""
|
|
249
|
+
SELECT f2.path
|
|
250
|
+
FROM imports i
|
|
251
|
+
JOIN files f1 ON f1.id = i.file_id
|
|
252
|
+
JOIN files f2 ON f2.id = i.resolved_file_id
|
|
253
|
+
WHERE f1.path = ?
|
|
254
|
+
ORDER BY f2.path
|
|
255
|
+
""",
|
|
256
|
+
(str(path.resolve()),),
|
|
257
|
+
)
|
|
258
|
+
return [Path(r[0]) for r in cur.fetchall()]
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def get_model_by_file_path(conn: sqlite3.Connection, path: Path) -> tuple[int, str] | None:
|
|
262
|
+
cur = conn.execute(
|
|
263
|
+
"""
|
|
264
|
+
SELECT m.id, m.name
|
|
265
|
+
FROM models m
|
|
266
|
+
JOIN files f ON f.id = m.file_id
|
|
267
|
+
WHERE f.path = ?
|
|
268
|
+
""",
|
|
269
|
+
(str(path.resolve()),),
|
|
270
|
+
)
|
|
271
|
+
row = cur.fetchone()
|
|
272
|
+
if not row:
|
|
273
|
+
return None
|
|
274
|
+
return int(row[0]), str(row[1])
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def get_model_fields(conn: sqlite3.Connection, model_id: int) -> list[tuple[str, str, bool]]:
|
|
278
|
+
cur = conn.execute(
|
|
279
|
+
"SELECT name, type, is_primary_key FROM model_fields WHERE model_id = ? ORDER BY id",
|
|
280
|
+
(model_id,),
|
|
281
|
+
)
|
|
282
|
+
return [(str(n), str(t), bool(pk)) for n, t, pk in cur.fetchall()]
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def get_model_relationships(conn: sqlite3.Connection, model_id: int) -> list[dict[str, str]]:
|
|
286
|
+
cur = conn.execute(
|
|
287
|
+
"SELECT kind, target, field FROM relationships WHERE model_id = ? ORDER BY id",
|
|
288
|
+
(model_id,),
|
|
289
|
+
)
|
|
290
|
+
return [{"kind": str(k), "target": str(t), "field": str(f)} for k, t, f in cur.fetchall()]
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def get_fields_by_model_names(conn: sqlite3.Connection, names: list[str]) -> dict[str, list[tuple[str, str, bool]]]:
|
|
294
|
+
if not names:
|
|
295
|
+
return {}
|
|
296
|
+
CHUNK = 400
|
|
297
|
+
out: dict[str, list[tuple[str, str, bool]]] = {}
|
|
298
|
+
|
|
299
|
+
for i in range(0, len(names), CHUNK):
|
|
300
|
+
chunk = names[i : i + CHUNK]
|
|
301
|
+
placeholders = ",".join("?" for _ in chunk)
|
|
302
|
+
rows = conn.execute(
|
|
303
|
+
f"""
|
|
304
|
+
SELECT m.name, mf.name, mf.type, mf.is_primary_key
|
|
305
|
+
FROM model_fields mf
|
|
306
|
+
JOIN models m ON m.id = mf.model_id
|
|
307
|
+
WHERE m.name IN ({placeholders})
|
|
308
|
+
ORDER BY m.name, mf.id
|
|
309
|
+
""",
|
|
310
|
+
chunk,
|
|
311
|
+
).fetchall()
|
|
312
|
+
|
|
313
|
+
for mname, fname, ftype, pk in rows:
|
|
314
|
+
out.setdefault(str(mname), []).append((str(fname), str(ftype), bool(pk)))
|
|
315
|
+
|
|
316
|
+
for n in chunk:
|
|
317
|
+
out.setdefault(n, out.get(n, []))
|
|
318
|
+
|
|
319
|
+
return out
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def get_reverse_relationships(conn: sqlite3.Connection, target_model_name: str) -> list[dict[str, str]]:
|
|
323
|
+
cur = conn.execute(
|
|
324
|
+
"""
|
|
325
|
+
SELECT m.name, r.kind, r.field
|
|
326
|
+
FROM relationships r
|
|
327
|
+
JOIN models m ON m.id = r.model_id
|
|
328
|
+
WHERE r.target = ?
|
|
329
|
+
ORDER BY m.name, r.kind, r.field
|
|
330
|
+
""",
|
|
331
|
+
(target_model_name,),
|
|
332
|
+
)
|
|
333
|
+
return [{"source": str(src), "kind": str(kind), "field": str(field)} for src, kind, field in cur.fetchall()]
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import sqlite3
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
SCHEMA_VERSION = 2
|
|
7
|
+
|
|
8
|
+
SCHEMA_SQL = """
|
|
9
|
+
CREATE TABLE IF NOT EXISTS metadata (
|
|
10
|
+
key TEXT PRIMARY KEY,
|
|
11
|
+
value TEXT NOT NULL
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
CREATE TABLE IF NOT EXISTS files (
|
|
15
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
16
|
+
path TEXT UNIQUE NOT NULL,
|
|
17
|
+
rel_path TEXT NOT NULL,
|
|
18
|
+
language TEXT NOT NULL,
|
|
19
|
+
modified_time REAL NOT NULL,
|
|
20
|
+
scanned_at REAL NOT NULL,
|
|
21
|
+
content_hash TEXT
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
CREATE TABLE IF NOT EXISTS imports (
|
|
25
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
26
|
+
file_id INTEGER NOT NULL,
|
|
27
|
+
import_path TEXT NOT NULL,
|
|
28
|
+
import_level INTEGER NOT NULL DEFAULT 0,
|
|
29
|
+
resolved_file_id INTEGER,
|
|
30
|
+
FOREIGN KEY (file_id) REFERENCES files(id) ON DELETE CASCADE,
|
|
31
|
+
FOREIGN KEY (resolved_file_id) REFERENCES files(id) ON DELETE SET NULL
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
CREATE TABLE IF NOT EXISTS symbols (
|
|
35
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
36
|
+
file_id INTEGER NOT NULL,
|
|
37
|
+
name TEXT NOT NULL,
|
|
38
|
+
type TEXT NOT NULL,
|
|
39
|
+
line_number INTEGER,
|
|
40
|
+
FOREIGN KEY (file_id) REFERENCES files(id) ON DELETE CASCADE
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
CREATE TABLE IF NOT EXISTS models (
|
|
44
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
45
|
+
file_id INTEGER NOT NULL,
|
|
46
|
+
name TEXT NOT NULL,
|
|
47
|
+
FOREIGN KEY (file_id) REFERENCES files(id) ON DELETE CASCADE
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
CREATE TABLE IF NOT EXISTS model_fields (
|
|
51
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
52
|
+
model_id INTEGER NOT NULL,
|
|
53
|
+
name TEXT NOT NULL,
|
|
54
|
+
type TEXT NOT NULL,
|
|
55
|
+
is_primary_key INTEGER DEFAULT 0,
|
|
56
|
+
FOREIGN KEY (model_id) REFERENCES models(id) ON DELETE CASCADE
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
CREATE TABLE IF NOT EXISTS relationships (
|
|
60
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
61
|
+
model_id INTEGER NOT NULL,
|
|
62
|
+
kind TEXT NOT NULL,
|
|
63
|
+
target TEXT NOT NULL,
|
|
64
|
+
field TEXT NOT NULL,
|
|
65
|
+
FOREIGN KEY (model_id) REFERENCES models(id) ON DELETE CASCADE
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
CREATE INDEX IF NOT EXISTS idx_files_path ON files(path);
|
|
69
|
+
CREATE INDEX IF NOT EXISTS idx_files_rel_path ON files(rel_path);
|
|
70
|
+
CREATE INDEX IF NOT EXISTS idx_files_language ON files(language);
|
|
71
|
+
CREATE INDEX IF NOT EXISTS idx_imports_file_id ON imports(file_id);
|
|
72
|
+
CREATE INDEX IF NOT EXISTS idx_imports_resolved ON imports(resolved_file_id);
|
|
73
|
+
CREATE INDEX IF NOT EXISTS idx_symbols_file_id ON symbols(file_id);
|
|
74
|
+
CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name);
|
|
75
|
+
CREATE INDEX IF NOT EXISTS idx_models_file_id ON models(file_id);
|
|
76
|
+
CREATE INDEX IF NOT EXISTS idx_model_fields_model_id ON model_fields(model_id);
|
|
77
|
+
CREATE INDEX IF NOT EXISTS idx_relationships_model_id ON relationships(model_id);
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def init_db(db_path: Path) -> sqlite3.Connection:
|
|
81
|
+
db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
82
|
+
conn = sqlite3.connect(str(db_path))
|
|
83
|
+
|
|
84
|
+
conn.execute("PRAGMA foreign_keys = ON")
|
|
85
|
+
conn.execute("PRAGMA journal_mode = WAL")
|
|
86
|
+
conn.execute("PRAGMA synchronous = NORMAL")
|
|
87
|
+
conn.execute("PRAGMA temp_store = MEMORY")
|
|
88
|
+
conn.execute("PRAGMA cache_size = -20000")
|
|
89
|
+
conn.execute("PRAGMA busy_timeout = 3000")
|
|
90
|
+
|
|
91
|
+
conn.executescript(SCHEMA_SQL)
|
|
92
|
+
|
|
93
|
+
row = conn.execute("SELECT value FROM metadata WHERE key = 'schema_version'").fetchone()
|
|
94
|
+
if row is None:
|
|
95
|
+
conn.execute("INSERT INTO metadata (key, value) VALUES ('schema_version', ?)", (str(SCHEMA_VERSION),))
|
|
96
|
+
conn.execute("INSERT INTO metadata (key, value) VALUES ('created_at', ?)", (datetime.now().isoformat(),))
|
|
97
|
+
conn.commit()
|
|
98
|
+
|
|
99
|
+
return conn
|
|
100
|
+
|
|
101
|
+
def get_db_path(project_root: Path) -> Path:
|
|
102
|
+
return project_root / ".codeintel" / "index.db"
|
codeintel_cli/errors.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class CodeIntelError(Exception):
|
|
10
|
+
"""
|
|
11
|
+
Base exception for all expected (user-facing) errors.
|
|
12
|
+
The CLI catches these and prints clean messages.
|
|
13
|
+
"""
|
|
14
|
+
message: str
|
|
15
|
+
hint: Optional[str] = None
|
|
16
|
+
exit_code: int = 1
|
|
17
|
+
|
|
18
|
+
def __str__(self) -> str:
|
|
19
|
+
if self.hint:
|
|
20
|
+
return f"{self.message}\nHint: {self.hint}"
|
|
21
|
+
return self.message
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class InvalidPathError(CodeIntelError):
|
|
26
|
+
path: Path = Path(".")
|
|
27
|
+
exit_code: int = 2
|
|
28
|
+
|
|
29
|
+
def __post_init__(self) -> None:
|
|
30
|
+
if not self.message:
|
|
31
|
+
self.message = f"Path must be an existing folder: {self.path}"
|
|
32
|
+
self.hint = "Pass a directory path like '.' or 'src/'."
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class PermissionDeniedError(CodeIntelError):
|
|
37
|
+
path: Path = Path(".")
|
|
38
|
+
exit_code: int = 3
|
|
39
|
+
|
|
40
|
+
def __post_init__(self) -> None:
|
|
41
|
+
if not self.message:
|
|
42
|
+
self.message = f"Permission denied while reading: {self.path}"
|
|
43
|
+
self.hint = "Check folder permissions or run terminal with appropriate access."
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class ScanFailedError(CodeIntelError):
|
|
48
|
+
root: Path = Path(".")
|
|
49
|
+
exit_code: int = 4
|
|
50
|
+
|
|
51
|
+
def __post_init__(self) -> None:
|
|
52
|
+
if not self.message:
|
|
53
|
+
self.message = f"Scan failed under: {self.root}"
|
|
54
|
+
self.hint = "Try scanning a smaller folder or enable --verbose for details."
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class NoPythonFilesFoundError(CodeIntelError):
|
|
59
|
+
root: Path = Path(".")
|
|
60
|
+
exit_code: int = 5
|
|
61
|
+
|
|
62
|
+
def __post_init__(self) -> None:
|
|
63
|
+
if not self.message:
|
|
64
|
+
self.message = f"No Python files found under: {self.root}"
|
|
65
|
+
self.hint = "Confirm you are scanning the correct folder."
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def wrap_unexpected_error(err: Exception, context: str = "") -> CodeIntelError:
|
|
69
|
+
"""
|
|
70
|
+
Convert unknown exceptions into a user-facing CodeIntelError.
|
|
71
|
+
Use in CLI as a last-resort catch.
|
|
72
|
+
"""
|
|
73
|
+
prefix = f"{context}: " if context else ""
|
|
74
|
+
return CodeIntelError(
|
|
75
|
+
message=f"{prefix}{err.__class__.__name__}: {err}",
|
|
76
|
+
hint="Re-run with --verbose. If it persists, share the command + folder path.",
|
|
77
|
+
exit_code=1,
|
|
78
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__all__ = []
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from ..lang.router import extract_imports
|
|
7
|
+
from ..lang.java.resolve import resolve_java_import_to_file
|
|
8
|
+
from ..parser.resolve import resolve_import_to_file
|
|
9
|
+
from ..core.where import file_to_module
|
|
10
|
+
from ..db.cache import CacheManager
|
|
11
|
+
from ..db.operations import get_deps_map_for_paths
|
|
12
|
+
|
|
13
|
+
def _is_under_root(path: Path, root: Path) -> bool:
|
|
14
|
+
try:
|
|
15
|
+
path = path.resolve()
|
|
16
|
+
root = root.resolve()
|
|
17
|
+
except Exception:
|
|
18
|
+
return False
|
|
19
|
+
try:
|
|
20
|
+
path.relative_to(root)
|
|
21
|
+
return True
|
|
22
|
+
except Exception:
|
|
23
|
+
return False
|
|
24
|
+
|
|
25
|
+
def _is_stdlib_module(module: str) -> bool:
|
|
26
|
+
if not module:
|
|
27
|
+
return False
|
|
28
|
+
top = module.split(".", 1)[0]
|
|
29
|
+
stdlib_names = getattr(sys, "stdlib_module_names", None)
|
|
30
|
+
if stdlib_names and top in stdlib_names:
|
|
31
|
+
return True
|
|
32
|
+
if top in {"__future__", "builtins"}:
|
|
33
|
+
return True
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
def _detect_lang(path: Path) -> str:
|
|
37
|
+
return "java" if path.suffix == ".java" else "python"
|
|
38
|
+
|
|
39
|
+
def build_graph(files: list[Path], root: Path) -> dict[Path, set[Path]]:
|
|
40
|
+
graph, _ = build_graph_with_counts(files, root)
|
|
41
|
+
return graph
|
|
42
|
+
|
|
43
|
+
def build_graph_with_counts(
|
|
44
|
+
files: list[Path],
|
|
45
|
+
root: Path,
|
|
46
|
+
*,
|
|
47
|
+
ignore_stdlib: bool = True,
|
|
48
|
+
ignore_outside_root: bool = True,
|
|
49
|
+
use_sqlite_cache: bool = False,
|
|
50
|
+
) -> tuple[dict[Path, set[Path]], dict[Path, int]]:
|
|
51
|
+
root = root.resolve()
|
|
52
|
+
graph: dict[Path, set[Path]] = {}
|
|
53
|
+
dependents_count: dict[Path, int] = {}
|
|
54
|
+
|
|
55
|
+
java_map: dict[str, Path] = {}
|
|
56
|
+
for f0 in files:
|
|
57
|
+
f0r = f0.resolve()
|
|
58
|
+
if f0r.suffix.lower() == ".java":
|
|
59
|
+
m = file_to_module(f0r, root)
|
|
60
|
+
if m:
|
|
61
|
+
java_map[m] = f0r
|
|
62
|
+
|
|
63
|
+
cached_deps_by_path: dict[str, list[str]] | None = None
|
|
64
|
+
if use_sqlite_cache:
|
|
65
|
+
try:
|
|
66
|
+
with CacheManager(root) as cache:
|
|
67
|
+
if cache.needs_rescan():
|
|
68
|
+
cache.scan_project(verbose=False)
|
|
69
|
+
cached_deps_by_path = get_deps_map_for_paths(cache.conn, files) if cache.conn else None
|
|
70
|
+
except Exception:
|
|
71
|
+
cached_deps_by_path = None
|
|
72
|
+
|
|
73
|
+
for f in files:
|
|
74
|
+
f = f.resolve()
|
|
75
|
+
lang = _detect_lang(f)
|
|
76
|
+
|
|
77
|
+
deps: set[Path] = set()
|
|
78
|
+
|
|
79
|
+
if cached_deps_by_path is not None:
|
|
80
|
+
for dst in cached_deps_by_path.get(str(f), []):
|
|
81
|
+
t = Path(dst)
|
|
82
|
+
try:
|
|
83
|
+
t = t.resolve()
|
|
84
|
+
except Exception:
|
|
85
|
+
continue
|
|
86
|
+
if ignore_outside_root and not _is_under_root(t, root):
|
|
87
|
+
continue
|
|
88
|
+
if t == f:
|
|
89
|
+
continue
|
|
90
|
+
if t not in deps:
|
|
91
|
+
deps.add(t)
|
|
92
|
+
dependents_count[t] = dependents_count.get(t, 0) + 1
|
|
93
|
+
|
|
94
|
+
graph[f] = deps
|
|
95
|
+
continue
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
imports = extract_imports(f, lang)
|
|
99
|
+
except Exception:
|
|
100
|
+
imports = []
|
|
101
|
+
|
|
102
|
+
for item in imports:
|
|
103
|
+
if isinstance(item, tuple):
|
|
104
|
+
mod, level = item
|
|
105
|
+
else:
|
|
106
|
+
mod, level = str(item), 0
|
|
107
|
+
|
|
108
|
+
if lang == "python":
|
|
109
|
+
if ignore_stdlib and _is_stdlib_module(mod):
|
|
110
|
+
continue
|
|
111
|
+
target = resolve_import_to_file(mod, level, f, root)
|
|
112
|
+
else:
|
|
113
|
+
if not mod or mod.endswith(".*"):
|
|
114
|
+
continue
|
|
115
|
+
target = resolve_java_import_to_file(mod, root)
|
|
116
|
+
if not target:
|
|
117
|
+
target = java_map.get(mod)
|
|
118
|
+
|
|
119
|
+
if not target:
|
|
120
|
+
continue
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
target = target.resolve()
|
|
124
|
+
except Exception:
|
|
125
|
+
continue
|
|
126
|
+
|
|
127
|
+
if ignore_outside_root and not _is_under_root(target, root):
|
|
128
|
+
continue
|
|
129
|
+
|
|
130
|
+
if target == f:
|
|
131
|
+
continue
|
|
132
|
+
|
|
133
|
+
if target not in deps:
|
|
134
|
+
deps.add(target)
|
|
135
|
+
dependents_count[target] = dependents_count.get(target, 0) + 1
|
|
136
|
+
|
|
137
|
+
graph[f] = deps
|
|
138
|
+
|
|
139
|
+
return graph, dependents_count
|
|
140
|
+
|
|
141
|
+
def get_hub_files_by_ratio(
|
|
142
|
+
dependents_count: dict[Path, int],
|
|
143
|
+
total_files: int,
|
|
144
|
+
ratio: float,
|
|
145
|
+
) -> set[Path]:
|
|
146
|
+
if total_files <= 0 or ratio <= 0:
|
|
147
|
+
return set()
|
|
148
|
+
limit = total_files * ratio
|
|
149
|
+
return {p for p, c in dependents_count.items() if c > limit}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from .traverse import build_reverse_graph, bfs_related
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_direct_deps(graph: dict[Path, set[Path]], file: Path) -> set[Path]:
|
|
8
|
+
return graph.get(file, set())
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_reverse_deps(graph: dict[Path, set[Path]], file: Path) -> set[Path]:
|
|
12
|
+
rev = build_reverse_graph(graph)
|
|
13
|
+
return rev.get(file, set())
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_related(
|
|
17
|
+
graph: dict[Path, set[Path]],
|
|
18
|
+
file: Path,
|
|
19
|
+
depth: int,
|
|
20
|
+
include_reverse: bool = True,
|
|
21
|
+
hubs: set[Path] | None = None,
|
|
22
|
+
) -> set[Path]:
|
|
23
|
+
out = set()
|
|
24
|
+
out |= bfs_related(graph, file, depth, skip=hubs)
|
|
25
|
+
|
|
26
|
+
if include_reverse:
|
|
27
|
+
rev = build_reverse_graph(graph)
|
|
28
|
+
out |= bfs_related(rev, file, depth, skip=hubs)
|
|
29
|
+
|
|
30
|
+
return out
|