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.
Files changed (92) hide show
  1. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/PKG-INFO +1 -1
  2. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/aja_codeintel.egg-info/PKG-INFO +1 -1
  3. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/aja_codeintel.egg-info/SOURCES.txt +2 -0
  4. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/commands/project/__init__.py +4 -2
  5. aja_codeintel-0.1.6/codeintel_cli/commands/project/errors_cmd.py +238 -0
  6. aja_codeintel-0.1.6/codeintel_cli/commands/project/modeltree_cmd.py +217 -0
  7. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/commands/project/overview_cmd.py +61 -203
  8. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/commands/project/scan_cmd.py +46 -51
  9. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/commands/project/servicemap_cmd.py +173 -180
  10. aja_codeintel-0.1.6/codeintel_cli/commands/project/types_cmd.py +424 -0
  11. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/context/java_rel.py +330 -368
  12. aja_codeintel-0.1.6/codeintel_cli/context/java_service.py +226 -0
  13. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/core/fuzzy.py +72 -72
  14. aja_codeintel-0.1.6/codeintel_cli/core/resolve_model_target.py +135 -0
  15. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/core/resolve_target.py +59 -53
  16. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/db/cache.py +4 -5
  17. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/db/operations.py +54 -180
  18. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/graph/builder.py +148 -148
  19. aja_codeintel-0.1.6/codeintel_cli/lang/java/ast_engine.py +98 -0
  20. aja_codeintel-0.1.6/codeintel_cli/scanner/scanner.py +50 -0
  21. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/pyproject.toml +1 -1
  22. aja_codeintel-0.1.5/codeintel_cli/commands/project/modeltree_cmd.py +0 -476
  23. aja_codeintel-0.1.5/codeintel_cli/commands/project/types_cmd.py +0 -318
  24. aja_codeintel-0.1.5/codeintel_cli/context/java_service.py +0 -291
  25. aja_codeintel-0.1.5/codeintel_cli/core/resolve_model_target.py +0 -92
  26. aja_codeintel-0.1.5/codeintel_cli/scanner/scanner.py +0 -41
  27. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/LICENSE +0 -0
  28. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/README.md +0 -0
  29. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/aja_codeintel.egg-info/dependency_links.txt +0 -0
  30. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/aja_codeintel.egg-info/entry_points.txt +0 -0
  31. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/aja_codeintel.egg-info/requires.txt +0 -0
  32. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/aja_codeintel.egg-info/top_level.txt +0 -0
  33. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/__init__.py +0 -0
  34. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/__main__.py +0 -0
  35. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/cli.py +0 -0
  36. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/commands/__init__.py +0 -0
  37. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/commands/graph/__init__.py +0 -0
  38. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/commands/graph/deps_cmd.py +0 -0
  39. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/commands/graph/related_cmd.py +0 -0
  40. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/commands/graph/relsymbols_cmd.py +0 -0
  41. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/commands/graph/reverse_related_cmd.py +0 -0
  42. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/commands/nav/__init__.py +0 -0
  43. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/commands/nav/copy_cmd.py +0 -0
  44. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/commands/nav/open_cmd.py +0 -0
  45. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/commands/nav/where_cmd.py +0 -0
  46. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/commands/project/context_cmd.py +0 -0
  47. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/commands/project/endpoints_cmd.py +0 -0
  48. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/commands/project/folder_cmd.py +0 -0
  49. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/commands/project/frontend_cmd.py +0 -0
  50. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/commands/project/imports_cmd.py +0 -0
  51. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/commands/project/models_cmd.py +0 -0
  52. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/commands/project/new.py +0 -0
  53. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/commands/project/resolve_cmd.py +0 -0
  54. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/commands/project/tree_cmd.py +0 -0
  55. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/commands/project/version_cmd.py +0 -0
  56. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/context/java_context.py +0 -0
  57. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/context/python_context.py +0 -0
  58. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/context/python_rel.py +0 -0
  59. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/context/python_service.py +0 -0
  60. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/core/opener.py +0 -0
  61. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/core/project.py +0 -0
  62. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/core/resolve_folder.py +0 -0
  63. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/core/timing.py +0 -0
  64. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/core/where.py +0 -0
  65. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/db/__init__.py +0 -0
  66. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/db/schema.py +0 -0
  67. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/endpoints/__init__.py +0 -0
  68. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/endpoints/java_spring.py +0 -0
  69. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/endpoints/models.py +0 -0
  70. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/endpoints/openapi_spec.py +0 -0
  71. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/endpoints/python_web.py +0 -0
  72. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/endpoints/scan.py +0 -0
  73. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/errors.py +0 -0
  74. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/frontend/__init__.py +0 -0
  75. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/frontend/server.py +0 -0
  76. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/graph/__init__.py +0 -0
  77. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/graph/query.py +0 -0
  78. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/graph/traverse.py +0 -0
  79. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/lang/__init__.py +0 -0
  80. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/lang/java/__init__.py +0 -0
  81. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/lang/java/engine.py +0 -0
  82. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/lang/java/models.py +0 -0
  83. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/lang/java/resolve.py +0 -0
  84. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/lang/python/__init__.py +0 -0
  85. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/lang/python/engine.py +0 -0
  86. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/lang/python/models.py +0 -0
  87. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/lang/router.py +0 -0
  88. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/parser/imports.py +0 -0
  89. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/parser/resolve.py +0 -0
  90. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/parser/symbols.py +0 -0
  91. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/codeintel_cli/scanner/__init__.py +0 -0
  92. {aja_codeintel-0.1.5 → aja_codeintel-0.1.6}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aja-codeintel
3
- Version: 0.1.5
3
+ Version: 0.1.6
4
4
  Summary: CodeIntel CLI tool
5
5
  Author: Shiva Areti
6
6
  License: MIT License
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aja-codeintel
3
- Version: 0.1.5
3
+ Version: 0.1.6
4
4
  Summary: CodeIntel CLI tool
5
5
  Author: Shiva Areti
6
6
  License: MIT License
@@ -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
+