aja-codeintel 0.1.5__tar.gz → 0.1.6__tar.gz
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.5 → aja_codeintel-0.1.6}/PKG-INFO +1 -1
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/aja_codeintel.egg-info/PKG-INFO +1 -1
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/aja_codeintel.egg-info/SOURCES.txt +2 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/commands/project/__init__.py +4 -2
- aja_codeintel-0.1.6/codeintel_cli/commands/project/errors_cmd.py +238 -0
- aja_codeintel-0.1.6/codeintel_cli/commands/project/modeltree_cmd.py +217 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/commands/project/overview_cmd.py +61 -203
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/commands/project/scan_cmd.py +46 -51
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/commands/project/servicemap_cmd.py +173 -180
- aja_codeintel-0.1.6/codeintel_cli/commands/project/types_cmd.py +424 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/context/java_rel.py +330 -368
- aja_codeintel-0.1.6/codeintel_cli/context/java_service.py +226 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/core/fuzzy.py +72 -72
- aja_codeintel-0.1.6/codeintel_cli/core/resolve_model_target.py +135 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/core/resolve_target.py +59 -53
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/db/cache.py +4 -5
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/db/operations.py +54 -180
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/graph/builder.py +148 -148
- aja_codeintel-0.1.6/codeintel_cli/lang/java/ast_engine.py +98 -0
- aja_codeintel-0.1.6/codeintel_cli/scanner/scanner.py +50 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/pyproject.toml +1 -1
- aja_codeintel-0.1.5/codeintel_cli/commands/project/modeltree_cmd.py +0 -476
- aja_codeintel-0.1.5/codeintel_cli/commands/project/types_cmd.py +0 -318
- aja_codeintel-0.1.5/codeintel_cli/context/java_service.py +0 -291
- aja_codeintel-0.1.5/codeintel_cli/core/resolve_model_target.py +0 -92
- aja_codeintel-0.1.5/codeintel_cli/scanner/scanner.py +0 -41
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/LICENSE +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/README.md +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/aja_codeintel.egg-info/dependency_links.txt +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/aja_codeintel.egg-info/entry_points.txt +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/aja_codeintel.egg-info/requires.txt +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/aja_codeintel.egg-info/top_level.txt +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/__init__.py +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/__main__.py +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/cli.py +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/commands/__init__.py +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/commands/graph/__init__.py +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/commands/graph/deps_cmd.py +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/commands/graph/related_cmd.py +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/commands/graph/relsymbols_cmd.py +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/commands/graph/reverse_related_cmd.py +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/commands/nav/__init__.py +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/commands/nav/copy_cmd.py +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/commands/nav/open_cmd.py +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/commands/nav/where_cmd.py +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/commands/project/context_cmd.py +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/commands/project/endpoints_cmd.py +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/commands/project/folder_cmd.py +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/commands/project/frontend_cmd.py +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/commands/project/imports_cmd.py +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/commands/project/models_cmd.py +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/commands/project/new.py +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/commands/project/resolve_cmd.py +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/commands/project/tree_cmd.py +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/commands/project/version_cmd.py +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/context/java_context.py +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/context/python_context.py +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/context/python_rel.py +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/context/python_service.py +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/core/opener.py +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/core/project.py +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/core/resolve_folder.py +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/core/timing.py +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/core/where.py +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/db/__init__.py +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/db/schema.py +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/endpoints/__init__.py +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/endpoints/java_spring.py +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/endpoints/models.py +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/endpoints/openapi_spec.py +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/endpoints/python_web.py +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/endpoints/scan.py +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/errors.py +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/frontend/__init__.py +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/frontend/server.py +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/graph/__init__.py +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/graph/query.py +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/graph/traverse.py +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/lang/__init__.py +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/lang/java/__init__.py +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/lang/java/engine.py +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/lang/java/models.py +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/lang/java/resolve.py +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/lang/python/__init__.py +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/lang/python/engine.py +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/lang/python/models.py +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/lang/router.py +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/parser/imports.py +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/parser/resolve.py +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/parser/symbols.py +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/scanner/__init__.py +0 -0
- {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/setup.cfg +0 -0
|
@@ -24,6 +24,7 @@ codeintel_cli/commands/nav/where_cmd.py
|
|
|
24
24
|
codeintel_cli/commands/project/__init__.py
|
|
25
25
|
codeintel_cli/commands/project/context_cmd.py
|
|
26
26
|
codeintel_cli/commands/project/endpoints_cmd.py
|
|
27
|
+
codeintel_cli/commands/project/errors_cmd.py
|
|
27
28
|
codeintel_cli/commands/project/folder_cmd.py
|
|
28
29
|
codeintel_cli/commands/project/frontend_cmd.py
|
|
29
30
|
codeintel_cli/commands/project/imports_cmd.py
|
|
@@ -70,6 +71,7 @@ codeintel_cli/graph/traverse.py
|
|
|
70
71
|
codeintel_cli/lang/__init__.py
|
|
71
72
|
codeintel_cli/lang/router.py
|
|
72
73
|
codeintel_cli/lang/java/__init__.py
|
|
74
|
+
codeintel_cli/lang/java/ast_engine.py
|
|
73
75
|
codeintel_cli/lang/java/engine.py
|
|
74
76
|
codeintel_cli/lang/java/models.py
|
|
75
77
|
codeintel_cli/lang/java/resolve.py
|
|
@@ -17,7 +17,8 @@ def register_project_commands(app: typer.Typer) -> None:
|
|
|
17
17
|
from .servicemap_cmd import register_servicemap
|
|
18
18
|
from .modeltree_cmd import register_modeltree
|
|
19
19
|
from .version_cmd import register_version
|
|
20
|
-
from .frontend_cmd import register_frontend
|
|
20
|
+
from .frontend_cmd import register_frontend
|
|
21
|
+
from .errors_cmd import register_errors
|
|
21
22
|
|
|
22
23
|
register_overview(app)
|
|
23
24
|
register_models(app)
|
|
@@ -32,4 +33,5 @@ def register_project_commands(app: typer.Typer) -> None:
|
|
|
32
33
|
register_servicemap(app)
|
|
33
34
|
register_modeltree(app)
|
|
34
35
|
register_version(app)
|
|
35
|
-
register_frontend(app)
|
|
36
|
+
register_frontend(app)
|
|
37
|
+
register_errors(app)
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import subprocess
|
|
5
|
+
import time
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
from ...core.project import find_project_root
|
|
12
|
+
from ...scanner.scanner import find_all_supported_files
|
|
13
|
+
from ...db.cache import CacheManager
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# ─────────────────────────── helpers ────────────────────────────
|
|
17
|
+
|
|
18
|
+
def _find_maven_cmd(root: Path) -> str:
|
|
19
|
+
for name in ["mvnw.cmd", "mvnw"]:
|
|
20
|
+
if (root / name).exists():
|
|
21
|
+
return str(root / name)
|
|
22
|
+
return "mvn"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _changed_files(project_root: Path) -> list[Path]:
|
|
26
|
+
"""Return java files modified since last scan."""
|
|
27
|
+
try:
|
|
28
|
+
cache = CacheManager(project_root)
|
|
29
|
+
cached = {Path(p) for p in cache.get_cached_files()}
|
|
30
|
+
changed = []
|
|
31
|
+
for p in cached:
|
|
32
|
+
if not p.exists():
|
|
33
|
+
continue
|
|
34
|
+
db_mtime = cache.get_file_mtime(p)
|
|
35
|
+
if db_mtime is None or p.stat().st_mtime > db_mtime:
|
|
36
|
+
changed.append(p)
|
|
37
|
+
return changed
|
|
38
|
+
except Exception:
|
|
39
|
+
return []
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ─────────────────────── mvn compile errors ─────────────────────
|
|
43
|
+
|
|
44
|
+
_MVN_ERROR_RE = re.compile(
|
|
45
|
+
r'^\[ERROR\]\s+(.+?\.java):\[(\d+),\d+\]\s+(.+)$'
|
|
46
|
+
)
|
|
47
|
+
_MVN_WARNING_RE = re.compile(
|
|
48
|
+
r'^\[WARNING\]\s+(.+?\.java):\[(\d+),\d+\]\s+(.+)$'
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _run_mvn_compile(project_root: Path) -> tuple[list[dict], list[dict]]:
|
|
53
|
+
mvn = _find_maven_cmd(project_root)
|
|
54
|
+
try:
|
|
55
|
+
result = subprocess.run(
|
|
56
|
+
f'"{mvn}" compile -q --batch-mode',
|
|
57
|
+
cwd=str(project_root),
|
|
58
|
+
capture_output=True,
|
|
59
|
+
text=True,
|
|
60
|
+
shell=True,
|
|
61
|
+
timeout=120,
|
|
62
|
+
)
|
|
63
|
+
output = result.stdout + result.stderr
|
|
64
|
+
except subprocess.TimeoutExpired:
|
|
65
|
+
return [], [{"file": "?", "line": "?", "msg": "mvn compile timed out after 120s"}]
|
|
66
|
+
except Exception as e:
|
|
67
|
+
return [], [{"file": "?", "line": "?", "msg": str(e)}]
|
|
68
|
+
|
|
69
|
+
errors: list[dict] = []
|
|
70
|
+
warnings: list[dict] = []
|
|
71
|
+
for line in output.splitlines():
|
|
72
|
+
m = _MVN_ERROR_RE.match(line.strip())
|
|
73
|
+
if m:
|
|
74
|
+
errors.append({"file": Path(m.group(1)).name, "line": m.group(2), "msg": m.group(3).strip()})
|
|
75
|
+
continue
|
|
76
|
+
m = _MVN_WARNING_RE.match(line.strip())
|
|
77
|
+
if m:
|
|
78
|
+
warnings.append({"file": Path(m.group(1)).name, "line": m.group(2), "msg": m.group(3).strip()})
|
|
79
|
+
return errors, warnings
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ──────────────────────── static analysis ───────────────────────
|
|
83
|
+
|
|
84
|
+
# Patterns: (regex, message, severity)
|
|
85
|
+
_STATIC_CHECKS: list[tuple[re.Pattern, str, str]] = [
|
|
86
|
+
# Empty catch block
|
|
87
|
+
(re.compile(r'catch\s*\([^)]+\)\s*\{\s*\}'), "Empty catch block swallows exception", "warning"),
|
|
88
|
+
# Null dereference risk: .get() without check
|
|
89
|
+
(re.compile(r'\.get\(\)\s*\.'), "Possible null dereference after .get()", "warning"),
|
|
90
|
+
# Syso debug prints left in
|
|
91
|
+
(re.compile(r'System\.out\.print'), "Debug System.out.print left in code", "warning"),
|
|
92
|
+
# TODO/FIXME left in
|
|
93
|
+
(re.compile(r'//\s*(TODO|FIXME|HACK|XXX)\b'), "Unresolved TODO/FIXME comment", "info"),
|
|
94
|
+
# Catching raw Exception
|
|
95
|
+
(re.compile(r'catch\s*\(\s*Exception\s+\w+\s*\)'), "Catching raw Exception — too broad", "warning"),
|
|
96
|
+
# Unused import hint (simple heuristic)
|
|
97
|
+
(re.compile(r'^import\s+java\.util\.(Vector|Hashtable|Stack)\s*;', re.MULTILINE), "Outdated collection type import", "info"),
|
|
98
|
+
# Magic numbers (not 0/1/-1)
|
|
99
|
+
(re.compile(r'(?<![.\w])[2-9]\d{2,}(?![.\w])'), "Magic number — consider named constant", "info"),
|
|
100
|
+
# String == comparison
|
|
101
|
+
(re.compile(r'==\s*"[^"]*"'), 'String compared with == instead of .equals()', "error"),
|
|
102
|
+
# Mutable static field
|
|
103
|
+
(re.compile(r'static\s+(?!final)\w+\s+\w+\s*='), "Mutable static field — potential concurrency issue", "warning"),
|
|
104
|
+
# NullPointerException throw without message
|
|
105
|
+
(re.compile(r'throw\s+new\s+NullPointerException\s*\(\s*\)'), "NullPointerException thrown without message", "warning"),
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
_SKIP_DIRS = {"target", "build", ".git", "test", "tests"}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _static_analyze(files: list[Path]) -> list[dict]:
|
|
112
|
+
issues: list[dict] = []
|
|
113
|
+
for f in files:
|
|
114
|
+
if any(part in _SKIP_DIRS for part in f.parts):
|
|
115
|
+
continue
|
|
116
|
+
try:
|
|
117
|
+
lines = f.read_text(encoding="utf-8", errors="ignore").splitlines()
|
|
118
|
+
except Exception:
|
|
119
|
+
continue
|
|
120
|
+
for lineno, line in enumerate(lines, 1):
|
|
121
|
+
stripped = line.strip()
|
|
122
|
+
if stripped.startswith("//") and not re.match(r'//\s*(TODO|FIXME)', stripped):
|
|
123
|
+
continue # skip pure comment lines except TODO/FIXME
|
|
124
|
+
for pattern, msg, severity in _STATIC_CHECKS:
|
|
125
|
+
if pattern.search(line):
|
|
126
|
+
issues.append({
|
|
127
|
+
"file": f.name,
|
|
128
|
+
"line": str(lineno),
|
|
129
|
+
"msg": msg,
|
|
130
|
+
"severity": severity,
|
|
131
|
+
})
|
|
132
|
+
break # one issue per line
|
|
133
|
+
return issues
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# ────────────────────────── renderer ────────────────────────────
|
|
137
|
+
|
|
138
|
+
_SEV_ORDER = {"error": 0, "warning": 1, "info": 2}
|
|
139
|
+
_SEV_LABEL = {"error": "ERROR", "warning": "WARN ", "info": "INFO "}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _render(
|
|
143
|
+
compile_errors: list[dict],
|
|
144
|
+
compile_warnings: list[dict],
|
|
145
|
+
static_issues: list[dict],
|
|
146
|
+
) -> list[str]:
|
|
147
|
+
out: list[str] = []
|
|
148
|
+
total = len(compile_errors) + len(compile_warnings) + len(static_issues)
|
|
149
|
+
|
|
150
|
+
if total == 0:
|
|
151
|
+
return ["✓ No issues found."]
|
|
152
|
+
|
|
153
|
+
# ── Compile errors
|
|
154
|
+
if compile_errors:
|
|
155
|
+
out.append(f"COMPILE ERRORS ({len(compile_errors)})")
|
|
156
|
+
out.append("─" * 60)
|
|
157
|
+
for e in compile_errors:
|
|
158
|
+
pad = max(0, 30 - len(e["file"]))
|
|
159
|
+
out.append(f" {e['file']}{' '*pad} line {e['line']:<6} {e['msg']}")
|
|
160
|
+
out.append("")
|
|
161
|
+
|
|
162
|
+
# ── Compile warnings
|
|
163
|
+
if compile_warnings:
|
|
164
|
+
out.append(f"COMPILE WARNINGS ({len(compile_warnings)})")
|
|
165
|
+
out.append("─" * 60)
|
|
166
|
+
for w in compile_warnings:
|
|
167
|
+
pad = max(0, 30 - len(w["file"]))
|
|
168
|
+
out.append(f" {w['file']}{' '*pad} line {w['line']:<6} {w['msg']}")
|
|
169
|
+
out.append("")
|
|
170
|
+
|
|
171
|
+
# ── Static issues grouped by severity
|
|
172
|
+
if static_issues:
|
|
173
|
+
static_issues.sort(key=lambda x: (_SEV_ORDER.get(x["severity"], 9), x["file"], int(x["line"])))
|
|
174
|
+
by_sev: dict[str, list[dict]] = {}
|
|
175
|
+
for i in static_issues:
|
|
176
|
+
by_sev.setdefault(i["severity"], []).append(i)
|
|
177
|
+
for sev in ("error", "warning", "info"):
|
|
178
|
+
group = by_sev.get(sev, [])
|
|
179
|
+
if not group:
|
|
180
|
+
continue
|
|
181
|
+
label = {"error": "STATIC ERRORS", "warning": "STATIC WARNINGS", "info": "STATIC INFO"}[sev]
|
|
182
|
+
out.append(f"{label} ({len(group)})")
|
|
183
|
+
out.append("─" * 60)
|
|
184
|
+
for i in group:
|
|
185
|
+
pad = max(0, 30 - len(i["file"]))
|
|
186
|
+
out.append(f" {i['file']}{' '*pad} line {i['line']:<6} {i['msg']}")
|
|
187
|
+
out.append("")
|
|
188
|
+
|
|
189
|
+
out.append(f"{'─'*60}")
|
|
190
|
+
out.append(f" Total: {len(compile_errors)} compile error(s), {len(compile_warnings)} compile warning(s), {len(static_issues)} static issue(s)")
|
|
191
|
+
return out
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# ─────────────────────────── command ────────────────────────────
|
|
195
|
+
|
|
196
|
+
def register_errors(app: typer.Typer) -> None:
|
|
197
|
+
@app.command("errors", help="Show compile errors + static analysis issues.")
|
|
198
|
+
def errors(
|
|
199
|
+
path: str = typer.Argument(".", help="Project root folder"),
|
|
200
|
+
changed_only: bool = typer.Option(False, "--changed-only", "-c", help="Only scan files changed since last aja scan"),
|
|
201
|
+
skip_compile: bool = typer.Option(False, "--skip-compile", help="Skip mvn compile, only run static analysis"),
|
|
202
|
+
skip_static: bool = typer.Option(False, "--skip-static", help="Skip static analysis, only run mvn compile"),
|
|
203
|
+
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show INFO level issues"),
|
|
204
|
+
) -> None:
|
|
205
|
+
t0 = time.perf_counter()
|
|
206
|
+
project_root = find_project_root(Path(path).resolve())
|
|
207
|
+
|
|
208
|
+
# Collect files for static analysis
|
|
209
|
+
if changed_only:
|
|
210
|
+
java_files = _changed_files(project_root)
|
|
211
|
+
typer.echo(f"Scanning {len(java_files)} changed file(s)...")
|
|
212
|
+
else:
|
|
213
|
+
java_files = [
|
|
214
|
+
f for f in find_all_supported_files(project_root)
|
|
215
|
+
if f.suffix.lower() == ".java"
|
|
216
|
+
]
|
|
217
|
+
typer.echo(f"Scanning {len(java_files)} Java file(s)...")
|
|
218
|
+
typer.echo("")
|
|
219
|
+
|
|
220
|
+
compile_errors: list[dict] = []
|
|
221
|
+
compile_warnings: list[dict] = []
|
|
222
|
+
static_issues: list[dict] = []
|
|
223
|
+
|
|
224
|
+
if not skip_compile:
|
|
225
|
+
typer.echo("Running mvn compile...")
|
|
226
|
+
compile_errors, compile_warnings = _run_mvn_compile(project_root)
|
|
227
|
+
|
|
228
|
+
if not skip_static:
|
|
229
|
+
static_issues = _static_analyze(java_files)
|
|
230
|
+
if not verbose:
|
|
231
|
+
static_issues = [i for i in static_issues if i["severity"] != "info"]
|
|
232
|
+
|
|
233
|
+
for line in _render(compile_errors, compile_warnings, static_issues):
|
|
234
|
+
typer.echo(line)
|
|
235
|
+
|
|
236
|
+
elapsed = time.perf_counter() - t0
|
|
237
|
+
typer.echo(f"\nFinished in {elapsed:.3f}s")
|
|
238
|
+
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from collections import deque
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import os, subprocess, sys
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from ...errors import InvalidPathError
|
|
8
|
+
from ...core.resolve_model_target import resolve_model_target_file as _resolve_model_target_file
|
|
9
|
+
from ...core.resolve_model_target import _is_java_model_file
|
|
10
|
+
from ...lang.java.resolve import resolve_java_import_to_file
|
|
11
|
+
from ...graph.builder import build_graph_with_counts, get_hub_files_by_ratio
|
|
12
|
+
from ...context.java_rel import clean_java, top_type_name, java_fields_and_rels, model_fields_from_extractor, Rel
|
|
13
|
+
from ...db.cache import CacheManager
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _get_cached_files(project_root: Path) -> list[Path]:
|
|
17
|
+
with CacheManager(project_root) as cache:
|
|
18
|
+
if cache.needs_rescan():
|
|
19
|
+
cache.scan_project(verbose=False)
|
|
20
|
+
return [p.resolve() for p in cache.get_cached_files()]
|
|
21
|
+
|
|
22
|
+
def _fmt_fields(fields, indent=" ") -> list[str]:
|
|
23
|
+
return [f"{indent}- {n}: {t}" + (" (PK)" if pk else "") for n, t, pk in fields]
|
|
24
|
+
|
|
25
|
+
def _rel_label(kind: str) -> str:
|
|
26
|
+
return {"OneToOne":"1:1","OneToMany":"1:N","ManyToMany":"N:M","ManyToOne":"N:1"}.get(kind, kind)
|
|
27
|
+
|
|
28
|
+
def _rel_meta(r: Rel) -> str:
|
|
29
|
+
parts = []
|
|
30
|
+
if r.mapped_by: parts.append(f"mappedBy={r.mapped_by}")
|
|
31
|
+
if r.join_table: parts.append(f"joinTable={r.join_table}")
|
|
32
|
+
if r.join_columns: parts.append("joinCols="+",".join(r.join_columns))
|
|
33
|
+
return (" ["+"; ".join(parts)+"]") if parts else ""
|
|
34
|
+
|
|
35
|
+
def _default_outfile(root: Path, name: str) -> Path:
|
|
36
|
+
base = root / f"codeintel-modeltree-{name}.txt"
|
|
37
|
+
if not base.exists(): return base
|
|
38
|
+
i = 2
|
|
39
|
+
while True:
|
|
40
|
+
p = root / f"codeintel-modeltree-{name}-{i}.txt"
|
|
41
|
+
if not p.exists(): return p
|
|
42
|
+
i += 1
|
|
43
|
+
|
|
44
|
+
def _open_in_editor(path: Path) -> None:
|
|
45
|
+
p = path.resolve()
|
|
46
|
+
try:
|
|
47
|
+
from ...core.opener import open_file
|
|
48
|
+
open_file(p); return
|
|
49
|
+
except Exception: pass
|
|
50
|
+
try:
|
|
51
|
+
subprocess.Popen(["code", str(p)], close_fds=True); return
|
|
52
|
+
except Exception: pass
|
|
53
|
+
try:
|
|
54
|
+
if sys.platform.startswith("win"):
|
|
55
|
+
subprocess.Popen(["cmd","/c","start","",str(p)], close_fds=True)
|
|
56
|
+
except Exception: pass
|
|
57
|
+
|
|
58
|
+
def _java_model_scope_from_files(files: list[Path]) -> list[Path]:
|
|
59
|
+
return [p.resolve() for p in files if _is_java_model_file(p)]
|
|
60
|
+
|
|
61
|
+
def _layered_related(graph, start, depth, include_reverse, hubs):
|
|
62
|
+
adj = {k: set(v) for k, v in graph.items()}
|
|
63
|
+
if include_reverse:
|
|
64
|
+
rev = {}
|
|
65
|
+
for src, deps in adj.items():
|
|
66
|
+
for dst in deps:
|
|
67
|
+
rev.setdefault(dst, set()).add(src)
|
|
68
|
+
for node, incoming in rev.items():
|
|
69
|
+
adj.setdefault(node, set()).update(incoming)
|
|
70
|
+
visited = {start}
|
|
71
|
+
q = deque([(start, 0)])
|
|
72
|
+
by_depth = {}
|
|
73
|
+
while q:
|
|
74
|
+
node, d = q.popleft()
|
|
75
|
+
if d == depth: continue
|
|
76
|
+
for nxt in adj.get(node, set()):
|
|
77
|
+
if nxt in visited or nxt in hubs: continue
|
|
78
|
+
visited.add(nxt)
|
|
79
|
+
nd = d + 1
|
|
80
|
+
by_depth.setdefault(nd, []).append(nxt)
|
|
81
|
+
q.append((nxt, nd))
|
|
82
|
+
return [sorted(by_depth.get(i, [])) for i in range(1, depth+1)]
|
|
83
|
+
|
|
84
|
+
def _file_for_java_class(name, scope, root):
|
|
85
|
+
if not name: return None
|
|
86
|
+
exact = [p for p in scope if p.suffix.lower()==".java" and p.stem==name]
|
|
87
|
+
if exact: return exact[0].resolve()
|
|
88
|
+
try:
|
|
89
|
+
p = resolve_java_import_to_file(name, root)
|
|
90
|
+
if p and p.exists(): return p.resolve()
|
|
91
|
+
except Exception: pass
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
def _collect_java_relationship_map(target, project_root, depth, forward_only, include_hubs):
|
|
95
|
+
all_files = _get_cached_files(project_root)
|
|
96
|
+
graph, dep_counts = build_graph_with_counts(all_files, project_root, use_sqlite_cache=True)
|
|
97
|
+
hubs = set()
|
|
98
|
+
if not include_hubs:
|
|
99
|
+
hubs = get_hub_files_by_ratio(dep_counts, len(all_files), 0.5)
|
|
100
|
+
layers = _layered_related(graph, target.resolve(), depth, not forward_only, hubs)
|
|
101
|
+
model_files = set(_java_model_scope_from_files(all_files))
|
|
102
|
+
scope = [rp for layer in layers for p in layer if (rp := p.resolve()) in model_files]
|
|
103
|
+
try:
|
|
104
|
+
text = clean_java(target.read_text(encoding="utf-8", errors="ignore"))
|
|
105
|
+
except Exception:
|
|
106
|
+
text = ""
|
|
107
|
+
model_name = top_type_name(text, target.stem)
|
|
108
|
+
main_fields, main_rels = java_fields_and_rels(target)
|
|
109
|
+
fields_by_entity = {}
|
|
110
|
+
for r in main_rels:
|
|
111
|
+
if not r or not r.target: continue
|
|
112
|
+
p = _file_for_java_class(r.target, scope+[target], project_root)
|
|
113
|
+
if p and p.exists():
|
|
114
|
+
flds = model_fields_from_extractor(p, project_root) or []
|
|
115
|
+
if not flds:
|
|
116
|
+
flds, _ = java_fields_and_rels(p)
|
|
117
|
+
if flds:
|
|
118
|
+
fields_by_entity[r.target] = flds
|
|
119
|
+
# Cached reverse scan — build once per project_root per process
|
|
120
|
+
import functools
|
|
121
|
+
cache_key = str(project_root.resolve())
|
|
122
|
+
if not hasattr(_collect_java_relationship_map, "_rev_cache"):
|
|
123
|
+
_collect_java_relationship_map._rev_cache = {}
|
|
124
|
+
if cache_key not in _collect_java_relationship_map._rev_cache:
|
|
125
|
+
rev_map: dict[str, list[tuple]] = {}
|
|
126
|
+
rev_fields: dict[str, list] = {}
|
|
127
|
+
for p in model_files:
|
|
128
|
+
try:
|
|
129
|
+
t = clean_java(p.read_text(encoding="utf-8", errors="ignore"))
|
|
130
|
+
except Exception:
|
|
131
|
+
continue
|
|
132
|
+
src_name = top_type_name(t, p.stem)
|
|
133
|
+
_, rels = java_fields_and_rels(p)
|
|
134
|
+
for rr in rels:
|
|
135
|
+
if not rr or not rr.target:
|
|
136
|
+
continue
|
|
137
|
+
rev_map.setdefault(rr.target, []).append((src_name, rr))
|
|
138
|
+
if src_name not in rev_fields:
|
|
139
|
+
flds = model_fields_from_extractor(p, project_root) or []
|
|
140
|
+
if not flds:
|
|
141
|
+
flds, _ = java_fields_and_rels(p)
|
|
142
|
+
if flds:
|
|
143
|
+
rev_fields[src_name] = flds
|
|
144
|
+
_collect_java_relationship_map._rev_cache[cache_key] = (rev_map, rev_fields)
|
|
145
|
+
rev_map, rev_fields = _collect_java_relationship_map._rev_cache[cache_key]
|
|
146
|
+
reverse = sorted(rev_map.get(model_name, []), key=lambda x: (x[0].lower(), x[1].kind, x[1].field.lower()))
|
|
147
|
+
for src_name, _ in reverse:
|
|
148
|
+
if src_name not in fields_by_entity and src_name in rev_fields:
|
|
149
|
+
fields_by_entity[src_name] = rev_fields[src_name]
|
|
150
|
+
return model_name, main_fields, main_rels, fields_by_entity, reverse
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _view_tree(model, fields, rels, fields_by, reverse):
|
|
154
|
+
lines = [model, "+- Fields"]
|
|
155
|
+
for n,t,pk in fields:
|
|
156
|
+
lines.append(f"| +- {n}: {t}" + (" (PK)" if pk else ""))
|
|
157
|
+
lines.append("|")
|
|
158
|
+
groups = {}
|
|
159
|
+
for r in rels:
|
|
160
|
+
groups.setdefault(r.kind, []).append(r)
|
|
161
|
+
order = ["OneToOne","OneToMany","ManyToMany","ManyToOne"]
|
|
162
|
+
kinds = [k for k in order if k in groups]
|
|
163
|
+
if not kinds:
|
|
164
|
+
lines += ["+- Relationships", " +- (none)"]
|
|
165
|
+
else:
|
|
166
|
+
for ki, kind in enumerate(kinds):
|
|
167
|
+
kpad = " " if ki==len(kinds)-1 else "| "
|
|
168
|
+
lines.append(f"+- @{kind}")
|
|
169
|
+
for ri, r in enumerate(groups[kind]):
|
|
170
|
+
rlast = ri==len(groups[kind])-1
|
|
171
|
+
suffix = "[]" if kind in {"OneToMany","ManyToMany"} else ""
|
|
172
|
+
lines.append(f"{kpad}+- {r.target}{suffix} (field: {r.field}){_rel_meta(r)}")
|
|
173
|
+
for fn,ft,_ in fields_by.get(r.target,[]):
|
|
174
|
+
lines.append(f"{kpad}{' ' if rlast else '| '} +- {fn}: {ft}")
|
|
175
|
+
lines += ["","Reverse References:"]
|
|
176
|
+
lines += [f" <- {s} (@{r.kind} via {r.field}){_rel_meta(r)}" for s,r in reverse] or [" (none)"]
|
|
177
|
+
return lines
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def register_modeltree(app: typer.Typer) -> None:
|
|
181
|
+
@app.command(help="Model relationship tree")
|
|
182
|
+
def modeltree(
|
|
183
|
+
model: str = typer.Argument(...),
|
|
184
|
+
root: str = typer.Option("."),
|
|
185
|
+
depth: int = typer.Option(2, "--depth", "-d"),
|
|
186
|
+
forward_only: bool = typer.Option(False, "--forward-only"),
|
|
187
|
+
include_hubs: bool = typer.Option(False, "--include-hubs"),
|
|
188
|
+
out: Path | None = typer.Option(None, "--out"),
|
|
189
|
+
) -> None:
|
|
190
|
+
auto = os.environ.get("AJA_AUTO") == "1"
|
|
191
|
+
target, project_root = _resolve_model_target_file(model, root=root)
|
|
192
|
+
|
|
193
|
+
if target.suffix.lower() != ".java":
|
|
194
|
+
raise InvalidPathError(message="Java-only mode: only .java model files supported", path=target)
|
|
195
|
+
if not _is_java_model_file(target):
|
|
196
|
+
raise InvalidPathError(message="Not a model/entity file", path=target)
|
|
197
|
+
|
|
198
|
+
model_name, fields, rels, fields_by, reverse = _collect_java_relationship_map(
|
|
199
|
+
target, project_root, depth, forward_only, include_hubs
|
|
200
|
+
)
|
|
201
|
+
lines = _view_tree(model_name, fields, rels, fields_by, reverse)
|
|
202
|
+
|
|
203
|
+
# Always print to terminal
|
|
204
|
+
typer.echo("")
|
|
205
|
+
for ln in lines:
|
|
206
|
+
typer.echo(ln)
|
|
207
|
+
|
|
208
|
+
# Ask user if they want to save (skip in auto mode)
|
|
209
|
+
save = False if auto else typer.confirm("\nSave to file?", default=False)
|
|
210
|
+
if save:
|
|
211
|
+
out_path = out.resolve() if out else _default_outfile(project_root, model_name)
|
|
212
|
+
out_path.write_text("\n".join(lines)+"\n", encoding="utf-8")
|
|
213
|
+
typer.echo(out_path.as_posix())
|
|
214
|
+
_open_in_editor(out_path)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
|