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.
Files changed (68) hide show
  1. aja_codeintel-0.1.0.dist-info/METADATA +436 -0
  2. aja_codeintel-0.1.0.dist-info/RECORD +68 -0
  3. aja_codeintel-0.1.0.dist-info/WHEEL +5 -0
  4. aja_codeintel-0.1.0.dist-info/entry_points.txt +3 -0
  5. aja_codeintel-0.1.0.dist-info/licenses/LICENSE +21 -0
  6. aja_codeintel-0.1.0.dist-info/top_level.txt +1 -0
  7. codeintel_cli/__init__.py +1 -0
  8. codeintel_cli/__main__.py +4 -0
  9. codeintel_cli/cli.py +41 -0
  10. codeintel_cli/commands/__init__.py +1 -0
  11. codeintel_cli/commands/graph/__init__.py +18 -0
  12. codeintel_cli/commands/graph/deps_cmd.py +35 -0
  13. codeintel_cli/commands/graph/related_cmd.py +121 -0
  14. codeintel_cli/commands/graph/relsymbols_cmd.py +347 -0
  15. codeintel_cli/commands/graph/reverse_related_cmd.py +54 -0
  16. codeintel_cli/commands/nav/__init__.py +12 -0
  17. codeintel_cli/commands/nav/copy_cmd.py +101 -0
  18. codeintel_cli/commands/nav/open_cmd.py +18 -0
  19. codeintel_cli/commands/nav/where_cmd.py +21 -0
  20. codeintel_cli/commands/project/__init__.py +26 -0
  21. codeintel_cli/commands/project/context_cmd.py +326 -0
  22. codeintel_cli/commands/project/folder_cmd.py +51 -0
  23. codeintel_cli/commands/project/imports_cmd.py +90 -0
  24. codeintel_cli/commands/project/models_cmd.py +98 -0
  25. codeintel_cli/commands/project/modeltree_cmd.py +476 -0
  26. codeintel_cli/commands/project/new.py +0 -0
  27. codeintel_cli/commands/project/resolve_cmd.py +29 -0
  28. codeintel_cli/commands/project/scan_cmd.py +51 -0
  29. codeintel_cli/commands/project/servicemap_cmd.py +180 -0
  30. codeintel_cli/commands/project/tree_cmd.py +203 -0
  31. codeintel_cli/commands/project/version_cmd.py +14 -0
  32. codeintel_cli/context/java_context.py +180 -0
  33. codeintel_cli/context/java_rel.py +299 -0
  34. codeintel_cli/context/java_service.py +291 -0
  35. codeintel_cli/context/python_context.py +91 -0
  36. codeintel_cli/context/python_rel.py +251 -0
  37. codeintel_cli/context/python_service.py +205 -0
  38. codeintel_cli/core/fuzzy.py +72 -0
  39. codeintel_cli/core/opener.py +37 -0
  40. codeintel_cli/core/project.py +34 -0
  41. codeintel_cli/core/resolve_folder.py +68 -0
  42. codeintel_cli/core/resolve_model_target.py +92 -0
  43. codeintel_cli/core/resolve_target.py +53 -0
  44. codeintel_cli/core/timing.py +13 -0
  45. codeintel_cli/core/where.py +77 -0
  46. codeintel_cli/db/__init__.py +7 -0
  47. codeintel_cli/db/cache.py +224 -0
  48. codeintel_cli/db/operations.py +333 -0
  49. codeintel_cli/db/schema.py +102 -0
  50. codeintel_cli/errors.py +78 -0
  51. codeintel_cli/graph/__init__.py +1 -0
  52. codeintel_cli/graph/builder.py +149 -0
  53. codeintel_cli/graph/query.py +30 -0
  54. codeintel_cli/graph/traverse.py +49 -0
  55. codeintel_cli/lang/__init__.py +0 -0
  56. codeintel_cli/lang/java/__init__.py +0 -0
  57. codeintel_cli/lang/java/engine.py +18 -0
  58. codeintel_cli/lang/java/models.py +105 -0
  59. codeintel_cli/lang/java/resolve.py +49 -0
  60. codeintel_cli/lang/python/__init__.py +0 -0
  61. codeintel_cli/lang/python/engine.py +8 -0
  62. codeintel_cli/lang/python/models.py +86 -0
  63. codeintel_cli/lang/router.py +24 -0
  64. codeintel_cli/parser/imports.py +26 -0
  65. codeintel_cli/parser/resolve.py +49 -0
  66. codeintel_cli/parser/symbols.py +92 -0
  67. codeintel_cli/scanner/__init__.py +0 -0
  68. 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"
@@ -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