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,121 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import deque
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from ...graph.builder import build_graph_with_counts, get_hub_files_by_ratio
|
|
8
|
+
from ...core.resolve_target import resolve_target_file
|
|
9
|
+
from ...db.cache import CacheManager
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _layered_related(
|
|
13
|
+
graph: dict[Path, set[Path]],
|
|
14
|
+
start: Path,
|
|
15
|
+
depth: int,
|
|
16
|
+
include_reverse: bool,
|
|
17
|
+
hubs: set[Path],
|
|
18
|
+
) -> list[list[Path]]:
|
|
19
|
+
adj: dict[Path, set[Path]] = {k: set(v) for k, v in graph.items()}
|
|
20
|
+
|
|
21
|
+
if include_reverse:
|
|
22
|
+
rev: dict[Path, set[Path]] = {}
|
|
23
|
+
for src, deps in adj.items():
|
|
24
|
+
for dst in deps:
|
|
25
|
+
rev.setdefault(dst, set()).add(src)
|
|
26
|
+
for node, incoming in rev.items():
|
|
27
|
+
adj.setdefault(node, set()).update(incoming)
|
|
28
|
+
|
|
29
|
+
layers: list[list[Path]] = []
|
|
30
|
+
visited: set[Path] = {start}
|
|
31
|
+
q: deque[tuple[Path, int]] = deque([(start, 0)])
|
|
32
|
+
by_depth: dict[int, list[Path]] = {}
|
|
33
|
+
|
|
34
|
+
while q:
|
|
35
|
+
node, d = q.popleft()
|
|
36
|
+
if d == depth:
|
|
37
|
+
continue
|
|
38
|
+
|
|
39
|
+
for nxt in adj.get(node, set()):
|
|
40
|
+
if nxt in visited:
|
|
41
|
+
continue
|
|
42
|
+
if nxt in hubs:
|
|
43
|
+
continue
|
|
44
|
+
visited.add(nxt)
|
|
45
|
+
nd = d + 1
|
|
46
|
+
by_depth.setdefault(nd, []).append(nxt)
|
|
47
|
+
q.append((nxt, nd))
|
|
48
|
+
|
|
49
|
+
for d in range(1, depth + 1):
|
|
50
|
+
items = sorted(by_depth.get(d, []))
|
|
51
|
+
layers.append(items)
|
|
52
|
+
|
|
53
|
+
return layers
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def register_related(app: typer.Typer) -> None:
|
|
57
|
+
@app.command(help="Show related files using depth traversal (layered).")
|
|
58
|
+
def related(
|
|
59
|
+
file: str = typer.Argument(...),
|
|
60
|
+
root: str = typer.Argument("."),
|
|
61
|
+
depth: int = typer.Option(2, "--depth", "-d"),
|
|
62
|
+
forward_only: bool = typer.Option(False, "--forward-only"),
|
|
63
|
+
include_hubs: bool = typer.Option(False, "--include-hubs"),
|
|
64
|
+
use_sqlite_cache: bool = typer.Option(True, "--use-sqlite-cache/--no-sqlite-cache"),
|
|
65
|
+
):
|
|
66
|
+
target, root_path = resolve_target_file(file, root=root)
|
|
67
|
+
|
|
68
|
+
if use_sqlite_cache:
|
|
69
|
+
with CacheManager(root_path) as cache:
|
|
70
|
+
if cache.needs_rescan():
|
|
71
|
+
cache.scan_project(verbose=False)
|
|
72
|
+
files = cache.get_cached_files()
|
|
73
|
+
else:
|
|
74
|
+
files = []
|
|
75
|
+
for p in root_path.rglob("*"):
|
|
76
|
+
if p.is_file() and p.suffix.lower() in {".py", ".java"}:
|
|
77
|
+
files.append(p.resolve())
|
|
78
|
+
files = sorted(set(files))
|
|
79
|
+
|
|
80
|
+
graph, dependents_count = build_graph_with_counts(
|
|
81
|
+
files,
|
|
82
|
+
root_path,
|
|
83
|
+
use_sqlite_cache=use_sqlite_cache,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
hubs: set[Path] = set()
|
|
87
|
+
if not include_hubs:
|
|
88
|
+
hubs = get_hub_files_by_ratio(dependents_count, len(files), 0.5)
|
|
89
|
+
|
|
90
|
+
layers = _layered_related(
|
|
91
|
+
graph=graph,
|
|
92
|
+
start=target,
|
|
93
|
+
depth=depth,
|
|
94
|
+
include_reverse=not forward_only,
|
|
95
|
+
hubs=hubs,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
total = sum(len(x) for x in layers)
|
|
99
|
+
if total == 0:
|
|
100
|
+
typer.echo("No related files.")
|
|
101
|
+
raise typer.Exit(0)
|
|
102
|
+
|
|
103
|
+
typer.echo(f"TARGET: {target.relative_to(root_path)}")
|
|
104
|
+
typer.echo(f"DEPTH: {depth} ({'forward-only' if forward_only else 'forward+reverse'})")
|
|
105
|
+
typer.echo(f"FILES: {total}")
|
|
106
|
+
typer.echo("")
|
|
107
|
+
|
|
108
|
+
for i, items in enumerate(layers, 1):
|
|
109
|
+
typer.echo(f"LEVEL {i} ({len(items)})")
|
|
110
|
+
if not items:
|
|
111
|
+
typer.echo(" (none)")
|
|
112
|
+
typer.echo("")
|
|
113
|
+
continue
|
|
114
|
+
|
|
115
|
+
for p in items:
|
|
116
|
+
try:
|
|
117
|
+
typer.echo(f" {p.relative_to(root_path)}")
|
|
118
|
+
except Exception:
|
|
119
|
+
typer.echo(f" {p}")
|
|
120
|
+
|
|
121
|
+
typer.echo("")
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import deque
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import re
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from ...graph.builder import build_graph_with_counts, get_hub_files_by_ratio
|
|
9
|
+
from ...parser.symbols import extract_classes_from_file, extract_funcs_from_file
|
|
10
|
+
from ...core.resolve_target import resolve_target_file
|
|
11
|
+
from ...db.cache import CacheManager
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
_JAVA_TYPE_RE = re.compile(r"\b(class|interface|enum|record)\s+([A-Za-z_][A-Za-z0-9_]*)\b")
|
|
15
|
+
_JAVA_METHOD_RE = re.compile(
|
|
16
|
+
r"^\s*(?:@\w+(?:\([^)]*\))?\s*)*(?:public|protected|private)\s+(?:static\s+)?"
|
|
17
|
+
r"(?:final\s+)?([A-Za-z0-9_<>\[\].,?]+)\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(([^)]*)\)\s*"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _detect_lang(p: Path) -> str:
|
|
22
|
+
return "java" if p.suffix.lower() == ".java" else "python"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _short_path(p: Path, root: Path) -> str:
|
|
26
|
+
try:
|
|
27
|
+
return str(p.relative_to(root))
|
|
28
|
+
except Exception:
|
|
29
|
+
return str(p)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _java_types(path: Path) -> dict[str, list[str]]:
|
|
33
|
+
try:
|
|
34
|
+
text = path.read_text(encoding="utf-8", errors="ignore")
|
|
35
|
+
except Exception:
|
|
36
|
+
return {"class": [], "interface": [], "enum": [], "record": []}
|
|
37
|
+
|
|
38
|
+
out: dict[str, list[str]] = {"class": [], "interface": [], "enum": [], "record": []}
|
|
39
|
+
seen: set[str] = set()
|
|
40
|
+
|
|
41
|
+
for line in text.splitlines():
|
|
42
|
+
m = _JAVA_TYPE_RE.search(line)
|
|
43
|
+
if not m:
|
|
44
|
+
continue
|
|
45
|
+
kind = m.group(1)
|
|
46
|
+
name = m.group(2)
|
|
47
|
+
key = f"{kind}:{name}"
|
|
48
|
+
if key in seen:
|
|
49
|
+
continue
|
|
50
|
+
seen.add(key)
|
|
51
|
+
out.setdefault(kind, []).append(name)
|
|
52
|
+
|
|
53
|
+
return out
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _java_methods(path: Path) -> list[str]:
|
|
57
|
+
try:
|
|
58
|
+
lines = path.read_text(encoding="utf-8", errors="ignore").splitlines()
|
|
59
|
+
except Exception:
|
|
60
|
+
return []
|
|
61
|
+
|
|
62
|
+
out: list[str] = []
|
|
63
|
+
seen: set[str] = set()
|
|
64
|
+
|
|
65
|
+
for line in lines:
|
|
66
|
+
s = line.strip()
|
|
67
|
+
if not s:
|
|
68
|
+
continue
|
|
69
|
+
if s.startswith(("class ", "interface ", "record ", "enum ")):
|
|
70
|
+
continue
|
|
71
|
+
m = _JAVA_METHOD_RE.match(line)
|
|
72
|
+
if not m:
|
|
73
|
+
continue
|
|
74
|
+
ret = m.group(1).strip()
|
|
75
|
+
name = m.group(2).strip()
|
|
76
|
+
args = m.group(3).strip()
|
|
77
|
+
sig = f"{name}({args}) → {ret}"
|
|
78
|
+
if sig not in seen:
|
|
79
|
+
seen.add(sig)
|
|
80
|
+
out.append(sig)
|
|
81
|
+
|
|
82
|
+
return out
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _py_symbols(path: Path) -> tuple[list[str], list[str]]:
|
|
86
|
+
classes: list[str] = []
|
|
87
|
+
funcs: list[str] = []
|
|
88
|
+
|
|
89
|
+
for c in extract_classes_from_file(path):
|
|
90
|
+
if not c.name.startswith("_"):
|
|
91
|
+
classes.append(c.name)
|
|
92
|
+
|
|
93
|
+
for fn in extract_funcs_from_file(path):
|
|
94
|
+
if fn.name.startswith("_") or fn.name.startswith("register_"):
|
|
95
|
+
continue
|
|
96
|
+
funcs.append(fn.signature)
|
|
97
|
+
|
|
98
|
+
return classes, funcs
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _build_layers(
|
|
102
|
+
graph: dict[Path, set[Path]],
|
|
103
|
+
start: Path,
|
|
104
|
+
depth: int,
|
|
105
|
+
*,
|
|
106
|
+
include_reverse: bool,
|
|
107
|
+
hubs: set[Path],
|
|
108
|
+
) -> dict[int, list[Path]]:
|
|
109
|
+
adj: dict[Path, set[Path]] = {k: set(v) for k, v in graph.items()}
|
|
110
|
+
|
|
111
|
+
if include_reverse:
|
|
112
|
+
rev: dict[Path, set[Path]] = {}
|
|
113
|
+
for src, deps in adj.items():
|
|
114
|
+
for dst in deps:
|
|
115
|
+
rev.setdefault(dst, set()).add(src)
|
|
116
|
+
for node, incoming in rev.items():
|
|
117
|
+
adj.setdefault(node, set()).update(incoming)
|
|
118
|
+
|
|
119
|
+
visited: set[Path] = {start}
|
|
120
|
+
q: deque[tuple[Path, int]] = deque([(start, 0)])
|
|
121
|
+
by_depth: dict[int, list[Path]] = {}
|
|
122
|
+
|
|
123
|
+
while q:
|
|
124
|
+
node, d = q.popleft()
|
|
125
|
+
if d >= depth:
|
|
126
|
+
continue
|
|
127
|
+
|
|
128
|
+
for nxt in adj.get(node, set()):
|
|
129
|
+
if nxt in visited:
|
|
130
|
+
continue
|
|
131
|
+
if hubs and nxt in hubs:
|
|
132
|
+
continue
|
|
133
|
+
visited.add(nxt)
|
|
134
|
+
nd = d + 1
|
|
135
|
+
by_depth.setdefault(nd, []).append(nxt)
|
|
136
|
+
q.append((nxt, nd))
|
|
137
|
+
|
|
138
|
+
for k in list(by_depth.keys()):
|
|
139
|
+
by_depth[k] = sorted(set(by_depth[k]))
|
|
140
|
+
|
|
141
|
+
return by_depth
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _print_java_target(path: Path, root: Path, *, all: bool) -> None:
|
|
145
|
+
typer.echo(f"TARGET: {_short_path(path, root)}")
|
|
146
|
+
typer.echo("")
|
|
147
|
+
|
|
148
|
+
types_map = _java_types(path)
|
|
149
|
+
methods = _java_methods(path)
|
|
150
|
+
|
|
151
|
+
if types_map["interface"]:
|
|
152
|
+
typer.echo("INTERFACES")
|
|
153
|
+
for n in types_map["interface"]:
|
|
154
|
+
typer.echo(f" {n}")
|
|
155
|
+
typer.echo("")
|
|
156
|
+
|
|
157
|
+
if all:
|
|
158
|
+
if types_map["class"]:
|
|
159
|
+
typer.echo("CLASSES")
|
|
160
|
+
for n in types_map["class"]:
|
|
161
|
+
typer.echo(f" {n}")
|
|
162
|
+
typer.echo("")
|
|
163
|
+
|
|
164
|
+
if types_map["record"]:
|
|
165
|
+
typer.echo("RECORDS")
|
|
166
|
+
for n in types_map["record"]:
|
|
167
|
+
typer.echo(f" {n}")
|
|
168
|
+
typer.echo("")
|
|
169
|
+
|
|
170
|
+
if types_map["enum"]:
|
|
171
|
+
typer.echo("ENUMS")
|
|
172
|
+
for n in types_map["enum"]:
|
|
173
|
+
typer.echo(f" {n}")
|
|
174
|
+
typer.echo("")
|
|
175
|
+
|
|
176
|
+
if methods:
|
|
177
|
+
typer.echo("METHODS")
|
|
178
|
+
for m in methods:
|
|
179
|
+
typer.echo(f" {m}")
|
|
180
|
+
typer.echo("")
|
|
181
|
+
|
|
182
|
+
if not types_map["interface"] and not (
|
|
183
|
+
all and (types_map["class"] or types_map["enum"] or types_map["record"] or methods)
|
|
184
|
+
):
|
|
185
|
+
typer.echo("(no symbols)")
|
|
186
|
+
typer.echo("")
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _print_python_target(path: Path, root: Path) -> None:
|
|
190
|
+
typer.echo(f"TARGET: {_short_path(path, root)}")
|
|
191
|
+
typer.echo("")
|
|
192
|
+
|
|
193
|
+
classes, funcs = _py_symbols(path)
|
|
194
|
+
|
|
195
|
+
if classes:
|
|
196
|
+
typer.echo("CLASSES")
|
|
197
|
+
for c in classes:
|
|
198
|
+
typer.echo(f" {c}")
|
|
199
|
+
typer.echo("")
|
|
200
|
+
|
|
201
|
+
if funcs:
|
|
202
|
+
typer.echo("FUNCTIONS")
|
|
203
|
+
for f in funcs:
|
|
204
|
+
typer.echo(f" {f}")
|
|
205
|
+
typer.echo("")
|
|
206
|
+
|
|
207
|
+
if not classes and not funcs:
|
|
208
|
+
typer.echo("(no symbols)")
|
|
209
|
+
typer.echo("")
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _print_java_related(path: Path, root: Path, *, all: bool) -> None:
|
|
213
|
+
typer.echo(_short_path(path, root))
|
|
214
|
+
|
|
215
|
+
types_map = _java_types(path)
|
|
216
|
+
|
|
217
|
+
if types_map["interface"]:
|
|
218
|
+
typer.echo(" INTERFACES")
|
|
219
|
+
for n in types_map["interface"]:
|
|
220
|
+
typer.echo(f" {n}")
|
|
221
|
+
|
|
222
|
+
if all:
|
|
223
|
+
if types_map["class"]:
|
|
224
|
+
typer.echo(" CLASSES")
|
|
225
|
+
for n in types_map["class"]:
|
|
226
|
+
typer.echo(f" {n}")
|
|
227
|
+
|
|
228
|
+
if types_map["record"]:
|
|
229
|
+
typer.echo(" RECORDS")
|
|
230
|
+
for n in types_map["record"]:
|
|
231
|
+
typer.echo(f" {n}")
|
|
232
|
+
|
|
233
|
+
if types_map["enum"]:
|
|
234
|
+
typer.echo(" ENUMS")
|
|
235
|
+
for n in types_map["enum"]:
|
|
236
|
+
typer.echo(f" {n}")
|
|
237
|
+
|
|
238
|
+
methods = _java_methods(path)
|
|
239
|
+
if methods:
|
|
240
|
+
typer.echo(" METHODS")
|
|
241
|
+
for m in methods:
|
|
242
|
+
typer.echo(f" {m}")
|
|
243
|
+
|
|
244
|
+
typer.echo("")
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _print_python_related(path: Path, root: Path) -> None:
|
|
248
|
+
typer.echo(_short_path(path, root))
|
|
249
|
+
|
|
250
|
+
classes, funcs = _py_symbols(path)
|
|
251
|
+
|
|
252
|
+
if classes:
|
|
253
|
+
typer.echo(" CLASSES")
|
|
254
|
+
for c in classes:
|
|
255
|
+
typer.echo(f" {c}")
|
|
256
|
+
|
|
257
|
+
if funcs:
|
|
258
|
+
typer.echo(" FUNCTIONS")
|
|
259
|
+
for f in funcs:
|
|
260
|
+
typer.echo(f" {f}")
|
|
261
|
+
|
|
262
|
+
typer.echo("")
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def register_relsymbols(app: typer.Typer) -> None:
|
|
266
|
+
@app.command(help="Show related classes/functions/interfaces")
|
|
267
|
+
def relsymbols(
|
|
268
|
+
file: str = typer.Argument(...),
|
|
269
|
+
root: str = typer.Argument("."),
|
|
270
|
+
depth: int = typer.Option(1, "--depth", "-d"),
|
|
271
|
+
forward_only: bool = typer.Option(False, "--forward-only"),
|
|
272
|
+
include_hubs: bool = typer.Option(False, "--include-hubs"),
|
|
273
|
+
all: bool = typer.Option(False, "--all", help="Show all symbols (Java: classes+methods, Python: unchanged)"),
|
|
274
|
+
use_sqlite_cache: bool = typer.Option(True, "--use-sqlite-cache/--no-sqlite-cache"),
|
|
275
|
+
):
|
|
276
|
+
target, root_path = resolve_target_file(file, root=root)
|
|
277
|
+
|
|
278
|
+
if use_sqlite_cache:
|
|
279
|
+
with CacheManager(root_path) as cache:
|
|
280
|
+
if cache.needs_rescan():
|
|
281
|
+
cache.scan_project(verbose=False)
|
|
282
|
+
files = cache.get_cached_files()
|
|
283
|
+
else:
|
|
284
|
+
files = []
|
|
285
|
+
for p in root_path.rglob("*"):
|
|
286
|
+
if p.is_file() and p.suffix.lower() in {".py", ".java"}:
|
|
287
|
+
files.append(p.resolve())
|
|
288
|
+
files = sorted(set(files))
|
|
289
|
+
|
|
290
|
+
graph, dependents_count = build_graph_with_counts(
|
|
291
|
+
files,
|
|
292
|
+
root_path,
|
|
293
|
+
use_sqlite_cache=use_sqlite_cache,
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
hubs: set[Path] = set()
|
|
297
|
+
if not include_hubs:
|
|
298
|
+
hubs = get_hub_files_by_ratio(dependents_count, len(files), 0.5)
|
|
299
|
+
|
|
300
|
+
depth = max(1, depth)
|
|
301
|
+
|
|
302
|
+
layers = _build_layers(
|
|
303
|
+
graph=graph,
|
|
304
|
+
start=target,
|
|
305
|
+
depth=depth,
|
|
306
|
+
include_reverse=not forward_only,
|
|
307
|
+
hubs=hubs,
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
lang = _detect_lang(target)
|
|
311
|
+
if lang == "java":
|
|
312
|
+
_print_java_target(target, root_path, all=all)
|
|
313
|
+
else:
|
|
314
|
+
_print_python_target(target, root_path)
|
|
315
|
+
|
|
316
|
+
printed_any = False
|
|
317
|
+
|
|
318
|
+
for lvl in sorted(layers.keys()):
|
|
319
|
+
items = layers[lvl]
|
|
320
|
+
if not items:
|
|
321
|
+
continue
|
|
322
|
+
|
|
323
|
+
printed_any = True
|
|
324
|
+
typer.echo(f"LEVEL {lvl}")
|
|
325
|
+
typer.echo("")
|
|
326
|
+
|
|
327
|
+
for item_path in items:
|
|
328
|
+
if item_path.name == "__init__.py":
|
|
329
|
+
continue
|
|
330
|
+
|
|
331
|
+
item_lang = _detect_lang(item_path)
|
|
332
|
+
|
|
333
|
+
if item_lang == "java":
|
|
334
|
+
types_map = _java_types(item_path)
|
|
335
|
+
if not types_map["interface"] and not (
|
|
336
|
+
all and (types_map["class"] or types_map["enum"] or types_map["record"])
|
|
337
|
+
):
|
|
338
|
+
continue
|
|
339
|
+
_print_java_related(item_path, root_path, all=all)
|
|
340
|
+
else:
|
|
341
|
+
classes, funcs = _py_symbols(item_path)
|
|
342
|
+
if not classes and not funcs:
|
|
343
|
+
continue
|
|
344
|
+
_print_python_related(item_path, root_path)
|
|
345
|
+
|
|
346
|
+
if not printed_any:
|
|
347
|
+
typer.echo("(no related symbols)")
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from ...graph.builder import build_graph_with_counts, get_hub_files_by_ratio
|
|
6
|
+
from ...graph.traverse import build_reverse_graph, bfs_related
|
|
7
|
+
from ...core.resolve_target import resolve_target_file
|
|
8
|
+
from ...db.cache import CacheManager
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def register_rrelated(app: typer.Typer) -> None:
|
|
12
|
+
@app.command(help="Show reverse-related files (who depends on this) using depth traversal.")
|
|
13
|
+
def rrelated(
|
|
14
|
+
file: str = typer.Argument(..., help="Target source file (path or filename)"),
|
|
15
|
+
depth: int = typer.Option(2, "--depth", "-d", help="Traversal depth"),
|
|
16
|
+
include_hubs: bool = typer.Option(False, "--include-hubs"),
|
|
17
|
+
use_sqlite_cache: bool = typer.Option(True, "--use-sqlite-cache/--no-sqlite-cache"),
|
|
18
|
+
):
|
|
19
|
+
target_file, root_path = resolve_target_file(file)
|
|
20
|
+
|
|
21
|
+
if use_sqlite_cache:
|
|
22
|
+
with CacheManager(root_path) as cache:
|
|
23
|
+
if cache.needs_rescan():
|
|
24
|
+
cache.scan_project(verbose=False)
|
|
25
|
+
files = cache.get_cached_files()
|
|
26
|
+
else:
|
|
27
|
+
files = []
|
|
28
|
+
for p in root_path.rglob("*"):
|
|
29
|
+
if p.is_file() and p.suffix.lower() in {".py", ".java"}:
|
|
30
|
+
files.append(p.resolve())
|
|
31
|
+
files = sorted(set(files))
|
|
32
|
+
|
|
33
|
+
graph, dependents_count = build_graph_with_counts(
|
|
34
|
+
files,
|
|
35
|
+
root_path,
|
|
36
|
+
use_sqlite_cache=use_sqlite_cache,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
hubs: set = set()
|
|
40
|
+
if not include_hubs:
|
|
41
|
+
hubs = get_hub_files_by_ratio(dependents_count, len(files), 0.5)
|
|
42
|
+
|
|
43
|
+
rev = build_reverse_graph(graph)
|
|
44
|
+
rel = bfs_related(rev, target_file, depth, skip=hubs)
|
|
45
|
+
|
|
46
|
+
if not rel:
|
|
47
|
+
typer.echo("No reverse-related files.")
|
|
48
|
+
raise typer.Exit(0)
|
|
49
|
+
|
|
50
|
+
for p in sorted(rel):
|
|
51
|
+
try:
|
|
52
|
+
typer.echo(str(p.relative_to(root_path)))
|
|
53
|
+
except Exception:
|
|
54
|
+
typer.echo(str(p))
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import typer
|
|
3
|
+
|
|
4
|
+
from .copy_cmd import register_copy
|
|
5
|
+
from .open_cmd import register_open
|
|
6
|
+
from .where_cmd import register_where
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def register_nav_commands(app: typer.Typer) -> None:
|
|
10
|
+
register_copy(app)
|
|
11
|
+
register_open(app)
|
|
12
|
+
register_where(app)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from ...core.resolve_target import resolve_target_file
|
|
6
|
+
from ...core.project import find_project_root
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _format_for_ai(files_content: list[tuple[Path, str]], root: Path) -> str:
|
|
10
|
+
lines: list[str] = []
|
|
11
|
+
|
|
12
|
+
lines.append("=" * 80)
|
|
13
|
+
lines.append(f"COPIED {len(files_content)} FILE(S)")
|
|
14
|
+
lines.append("=" * 80)
|
|
15
|
+
lines.append("")
|
|
16
|
+
|
|
17
|
+
for i, (path, content) in enumerate(files_content, 1):
|
|
18
|
+
try:
|
|
19
|
+
rel_path = path.relative_to(root)
|
|
20
|
+
except Exception:
|
|
21
|
+
rel_path = path
|
|
22
|
+
|
|
23
|
+
file_name = path.name
|
|
24
|
+
full_path = rel_path.as_posix()
|
|
25
|
+
lang = "java" if path.suffix == ".java" else "python"
|
|
26
|
+
line_count = content.count("\n") + 1
|
|
27
|
+
|
|
28
|
+
lines.append("=" * 80)
|
|
29
|
+
lines.append(f"FILE {i}/{len(files_content)}: {file_name}")
|
|
30
|
+
lines.append(f"Path: {full_path}")
|
|
31
|
+
lines.append(f"Lines: {line_count}")
|
|
32
|
+
lines.append("=" * 80)
|
|
33
|
+
lines.append("")
|
|
34
|
+
lines.append(f"```{lang}")
|
|
35
|
+
lines.append(content.rstrip())
|
|
36
|
+
lines.append("```")
|
|
37
|
+
lines.append("")
|
|
38
|
+
|
|
39
|
+
return "\n".join(lines)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def register_copy(app: typer.Typer) -> None:
|
|
43
|
+
@app.command(help="Copy file(s) contents to clipboard")
|
|
44
|
+
def copy(
|
|
45
|
+
files: list[str] = typer.Argument(..., help="File(s) to copy"),
|
|
46
|
+
root: str = typer.Option(".", "--root", "-r"),
|
|
47
|
+
raw: bool = typer.Option(False, "--raw", help="Raw content without formatting"),
|
|
48
|
+
) -> None:
|
|
49
|
+
try:
|
|
50
|
+
import pyperclip
|
|
51
|
+
except ImportError:
|
|
52
|
+
typer.echo("Error: pyperclip not installed")
|
|
53
|
+
typer.echo("Install: pip install pyperclip")
|
|
54
|
+
raise typer.Exit(1)
|
|
55
|
+
|
|
56
|
+
root_path = Path(root).resolve()
|
|
57
|
+
project_root = find_project_root(root_path)
|
|
58
|
+
|
|
59
|
+
files_content: list[tuple[Path, str]] = []
|
|
60
|
+
total_lines = 0
|
|
61
|
+
total_chars = 0
|
|
62
|
+
|
|
63
|
+
for file_query in files:
|
|
64
|
+
try:
|
|
65
|
+
target, _ = resolve_target_file(file_query, root=str(project_root))
|
|
66
|
+
except Exception as e:
|
|
67
|
+
typer.echo(f"✗ Could not find: {file_query}")
|
|
68
|
+
continue
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
content = target.read_text(encoding="utf-8", errors="ignore")
|
|
72
|
+
except Exception as e:
|
|
73
|
+
typer.echo(f"✗ Error reading {target.name}: {e}")
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
files_content.append((target, content))
|
|
77
|
+
total_lines += content.count("\n") + 1
|
|
78
|
+
total_chars += len(content)
|
|
79
|
+
|
|
80
|
+
if not files_content:
|
|
81
|
+
typer.echo("✗ No files copied")
|
|
82
|
+
raise typer.Exit(1)
|
|
83
|
+
|
|
84
|
+
if raw:
|
|
85
|
+
combined = "\n\n".join(content for _, content in files_content)
|
|
86
|
+
else:
|
|
87
|
+
combined = _format_for_ai(files_content, project_root)
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
pyperclip.copy(combined)
|
|
91
|
+
except Exception as e:
|
|
92
|
+
typer.echo(f"✗ Error copying to clipboard: {e}")
|
|
93
|
+
raise typer.Exit(1)
|
|
94
|
+
|
|
95
|
+
typer.echo(f"✓ Copied {len(files_content)} file(s) to clipboard")
|
|
96
|
+
typer.echo("")
|
|
97
|
+
for path, content in files_content:
|
|
98
|
+
lines = content.count("\n") + 1
|
|
99
|
+
typer.echo(f" • {path.name} ({lines:,} lines)")
|
|
100
|
+
typer.echo("")
|
|
101
|
+
typer.echo(f"Total: {total_lines:,} lines, {total_chars:,} characters")
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from ...core.opener import open_file
|
|
6
|
+
from ...core.resolve_target import resolve_target_file
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def register_open(app: typer.Typer) -> None:
|
|
10
|
+
@app.command(help="Open a file by name or partial path (project-local).")
|
|
11
|
+
def open(
|
|
12
|
+
query: str = typer.Argument(..., help="Example: 'imports.py' or 'parser/imports.py'"),
|
|
13
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Open without confirmation"),
|
|
14
|
+
):
|
|
15
|
+
target, _root = resolve_target_file(query)
|
|
16
|
+
|
|
17
|
+
if yes or typer.confirm(f"Open this file?\n{target}"):
|
|
18
|
+
open_file(target)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from ...errors import InvalidPathError
|
|
6
|
+
from ...core.where import file_to_module
|
|
7
|
+
from ...core.resolve_target import resolve_target_file
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def register_where(app: typer.Typer) -> None:
|
|
11
|
+
@app.command(help="Convert file path (or filename) to import path.")
|
|
12
|
+
def where(
|
|
13
|
+
file: str = typer.Argument(..., help="File path OR filename like 'imports.py' or 'UserService.java'"),
|
|
14
|
+
):
|
|
15
|
+
target, root_path = resolve_target_file(file)
|
|
16
|
+
|
|
17
|
+
mod = file_to_module(target, root_path)
|
|
18
|
+
if not mod:
|
|
19
|
+
raise InvalidPathError(message="Cannot convert to module path", path=target)
|
|
20
|
+
|
|
21
|
+
typer.echo(mod)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import typer
|
|
3
|
+
|
|
4
|
+
from .scan_cmd import register_scan
|
|
5
|
+
from .imports_cmd import register_imports
|
|
6
|
+
from .version_cmd import register_version
|
|
7
|
+
from .resolve_cmd import register_resolve
|
|
8
|
+
from .context_cmd import register_context
|
|
9
|
+
from .folder_cmd import register_folder
|
|
10
|
+
from .tree_cmd import register_tree
|
|
11
|
+
from .modeltree_cmd import register_modeltree
|
|
12
|
+
from .models_cmd import register_models
|
|
13
|
+
from .servicemap_cmd import register_servicemap
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def register_project_commands(app: typer.Typer) -> None:
|
|
17
|
+
register_scan(app)
|
|
18
|
+
register_imports(app)
|
|
19
|
+
register_version(app)
|
|
20
|
+
register_resolve(app)
|
|
21
|
+
register_context(app)
|
|
22
|
+
register_folder(app)
|
|
23
|
+
register_tree(app)
|
|
24
|
+
register_modeltree(app)
|
|
25
|
+
register_models(app)
|
|
26
|
+
register_servicemap(app)
|