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,180 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from ...errors import InvalidPathError
|
|
6
|
+
from ...core.project import find_project_root
|
|
7
|
+
from ...core.fuzzy import rank_paths, fuzzy_is_confident
|
|
8
|
+
from ...context.java_service import analyze_java_service
|
|
9
|
+
from ...context.python_service import analyze_python_service
|
|
10
|
+
from ...db.cache import CacheManager
|
|
11
|
+
|
|
12
|
+
PYTHON_SERVICE_DIRS = {"service", "services", "api", "views", "routes"}
|
|
13
|
+
|
|
14
|
+
ANALYZERS = {
|
|
15
|
+
".java": analyze_java_service,
|
|
16
|
+
".py": analyze_python_service,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _is_excluded_java_service_path(path: Path) -> bool:
|
|
21
|
+
parts = {p.lower() for p in path.parts}
|
|
22
|
+
stem = path.stem.lower()
|
|
23
|
+
name = path.name.lower()
|
|
24
|
+
|
|
25
|
+
if name == "package-info.java":
|
|
26
|
+
return True
|
|
27
|
+
if "dto" in parts or "mapper" in parts or "vm" in parts:
|
|
28
|
+
return True
|
|
29
|
+
if stem.endswith("dto") or "dto" in stem:
|
|
30
|
+
return True
|
|
31
|
+
if stem.endswith("mapper") or "mapper" in stem:
|
|
32
|
+
return True
|
|
33
|
+
return False
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _java_has_service_annotation(path: Path) -> bool:
|
|
37
|
+
try:
|
|
38
|
+
txt = path.read_text(encoding="utf-8", errors="ignore")
|
|
39
|
+
except OSError:
|
|
40
|
+
return False
|
|
41
|
+
return "@Service" in txt
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _is_service_file(path: Path) -> bool:
|
|
45
|
+
suffix = path.suffix.lower()
|
|
46
|
+
stem_lower = path.stem.lower()
|
|
47
|
+
|
|
48
|
+
if suffix == ".java":
|
|
49
|
+
if _is_excluded_java_service_path(path):
|
|
50
|
+
return False
|
|
51
|
+
if stem_lower.endswith("service"):
|
|
52
|
+
return True
|
|
53
|
+
return _java_has_service_annotation(path)
|
|
54
|
+
|
|
55
|
+
if suffix == ".py":
|
|
56
|
+
parts = {p.lower() for p in path.parts}
|
|
57
|
+
return bool(parts & PYTHON_SERVICE_DIRS)
|
|
58
|
+
|
|
59
|
+
return False
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _short_rel(path: Path, root: Path, keep: int = 5) -> str:
|
|
63
|
+
try:
|
|
64
|
+
parts = list(path.relative_to(root).parts)
|
|
65
|
+
except ValueError:
|
|
66
|
+
return str(path)
|
|
67
|
+
tail = parts[-keep:] if len(parts) > keep else parts
|
|
68
|
+
return "/".join(tail)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _resolve_service_file(query: str, root: str = ".", top: int = 6) -> tuple[Path, Path]:
|
|
72
|
+
root_path = Path(root).resolve()
|
|
73
|
+
project_root = find_project_root(root_path)
|
|
74
|
+
|
|
75
|
+
direct = Path(query)
|
|
76
|
+
if direct.is_file():
|
|
77
|
+
return direct.resolve(), project_root
|
|
78
|
+
|
|
79
|
+
with CacheManager(project_root) as cache:
|
|
80
|
+
if cache.needs_rescan():
|
|
81
|
+
cache.scan_project(verbose=False)
|
|
82
|
+
all_files = cache.get_cached_files()
|
|
83
|
+
|
|
84
|
+
service_files = [p for p in all_files if _is_service_file(p)]
|
|
85
|
+
if not service_files:
|
|
86
|
+
raise InvalidPathError(message="No service files found", path=project_root)
|
|
87
|
+
|
|
88
|
+
query_stem = Path(query).stem.lower()
|
|
89
|
+
exact = [f for f in service_files if f.stem.lower() == query_stem]
|
|
90
|
+
if len(exact) == 1:
|
|
91
|
+
return exact[0].resolve(), project_root
|
|
92
|
+
|
|
93
|
+
ranked = rank_paths(query, service_files, project_root, top=top)
|
|
94
|
+
if not ranked:
|
|
95
|
+
raise InvalidPathError(message="Service not found", path=Path(query))
|
|
96
|
+
|
|
97
|
+
if fuzzy_is_confident(ranked):
|
|
98
|
+
top_score = ranked[0][1]
|
|
99
|
+
if sum(1 for _, s in ranked if abs(s - top_score) < 0.01) == 1:
|
|
100
|
+
return ranked[0][0].resolve(), project_root
|
|
101
|
+
|
|
102
|
+
typer.echo("Multiple matches found — select a service:")
|
|
103
|
+
for i, (p, score) in enumerate(ranked, 1):
|
|
104
|
+
typer.echo(f" {i}. {_short_rel(p, project_root)} (score={score:.2f})")
|
|
105
|
+
|
|
106
|
+
choice = typer.prompt("Enter number (0 to cancel)", type=int, default=0)
|
|
107
|
+
if not (1 <= choice <= len(ranked)):
|
|
108
|
+
raise typer.Exit()
|
|
109
|
+
|
|
110
|
+
return ranked[choice - 1][0].resolve(), project_root
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _render_service_map(data: dict) -> list[str]:
|
|
114
|
+
lines: list[str] = [data["service_name"], "━" * 80, ""]
|
|
115
|
+
|
|
116
|
+
if endpoints := data.get("endpoints", []):
|
|
117
|
+
lines.append("ENDPOINTS")
|
|
118
|
+
for ep in endpoints:
|
|
119
|
+
lines.append(f" {ep.get('method', '').ljust(7)} {ep.get('path', '')}")
|
|
120
|
+
if handler := ep.get("handler"):
|
|
121
|
+
lines.append(f" → {handler}")
|
|
122
|
+
lines.append("")
|
|
123
|
+
|
|
124
|
+
if methods := data.get("methods", []):
|
|
125
|
+
lines.append("SERVICE METHODS")
|
|
126
|
+
for m in methods:
|
|
127
|
+
sig = f"{m.get('name', '')}({m.get('params', '')})"
|
|
128
|
+
if ret := m.get("return"):
|
|
129
|
+
sig += f" → {ret}"
|
|
130
|
+
lines.append(f" • {sig}")
|
|
131
|
+
lines.append("")
|
|
132
|
+
|
|
133
|
+
if models := data.get("models", {}):
|
|
134
|
+
lines.append("MODELS")
|
|
135
|
+
for model_name, model_data in models.items():
|
|
136
|
+
lines.append(f" {model_name}")
|
|
137
|
+
for fname, ftype, is_pk in model_data.get("fields", []):
|
|
138
|
+
marker = "🔑" if is_pk else " "
|
|
139
|
+
lines.append(f" {marker} {fname}: {ftype}")
|
|
140
|
+
for rel in model_data.get("relationships", []):
|
|
141
|
+
lines.append(f" → {rel.get('kind', '')} {rel.get('target', '')} (via {rel.get('field', '')})")
|
|
142
|
+
lines.append("")
|
|
143
|
+
|
|
144
|
+
if repos := data.get("repositories", []):
|
|
145
|
+
lines.append("REPOSITORIES")
|
|
146
|
+
for repo in repos:
|
|
147
|
+
lines.append(f" {repo.get('name', '')}")
|
|
148
|
+
for method in repo.get("methods", [])[:3]:
|
|
149
|
+
lines.append(f" • {method}")
|
|
150
|
+
lines.append("")
|
|
151
|
+
|
|
152
|
+
if summary := data.get("summary", {}):
|
|
153
|
+
lines.append("SUMMARY")
|
|
154
|
+
lines.extend(f" {k}: {v}" for k, v in summary.items())
|
|
155
|
+
|
|
156
|
+
return lines
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def register_servicemap(app: typer.Typer) -> None:
|
|
160
|
+
@app.command(help="Service endpoint-model relationship map")
|
|
161
|
+
def servicemap(
|
|
162
|
+
service: str = typer.Argument(..., help="Service name or path"),
|
|
163
|
+
root: str = typer.Option(".", help="Project root directory"),
|
|
164
|
+
save: bool = typer.Option(False, "--save", "-s", help="Save output to file"),
|
|
165
|
+
) -> None:
|
|
166
|
+
target, project_root = _resolve_service_file(service, root=root)
|
|
167
|
+
|
|
168
|
+
analyzer = ANALYZERS.get(target.suffix.lower())
|
|
169
|
+
if analyzer is None:
|
|
170
|
+
raise InvalidPathError(message="Unsupported file type", path=target)
|
|
171
|
+
|
|
172
|
+
result = analyzer(target, project_root)
|
|
173
|
+
lines = _render_service_map(result)
|
|
174
|
+
|
|
175
|
+
if save:
|
|
176
|
+
out_path = project_root / f"codeintel-servicemap-{result['service_name']}.txt"
|
|
177
|
+
out_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
178
|
+
typer.echo(f"Saved: {out_path.as_posix()}")
|
|
179
|
+
else:
|
|
180
|
+
typer.echo("\n" + "\n".join(lines))
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
# Optional: pip install pathspec
|
|
8
|
+
try:
|
|
9
|
+
import pathspec # type: ignore
|
|
10
|
+
except Exception: # pragma: no cover
|
|
11
|
+
pathspec = None
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
DEFAULT_IGNORE_NAMES = {
|
|
15
|
+
".git", ".idea", ".vscode",
|
|
16
|
+
"__pycache__", ".mypy_cache", ".pytest_cache", ".ruff_cache",
|
|
17
|
+
"venv", ".venv",
|
|
18
|
+
"node_modules", "dist", "build",
|
|
19
|
+
".DS_Store",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class TreeOptions:
|
|
25
|
+
depth: int # -1 = unlimited
|
|
26
|
+
show_hidden: bool # show dotfiles/dirs
|
|
27
|
+
keep_dotenv: bool # always show ".env"
|
|
28
|
+
use_gitignore: bool # apply .gitignore patterns
|
|
29
|
+
exts: tuple[str, ...] # file extensions to include (".py", ".java")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _load_gitignore_spec(root: Path):
|
|
33
|
+
if pathspec is None:
|
|
34
|
+
return None
|
|
35
|
+
gi = root / ".gitignore"
|
|
36
|
+
if not gi.exists():
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
lines: list[str] = []
|
|
40
|
+
for line in gi.read_text(encoding="utf-8", errors="ignore").splitlines():
|
|
41
|
+
s = line.strip()
|
|
42
|
+
if not s or s.startswith("#"):
|
|
43
|
+
continue
|
|
44
|
+
lines.append(s)
|
|
45
|
+
|
|
46
|
+
if not lines:
|
|
47
|
+
return None
|
|
48
|
+
return pathspec.PathSpec.from_lines("gitwildmatch", lines)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _is_hidden(name: str) -> bool:
|
|
52
|
+
return name.startswith(".")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _gitignore_matches(spec, rel_posix: str, is_dir: bool) -> bool:
|
|
56
|
+
if spec is None:
|
|
57
|
+
return False
|
|
58
|
+
if spec.match_file(rel_posix):
|
|
59
|
+
return True
|
|
60
|
+
if is_dir and spec.match_file(rel_posix.rstrip("/") + "/"):
|
|
61
|
+
return True
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _is_ignored(root: Path, p: Path, spec, opts: TreeOptions) -> bool:
|
|
66
|
+
name = p.name
|
|
67
|
+
|
|
68
|
+
if opts.keep_dotenv and name == ".env":
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
if name in DEFAULT_IGNORE_NAMES:
|
|
72
|
+
return True
|
|
73
|
+
|
|
74
|
+
if _is_hidden(name) and not opts.show_hidden:
|
|
75
|
+
return True
|
|
76
|
+
|
|
77
|
+
if opts.use_gitignore and spec is not None:
|
|
78
|
+
rel = p.relative_to(root).as_posix()
|
|
79
|
+
if _gitignore_matches(spec, rel, p.is_dir()):
|
|
80
|
+
return True
|
|
81
|
+
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _is_source_file(p: Path, opts: TreeOptions) -> bool:
|
|
86
|
+
if not p.is_file():
|
|
87
|
+
return False
|
|
88
|
+
# include only selected extensions
|
|
89
|
+
return p.suffix.lower() in opts.exts
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _list_children(root: Path, cur: Path, spec, opts: TreeOptions) -> list[Path]:
|
|
93
|
+
try:
|
|
94
|
+
entries = list(cur.iterdir())
|
|
95
|
+
except (PermissionError, FileNotFoundError):
|
|
96
|
+
return []
|
|
97
|
+
|
|
98
|
+
kept: list[Path] = []
|
|
99
|
+
for p in entries:
|
|
100
|
+
if _is_ignored(root, p, spec, opts):
|
|
101
|
+
continue
|
|
102
|
+
if p.is_dir():
|
|
103
|
+
kept.append(p)
|
|
104
|
+
else:
|
|
105
|
+
if _is_source_file(p, opts) or (opts.keep_dotenv and p.name == ".env"):
|
|
106
|
+
kept.append(p)
|
|
107
|
+
|
|
108
|
+
# dirs first, then files; alpha
|
|
109
|
+
kept.sort(key=lambda x: (x.is_file(), x.name.lower()))
|
|
110
|
+
return kept
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _dir_has_visible_sources(root: Path, cur: Path, spec, opts: TreeOptions, remaining_depth: int) -> bool:
|
|
114
|
+
"""
|
|
115
|
+
True if this directory contains any visible source file (directly or deeper).
|
|
116
|
+
remaining_depth: how many more levels we can descend; -1 for unlimited.
|
|
117
|
+
"""
|
|
118
|
+
children = _list_children(root, cur, spec, opts)
|
|
119
|
+
if not children:
|
|
120
|
+
return False
|
|
121
|
+
|
|
122
|
+
for c in children:
|
|
123
|
+
if c.is_file():
|
|
124
|
+
return True
|
|
125
|
+
|
|
126
|
+
if remaining_depth == 0:
|
|
127
|
+
# we can't descend further; if it has dirs but no files shown, treat as empty
|
|
128
|
+
return False
|
|
129
|
+
|
|
130
|
+
next_depth = -1 if remaining_depth == -1 else remaining_depth - 1
|
|
131
|
+
for c in children:
|
|
132
|
+
if c.is_dir() and _dir_has_visible_sources(root, c, spec, opts, next_depth):
|
|
133
|
+
return True
|
|
134
|
+
|
|
135
|
+
return False
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _print_tree(root: Path, opts: TreeOptions) -> None:
|
|
139
|
+
spec = _load_gitignore_spec(root) if opts.use_gitignore else None
|
|
140
|
+
|
|
141
|
+
def walk(cur: Path, prefix: str, level: int) -> None:
|
|
142
|
+
if opts.depth != -1 and level > opts.depth:
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
items = _list_children(root, cur, spec, opts)
|
|
146
|
+
|
|
147
|
+
# prune empty dirs (dirs that contain no visible sources)
|
|
148
|
+
pruned: list[Path] = []
|
|
149
|
+
for p in items:
|
|
150
|
+
if p.is_dir():
|
|
151
|
+
remaining = -1 if opts.depth == -1 else max(opts.depth - level, 0)
|
|
152
|
+
if _dir_has_visible_sources(root, p, spec, opts, remaining):
|
|
153
|
+
pruned.append(p)
|
|
154
|
+
else:
|
|
155
|
+
pruned.append(p)
|
|
156
|
+
|
|
157
|
+
items = pruned
|
|
158
|
+
|
|
159
|
+
for i, p in enumerate(items):
|
|
160
|
+
last = i == len(items) - 1
|
|
161
|
+
branch = "└── " if last else "├── "
|
|
162
|
+
typer.echo(prefix + branch + p.name)
|
|
163
|
+
|
|
164
|
+
if p.is_dir():
|
|
165
|
+
extension = " " if last else "│ "
|
|
166
|
+
walk(p, prefix + extension, level + 1)
|
|
167
|
+
|
|
168
|
+
typer.echo(root.name)
|
|
169
|
+
walk(root, prefix="", level=1)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def register_tree(app: typer.Typer) -> None:
|
|
173
|
+
@app.command(help="Tree view of source files only (python/java).")
|
|
174
|
+
def tree(
|
|
175
|
+
root: str = typer.Argument(".", help="Folder to print tree for"),
|
|
176
|
+
depth: int = typer.Option(12, "--depth", help="Max depth (-1 = unlimited)"),
|
|
177
|
+
hidden: bool = typer.Option(False, "--hidden", help="Show hidden dotfiles/folders"),
|
|
178
|
+
no_gitignore: bool = typer.Option(False, "--no-gitignore", help="Do not apply .gitignore rules"),
|
|
179
|
+
keep_dotenv: bool = typer.Option(True, "--keep-dotenv/--no-keep-dotenv", help="Always show .env"),
|
|
180
|
+
lang: str = typer.Option("all", "--lang", help="all | py | java"),
|
|
181
|
+
):
|
|
182
|
+
root_path = Path(root).resolve()
|
|
183
|
+
if not root_path.exists() or not root_path.is_dir():
|
|
184
|
+
raise typer.BadParameter(f"Invalid folder: {root_path}")
|
|
185
|
+
|
|
186
|
+
lang_norm = lang.strip().lower()
|
|
187
|
+
if lang_norm == "py":
|
|
188
|
+
exts = (".py",)
|
|
189
|
+
elif lang_norm == "java":
|
|
190
|
+
exts = (".java",)
|
|
191
|
+
elif lang_norm == "all":
|
|
192
|
+
exts = (".py", ".java")
|
|
193
|
+
else:
|
|
194
|
+
raise typer.BadParameter("Invalid --lang. Use: all | py | java")
|
|
195
|
+
|
|
196
|
+
opts = TreeOptions(
|
|
197
|
+
depth=depth,
|
|
198
|
+
show_hidden=hidden,
|
|
199
|
+
keep_dotenv=keep_dotenv,
|
|
200
|
+
use_gitignore=not no_gitignore,
|
|
201
|
+
exts=exts,
|
|
202
|
+
)
|
|
203
|
+
_print_tree(root_path, opts)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
import importlib.metadata as md
|
|
5
|
+
from ...core.resolve_target import resolve_target_file
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def register_version(app: typer.Typer) -> None:
|
|
9
|
+
@app.command(help="Show CodeIntel version.")
|
|
10
|
+
def version():
|
|
11
|
+
try:
|
|
12
|
+
typer.echo(md.version("codeintel"))
|
|
13
|
+
except md.PackageNotFoundError:
|
|
14
|
+
typer.echo("codeintel (not installed as a package)")
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from collections import deque
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from ..graph.builder import build_graph_with_counts, get_hub_files_by_ratio
|
|
9
|
+
from ..lang.router import extract_imports
|
|
10
|
+
|
|
11
|
+
TYPE = re.compile(r"\b(class|interface|enum|record)\s+([A-Za-z_][A-Za-z0-9_]*)\b")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _java_types_only(path: Path) -> list[str]:
|
|
15
|
+
out: list[str] = []
|
|
16
|
+
try:
|
|
17
|
+
for line in path.read_text(errors="ignore").splitlines():
|
|
18
|
+
m = TYPE.search(line)
|
|
19
|
+
if m:
|
|
20
|
+
kind = m.group(1)
|
|
21
|
+
name = m.group(2)
|
|
22
|
+
label = f"{name} ({kind})"
|
|
23
|
+
if label not in out:
|
|
24
|
+
out.append(label)
|
|
25
|
+
except Exception:
|
|
26
|
+
return out
|
|
27
|
+
return out
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _collect_java_imports(files: list[Path]) -> list[str]:
|
|
31
|
+
acc: set[str] = set()
|
|
32
|
+
for f in files:
|
|
33
|
+
if f.suffix != ".java":
|
|
34
|
+
continue
|
|
35
|
+
for imp in extract_imports(f, "java"):
|
|
36
|
+
acc.add(str(imp))
|
|
37
|
+
return sorted(acc)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _print_files_with_types(title: str, files: list[Path], root: Path) -> None:
|
|
41
|
+
typer.echo(title)
|
|
42
|
+
if not files:
|
|
43
|
+
typer.echo(" (none)")
|
|
44
|
+
typer.echo("")
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
any_printed = False
|
|
48
|
+
for f in files:
|
|
49
|
+
if f.suffix != ".java":
|
|
50
|
+
continue
|
|
51
|
+
types_ = _java_types_only(f)
|
|
52
|
+
if not types_:
|
|
53
|
+
continue
|
|
54
|
+
any_printed = True
|
|
55
|
+
typer.echo(f"• {f.relative_to(root)}")
|
|
56
|
+
for t in types_:
|
|
57
|
+
typer.echo(f" {t}")
|
|
58
|
+
typer.echo("")
|
|
59
|
+
|
|
60
|
+
if not any_printed:
|
|
61
|
+
typer.echo(" (none)")
|
|
62
|
+
typer.echo("")
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _print_imports(title: str, files: list[Path]) -> None:
|
|
67
|
+
typer.echo(title)
|
|
68
|
+
imps = _collect_java_imports(files)
|
|
69
|
+
if imps:
|
|
70
|
+
for x in imps:
|
|
71
|
+
typer.echo(f" {x}")
|
|
72
|
+
else:
|
|
73
|
+
typer.echo(" (none)")
|
|
74
|
+
typer.echo("")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _build_reverse_graph(graph: dict[Path, set[Path]]) -> dict[Path, set[Path]]:
|
|
78
|
+
rev: dict[Path, set[Path]] = {}
|
|
79
|
+
for src, deps in graph.items():
|
|
80
|
+
for dep in deps:
|
|
81
|
+
rev.setdefault(dep, set()).add(src)
|
|
82
|
+
return rev
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _bfs_layers(
|
|
86
|
+
graph: dict[Path, set[Path]],
|
|
87
|
+
start: Path,
|
|
88
|
+
depth: int,
|
|
89
|
+
*,
|
|
90
|
+
include_reverse: bool,
|
|
91
|
+
hubs: set[Path],
|
|
92
|
+
) -> dict[int, set[Path]]:
|
|
93
|
+
if depth <= 0:
|
|
94
|
+
return {}
|
|
95
|
+
|
|
96
|
+
rev = _build_reverse_graph(graph) if include_reverse else {}
|
|
97
|
+
layers: dict[int, set[Path]] = {}
|
|
98
|
+
visited: set[Path] = {start}
|
|
99
|
+
|
|
100
|
+
q: deque[tuple[Path, int]] = deque()
|
|
101
|
+
q.append((start, 0))
|
|
102
|
+
|
|
103
|
+
while q:
|
|
104
|
+
node, d = q.popleft()
|
|
105
|
+
if d >= depth:
|
|
106
|
+
continue
|
|
107
|
+
|
|
108
|
+
neighbors: set[Path] = set()
|
|
109
|
+
neighbors |= graph.get(node, set())
|
|
110
|
+
if include_reverse:
|
|
111
|
+
neighbors |= rev.get(node, set())
|
|
112
|
+
|
|
113
|
+
for nb in neighbors:
|
|
114
|
+
if nb in visited:
|
|
115
|
+
continue
|
|
116
|
+
if hubs and nb in hubs:
|
|
117
|
+
continue
|
|
118
|
+
visited.add(nb)
|
|
119
|
+
nd = d + 1
|
|
120
|
+
layers.setdefault(nd, set()).add(nb)
|
|
121
|
+
q.append((nb, nd))
|
|
122
|
+
|
|
123
|
+
return layers
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def run_java_context(
|
|
127
|
+
*,
|
|
128
|
+
target: Path,
|
|
129
|
+
root: Path,
|
|
130
|
+
all_files: list[Path],
|
|
131
|
+
depth: int,
|
|
132
|
+
forward_only: bool,
|
|
133
|
+
include_hubs: bool,
|
|
134
|
+
) -> None:
|
|
135
|
+
graph, dependents_count = build_graph_with_counts(all_files, root)
|
|
136
|
+
|
|
137
|
+
hubs: set[Path] = set()
|
|
138
|
+
if not include_hubs:
|
|
139
|
+
hubs = get_hub_files_by_ratio(dependents_count, len(all_files), 0.5)
|
|
140
|
+
|
|
141
|
+
same_folder = sorted(
|
|
142
|
+
f
|
|
143
|
+
for f in all_files
|
|
144
|
+
if f.suffix == ".java" and f.parent == target.parent and f != target
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
layers = _bfs_layers(
|
|
148
|
+
graph,
|
|
149
|
+
target,
|
|
150
|
+
depth=max(depth, 1),
|
|
151
|
+
include_reverse=not forward_only,
|
|
152
|
+
hubs=hubs,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
level1_files = sorted(layers.get(1, set()))
|
|
156
|
+
level2_files = sorted(layers.get(2, set())) if depth >= 2 else []
|
|
157
|
+
|
|
158
|
+
typer.echo("")
|
|
159
|
+
typer.echo("CONTEXT")
|
|
160
|
+
typer.echo(f"Target: {target.relative_to(root)}")
|
|
161
|
+
typer.echo(f"Depth: {depth} Mode: {'forward-only' if forward_only else 'forward+reverse'}")
|
|
162
|
+
typer.echo("")
|
|
163
|
+
|
|
164
|
+
typer.echo("TARGET IMPORTS")
|
|
165
|
+
t_imps = extract_imports(target, "java")
|
|
166
|
+
if t_imps:
|
|
167
|
+
for x in t_imps:
|
|
168
|
+
typer.echo(f" {x}")
|
|
169
|
+
else:
|
|
170
|
+
typer.echo(" (none)")
|
|
171
|
+
typer.echo("")
|
|
172
|
+
|
|
173
|
+
_print_files_with_types("SAME FOLDER TYPES", same_folder, root)
|
|
174
|
+
|
|
175
|
+
_print_files_with_types("RELATED LEVEL 1 TYPES", level1_files, root)
|
|
176
|
+
_print_imports("RELATED LEVEL 1 IMPORTS", level1_files)
|
|
177
|
+
|
|
178
|
+
if depth >= 2:
|
|
179
|
+
_print_files_with_types("RELATED LEVEL 2 TYPES", level2_files, root)
|
|
180
|
+
_print_imports("RELATED LEVEL 2 IMPORTS", level2_files)
|