codeui-python 0.1.0__py3-none-any.whl → 0.1.1__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.
- codeui/__init__.py +1 -1
- codeui/agent/context.py +19 -3
- codeui/analysis/dead_code.py +13 -2
- codeui/analysis/undefined.py +70 -0
- codeui/analysis/unresolved_import.py +34 -4
- codeui/cli/main.py +101 -7
- codeui/core/cache.py +54 -0
- codeui/core/graph.py +233 -3
- codeui/core/override.py +1 -0
- codeui/core/resolver.py +51 -37
- codeui/errors.py +10 -0
- codeui/lang/base.py +5 -2
- codeui/lang/cpp.py +271 -0
- codeui/lang/csharp.py +262 -0
- codeui/lang/dart.py +278 -0
- codeui/lang/generic.py +6 -4
- codeui/lang/go.py +5 -2
- codeui/lang/java.py +320 -0
- codeui/lang/kotlin.py +303 -0
- codeui/lang/php.py +275 -0
- codeui/lang/python.py +49 -7
- codeui/lang/registry.py +22 -2
- codeui/lang/ruby.py +270 -0
- codeui/lang/rust.py +5 -2
- codeui/lang/scala.py +290 -0
- codeui/lang/swift.py +276 -0
- codeui/lang/ts.py +99 -8
- codeui/report/sarif_emitter.py +2 -1
- codeui/server/server.py +156 -85
- codeui/server/static/css/styles.css +1609 -0
- codeui/server/static/index.html +199 -0
- codeui/server/static/js/api.js +485 -0
- codeui/server/static/js/app.js +1044 -0
- codeui/server/static/js/graph_canvas.js +1485 -0
- codeui/server/static/js/minimap.js +383 -0
- codeui/server/static/js/sidebar.js +268 -0
- codeui/server/static/js/state.js +83 -0
- codeui/server/static/js/subgraph_window.js +1208 -0
- codeui/server/static/js/theme.js +332 -0
- codeui/tracer/__init__.py +4 -1
- codeui/tracer/tracer.py +109 -118
- codeui_python-0.1.1.dist-info/METADATA +171 -0
- codeui_python-0.1.1.dist-info/RECORD +71 -0
- {codeui_python-0.1.0.dist-info → codeui_python-0.1.1.dist-info}/licenses/LICENSE +1 -1
- codeui_python-0.1.0.dist-info/METADATA +0 -191
- codeui_python-0.1.0.dist-info/RECORD +0 -52
- {codeui_python-0.1.0.dist-info → codeui_python-0.1.1.dist-info}/WHEEL +0 -0
- {codeui_python-0.1.0.dist-info → codeui_python-0.1.1.dist-info}/entry_points.txt +0 -0
- {codeui_python-0.1.0.dist-info → codeui_python-0.1.1.dist-info}/top_level.txt +0 -0
codeui/lang/dart.py
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"""Dart language adapter for codeui."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import hashlib
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import ClassVar, Iterable, List, Sequence, TYPE_CHECKING
|
|
7
|
+
from codeui.core.ir import Edge, EdgeKind, Location, Symbol, SymbolKind, Visibility
|
|
8
|
+
from codeui.lang.base import ImportRef, LanguageAnalyzer, ParseResult
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from codeui.core.resolver import ResolveContext
|
|
12
|
+
|
|
13
|
+
class DartLanguageAnalyzer(LanguageAnalyzer):
|
|
14
|
+
"""Dart language adapter extracting classes, mixins, enums, functions, and properties.
|
|
15
|
+
Example:
|
|
16
|
+
>>> analyzer = DartLanguageAnalyzer()
|
|
17
|
+
>>> res = analyzer.parse(Path("main.dart"), "class User { final int id; User(this.id); }")
|
|
18
|
+
>>> syms = list(analyzer.extract_symbols(res))
|
|
19
|
+
>>> any(s.kind == SymbolKind.CLASS for s in syms)
|
|
20
|
+
True
|
|
21
|
+
"""
|
|
22
|
+
language: ClassVar[str] = "dart"
|
|
23
|
+
extensions: ClassVar[tuple[str, ...]] = (".dart",)
|
|
24
|
+
|
|
25
|
+
def parse(self, path: Path, source: str) -> ParseResult:
|
|
26
|
+
"""Parse Dart source into AST-emulating representation.
|
|
27
|
+
Example:
|
|
28
|
+
>>> analyzer = DartLanguageAnalyzer()
|
|
29
|
+
>>> res = analyzer.parse(Path("app.dart"), "void main() {}")
|
|
30
|
+
>>> res.errors
|
|
31
|
+
[]
|
|
32
|
+
"""
|
|
33
|
+
content_hash = hashlib.sha256(source.encode("utf-8")).hexdigest()
|
|
34
|
+
return ParseResult(
|
|
35
|
+
file_path=str(path),
|
|
36
|
+
source=source,
|
|
37
|
+
ast=source,
|
|
38
|
+
content_hash=content_hash,
|
|
39
|
+
errors=[],
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
def extract_symbols(self, parse: ParseResult) -> Iterable[Symbol]:
|
|
43
|
+
"""Extract Dart types, functions, methods, properties, and imports.
|
|
44
|
+
Example:
|
|
45
|
+
>>> analyzer = DartLanguageAnalyzer()
|
|
46
|
+
>>> res = analyzer.parse(Path("service.dart"), "class Service { Future<void> init() async {} }")
|
|
47
|
+
>>> syms = list(analyzer.extract_symbols(res))
|
|
48
|
+
>>> len(syms) >= 2
|
|
49
|
+
True
|
|
50
|
+
"""
|
|
51
|
+
source = parse.source
|
|
52
|
+
file_path = parse.file_path
|
|
53
|
+
lines = source.splitlines()
|
|
54
|
+
max_line = len(lines) if lines else 1
|
|
55
|
+
symbols: List[Symbol] = []
|
|
56
|
+
|
|
57
|
+
file_sym = Symbol(
|
|
58
|
+
id=file_path,
|
|
59
|
+
name=Path(file_path).name,
|
|
60
|
+
qualified_name=file_path,
|
|
61
|
+
kind=SymbolKind.FILE,
|
|
62
|
+
language=self.language,
|
|
63
|
+
location=Location(file_path, 1, 0, max_line, 0),
|
|
64
|
+
parent_id=None,
|
|
65
|
+
signature=None,
|
|
66
|
+
visibility=Visibility.PUBLIC,
|
|
67
|
+
modifiers=(),
|
|
68
|
+
content_hash=parse.content_hash,
|
|
69
|
+
)
|
|
70
|
+
symbols.append(file_sym)
|
|
71
|
+
|
|
72
|
+
import_pattern = re.compile(r'^\s*(?:import|export)\s+[\'"]([^\'"]+)[\'"](?:\s+as\s+([a-zA-Z0-9_]+))?')
|
|
73
|
+
type_pattern = re.compile(r'^\s*(?:(abstract|sealed|base|interface|final)\s+)?(class|mixin|enum|extension)\s+([a-zA-Z0-9_]+)(?:<[^>]+>)?(?:\s+extends\s+([a-zA-Z0-9_.,\s<>]+))?(?:\s+with\s+([a-zA-Z0-9_.,\s<>]+))?(?:\s+implements\s+([a-zA-Z0-9_.,\s<>]+))?')
|
|
74
|
+
func_pattern = re.compile(r'^\s*(?:(static|override|factory)\s+)*(?:([a-zA-Z0-9_?<>]+)\s+)?([a-zA-Z0-9_]+)\s*\(([^)]*)\)(?:\s*async\s*)?(?:\s*\{|\s*=>)')
|
|
75
|
+
prop_pattern = re.compile(r'^\s*(?:(static|final|const|late)\s+)*(?:([a-zA-Z0-9_?<>]+)\s+)?([a-zA-Z0-9_]+)(?:\s*=\s*[^;]+)?\s*;')
|
|
76
|
+
|
|
77
|
+
current_type: str | None = None
|
|
78
|
+
brace_depth = 0
|
|
79
|
+
type_depth = 0
|
|
80
|
+
|
|
81
|
+
for line_idx, line in enumerate(lines, start=1):
|
|
82
|
+
stripped = line.strip()
|
|
83
|
+
if not stripped or stripped.startswith("//") or stripped.startswith("/*") or stripped.startswith("*"):
|
|
84
|
+
continue
|
|
85
|
+
|
|
86
|
+
imp_match = import_pattern.match(line)
|
|
87
|
+
if imp_match:
|
|
88
|
+
imp_uri = imp_match.group(1)
|
|
89
|
+
alias = imp_match.group(2) or imp_uri.split("/")[-1].replace(".dart", "")
|
|
90
|
+
symbols.append(Symbol(
|
|
91
|
+
id=f"{file_path}::import::{imp_uri}",
|
|
92
|
+
name=alias,
|
|
93
|
+
qualified_name=imp_uri,
|
|
94
|
+
kind=SymbolKind.IMPORT,
|
|
95
|
+
language=self.language,
|
|
96
|
+
location=Location(file_path, line_idx, 0, line_idx, len(line)),
|
|
97
|
+
parent_id=file_path,
|
|
98
|
+
signature=f"import '{imp_uri}'",
|
|
99
|
+
visibility=Visibility.PUBLIC,
|
|
100
|
+
modifiers=(),
|
|
101
|
+
content_hash=hashlib.sha256(line.encode("utf-8")).hexdigest()[:16],
|
|
102
|
+
))
|
|
103
|
+
continue
|
|
104
|
+
|
|
105
|
+
type_match = type_pattern.match(line)
|
|
106
|
+
if type_match:
|
|
107
|
+
mod_str = type_match.group(1)
|
|
108
|
+
type_kind = type_match.group(2)
|
|
109
|
+
type_name = type_match.group(3)
|
|
110
|
+
vis = Visibility.PRIVATE if type_name.startswith("_") else Visibility.PUBLIC
|
|
111
|
+
kind = SymbolKind.INTERFACE if type_kind in ("mixin", "interface") else SymbolKind.CLASS
|
|
112
|
+
modifiers = tuple(filter(None, [mod_str, type_kind]))
|
|
113
|
+
sym_id = f"{file_path}::{type_name}"
|
|
114
|
+
current_type = sym_id
|
|
115
|
+
type_depth = brace_depth
|
|
116
|
+
|
|
117
|
+
symbols.append(Symbol(
|
|
118
|
+
id=sym_id,
|
|
119
|
+
name=type_name,
|
|
120
|
+
qualified_name=type_name,
|
|
121
|
+
kind=kind,
|
|
122
|
+
language=self.language,
|
|
123
|
+
location=Location(file_path, line_idx, 0, line_idx, len(line)),
|
|
124
|
+
parent_id=file_path,
|
|
125
|
+
signature=f"{type_kind} {type_name}",
|
|
126
|
+
visibility=vis,
|
|
127
|
+
modifiers=modifiers,
|
|
128
|
+
content_hash=hashlib.sha256(line.encode("utf-8")).hexdigest()[:16],
|
|
129
|
+
))
|
|
130
|
+
|
|
131
|
+
func_match = func_pattern.match(line)
|
|
132
|
+
if func_match:
|
|
133
|
+
func_mod = func_match.group(1)
|
|
134
|
+
func_ret = func_match.group(2) or "void"
|
|
135
|
+
func_name = func_match.group(3)
|
|
136
|
+
func_args = func_match.group(4) or ""
|
|
137
|
+
if func_name not in ("if", "for", "while", "switch", "catch"):
|
|
138
|
+
vis = Visibility.PRIVATE if func_name.startswith("_") else Visibility.PUBLIC
|
|
139
|
+
parent_id = current_type if current_type and brace_depth > type_depth else file_path
|
|
140
|
+
sym_id = f"{parent_id}::{func_name}"
|
|
141
|
+
symbols.append(Symbol(
|
|
142
|
+
id=sym_id,
|
|
143
|
+
name=func_name,
|
|
144
|
+
qualified_name=func_name,
|
|
145
|
+
kind=SymbolKind.FUNCTION,
|
|
146
|
+
language=self.language,
|
|
147
|
+
location=Location(file_path, line_idx, 0, line_idx, len(line)),
|
|
148
|
+
parent_id=parent_id,
|
|
149
|
+
signature=f"{func_ret} {func_name}({func_args})",
|
|
150
|
+
visibility=vis,
|
|
151
|
+
modifiers=tuple(filter(None, [func_mod])),
|
|
152
|
+
content_hash=hashlib.sha256(line.encode("utf-8")).hexdigest()[:16],
|
|
153
|
+
))
|
|
154
|
+
|
|
155
|
+
prop_match = prop_pattern.match(line)
|
|
156
|
+
if prop_match and not line.strip().startswith("return") and not line.strip().startswith("import"):
|
|
157
|
+
prop_mod = prop_match.group(1)
|
|
158
|
+
prop_type = prop_match.group(2) or "dynamic"
|
|
159
|
+
prop_name = prop_match.group(3)
|
|
160
|
+
if prop_name and prop_name not in ("return", "break", "continue", "throw"):
|
|
161
|
+
vis = Visibility.PRIVATE if prop_name.startswith("_") else Visibility.PUBLIC
|
|
162
|
+
parent_id = current_type if current_type and brace_depth > type_depth else file_path
|
|
163
|
+
sym_id = f"{parent_id}::{prop_name}"
|
|
164
|
+
symbols.append(Symbol(
|
|
165
|
+
id=sym_id,
|
|
166
|
+
name=prop_name,
|
|
167
|
+
qualified_name=prop_name,
|
|
168
|
+
kind=SymbolKind.VARIABLE,
|
|
169
|
+
language=self.language,
|
|
170
|
+
location=Location(file_path, line_idx, 0, line_idx, len(line)),
|
|
171
|
+
parent_id=parent_id,
|
|
172
|
+
signature=f"{prop_type} {prop_name}",
|
|
173
|
+
visibility=vis,
|
|
174
|
+
modifiers=tuple(filter(None, [prop_mod])),
|
|
175
|
+
content_hash=hashlib.sha256(line.encode("utf-8")).hexdigest()[:16],
|
|
176
|
+
))
|
|
177
|
+
|
|
178
|
+
brace_depth += line.count("{") - line.count("}")
|
|
179
|
+
if current_type and brace_depth <= type_depth:
|
|
180
|
+
current_type = None
|
|
181
|
+
|
|
182
|
+
return sorted(symbols, key=lambda s: (s.location.start_line if s.location else 0, s.name))
|
|
183
|
+
|
|
184
|
+
def extract_edges(self, parse: ParseResult, symbols: Sequence[Symbol]) -> Iterable[Edge]:
|
|
185
|
+
"""Extract containment, inheritance, mixin, and import edges for Dart.
|
|
186
|
+
Example:
|
|
187
|
+
>>> analyzer = DartLanguageAnalyzer()
|
|
188
|
+
>>> res = analyzer.parse(Path("App.dart"), "class Child extends Parent with Logger implements IService {}")
|
|
189
|
+
>>> syms = list(analyzer.extract_symbols(res))
|
|
190
|
+
>>> edges = list(analyzer.extract_edges(res, syms))
|
|
191
|
+
>>> len(edges) >= 1
|
|
192
|
+
True
|
|
193
|
+
"""
|
|
194
|
+
edges: List[Edge] = []
|
|
195
|
+
file_path = parse.file_path
|
|
196
|
+
symbol_map = {s.name: s for s in symbols}
|
|
197
|
+
type_inherit_pattern = re.compile(r'^\s*(?:[a-zA-Z0-9_\s<>]+)?(?:class|mixin|enum|extension)\s+([a-zA-Z0-9_]+)(?:<[^>]+>)?(?:\s+extends\s+([a-zA-Z0-9_.,\s<>]+))?(?:\s+with\s+([a-zA-Z0-9_.,\s<>]+))?(?:\s+implements\s+([a-zA-Z0-9_.,\s<>]+))?')
|
|
198
|
+
|
|
199
|
+
for s in symbols:
|
|
200
|
+
if s.parent_id and s.parent_id != s.id:
|
|
201
|
+
edges.append(Edge(
|
|
202
|
+
source_id=s.parent_id,
|
|
203
|
+
target_id=s.id,
|
|
204
|
+
kind=EdgeKind.CONTAINS,
|
|
205
|
+
weight=1.0,
|
|
206
|
+
confidence=1.0,
|
|
207
|
+
location=s.location,
|
|
208
|
+
))
|
|
209
|
+
|
|
210
|
+
for line_idx, line in enumerate(parse.source.splitlines(), start=1):
|
|
211
|
+
type_match = type_inherit_pattern.match(line)
|
|
212
|
+
if type_match:
|
|
213
|
+
type_name = type_match.group(1)
|
|
214
|
+
extends_raw = type_match.group(2)
|
|
215
|
+
with_raw = type_match.group(3)
|
|
216
|
+
implements_raw = type_match.group(4)
|
|
217
|
+
type_sym = symbol_map.get(type_name)
|
|
218
|
+
if type_sym:
|
|
219
|
+
super_candidates = []
|
|
220
|
+
if extends_raw:
|
|
221
|
+
super_candidates.append(extends_raw.strip().split("<")[0].strip())
|
|
222
|
+
if with_raw:
|
|
223
|
+
for w in with_raw.split(","):
|
|
224
|
+
clean_w = w.strip().split("<")[0].strip()
|
|
225
|
+
if clean_w:
|
|
226
|
+
super_candidates.append(clean_w)
|
|
227
|
+
if implements_raw:
|
|
228
|
+
for imp in implements_raw.split(","):
|
|
229
|
+
clean_imp = imp.strip().split("<")[0].strip()
|
|
230
|
+
if clean_imp:
|
|
231
|
+
super_candidates.append(clean_imp)
|
|
232
|
+
for sup in super_candidates:
|
|
233
|
+
if sup and sup not in ("Object", "Widget", "StatelessWidget", "StatefulWidget"):
|
|
234
|
+
target_id = symbol_map[sup].id if sup in symbol_map else sup
|
|
235
|
+
edges.append(Edge(
|
|
236
|
+
source_id=type_sym.id,
|
|
237
|
+
target_id=target_id,
|
|
238
|
+
kind=EdgeKind.INHERITS,
|
|
239
|
+
weight=1.0,
|
|
240
|
+
confidence=0.9,
|
|
241
|
+
location=Location(file_path, line_idx, 0, line_idx, len(line)),
|
|
242
|
+
))
|
|
243
|
+
|
|
244
|
+
if line.strip().startswith("import ") or line.strip().startswith("export "):
|
|
245
|
+
imp_match = re.search(r'[\'"]([^\'"]+)[\'"]', line)
|
|
246
|
+
if imp_match:
|
|
247
|
+
target_uri = imp_match.group(1)
|
|
248
|
+
edges.append(Edge(
|
|
249
|
+
source_id=file_path,
|
|
250
|
+
target_id=target_uri,
|
|
251
|
+
kind=EdgeKind.IMPORTS,
|
|
252
|
+
weight=1.0,
|
|
253
|
+
confidence=1.0,
|
|
254
|
+
location=Location(file_path, line_idx, 0, line_idx, len(line)),
|
|
255
|
+
))
|
|
256
|
+
|
|
257
|
+
return sorted(edges, key=lambda e: (e.source_id, e.target_id, e.kind.value))
|
|
258
|
+
|
|
259
|
+
def resolve_import(self, ref: ImportRef, ctx: ResolveContext) -> Iterable[str]:
|
|
260
|
+
"""Resolve Dart imports (relative or package:) against workspace files.
|
|
261
|
+
Example:
|
|
262
|
+
>>> analyzer = DartLanguageAnalyzer()
|
|
263
|
+
>>> from codeui.core.resolver import ResolveContext
|
|
264
|
+
>>> ctx = ResolveContext(Path("."))
|
|
265
|
+
>>> ref = ImportRef("lib/main.dart", "package:app/service.dart", None, False)
|
|
266
|
+
>>> list(analyzer.resolve_import(ref, ctx))
|
|
267
|
+
[]
|
|
268
|
+
"""
|
|
269
|
+
uri = ref.module_name
|
|
270
|
+
if uri.startswith("package:"):
|
|
271
|
+
parts = uri[len("package:"):].split("/", 1)
|
|
272
|
+
if len(parts) > 1:
|
|
273
|
+
pkg_sub = f"lib/{parts[1]}"
|
|
274
|
+
cands = ctx.resolve_path(ref.from_file, pkg_sub, "dart")
|
|
275
|
+
if cands:
|
|
276
|
+
return cands
|
|
277
|
+
return ctx.resolve_path(ref.from_file, parts[1], "dart")
|
|
278
|
+
return ctx.resolve_path(ref.from_file, uri, "dart")
|
codeui/lang/generic.py
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
"""Generic markup, config, and fallback language analyzer for codeui."""
|
|
2
|
+
from __future__ import annotations
|
|
2
3
|
import hashlib
|
|
3
4
|
from pathlib import Path
|
|
4
|
-
from typing import ClassVar, Iterable, List, Sequence
|
|
5
|
+
from typing import ClassVar, Iterable, List, Sequence, TYPE_CHECKING
|
|
5
6
|
from codeui.core.ir import Edge, EdgeKind, Location, Symbol, SymbolKind, Visibility
|
|
6
|
-
from codeui.core.resolver import ResolveContext
|
|
7
7
|
from codeui.lang.base import ImportRef, LanguageAnalyzer, ParseResult
|
|
8
8
|
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from codeui.core.resolver import ResolveContext
|
|
11
|
+
|
|
9
12
|
class GenericLanguageAnalyzer(LanguageAnalyzer):
|
|
10
13
|
"""Fallback analyzer for configuration, markup, and generic source files.
|
|
11
14
|
Example:
|
|
@@ -19,8 +22,7 @@ class GenericLanguageAnalyzer(LanguageAnalyzer):
|
|
|
19
22
|
has_ast_support: ClassVar[bool] = False
|
|
20
23
|
is_supported: ClassVar[bool] = False
|
|
21
24
|
extensions: ClassVar[tuple[str, ...]] = (
|
|
22
|
-
".
|
|
23
|
-
".c", ".cpp", ".cc", ".cxx", ".h", ".hpp", ".m", ".mm", ".scala"
|
|
25
|
+
".vue", ".svelte", ".m", ".mm", ".xml", ".html"
|
|
24
26
|
)
|
|
25
27
|
|
|
26
28
|
def parse(self, path: Path, source: str) -> ParseResult:
|
codeui/lang/go.py
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
"""Go language adapter for codeui."""
|
|
2
|
+
from __future__ import annotations
|
|
2
3
|
import hashlib
|
|
3
4
|
import re
|
|
4
5
|
from pathlib import Path
|
|
5
|
-
from typing import ClassVar, Iterable, List, Sequence
|
|
6
|
+
from typing import ClassVar, Iterable, List, Sequence, TYPE_CHECKING
|
|
6
7
|
from codeui.core.ir import Edge, EdgeKind, Location, Symbol, SymbolKind, Visibility
|
|
7
|
-
from codeui.core.resolver import ResolveContext
|
|
8
8
|
from codeui.lang.base import ImportRef, LanguageAnalyzer, ParseResult
|
|
9
9
|
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from codeui.core.resolver import ResolveContext
|
|
12
|
+
|
|
10
13
|
class GoLanguageAnalyzer(LanguageAnalyzer):
|
|
11
14
|
"""Go language analyzer.
|
|
12
15
|
Example:
|
codeui/lang/java.py
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
"""Java and Kotlin language analyzer for codeui."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import hashlib
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import ClassVar, Iterable, List, Sequence, TYPE_CHECKING
|
|
7
|
+
from codeui.core.ir import Edge, EdgeKind, Location, Symbol, SymbolKind, Visibility
|
|
8
|
+
from codeui.lang.base import ImportRef, LanguageAnalyzer, ParseResult
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from codeui.core.resolver import ResolveContext
|
|
12
|
+
|
|
13
|
+
class JavaLanguageAnalyzer(LanguageAnalyzer):
|
|
14
|
+
"""Java and Kotlin language analyzer.
|
|
15
|
+
Example:
|
|
16
|
+
>>> analyzer = JavaLanguageAnalyzer()
|
|
17
|
+
>>> res = analyzer.parse(Path("Main.java"), 'public class Main { public static void main(String[] args) {} }')
|
|
18
|
+
>>> syms = list(analyzer.extract_symbols(res))
|
|
19
|
+
>>> any(s.name == "Main" for s in syms)
|
|
20
|
+
True
|
|
21
|
+
"""
|
|
22
|
+
language: ClassVar[str] = "java"
|
|
23
|
+
extensions: ClassVar[tuple[str, ...]] = (".java", ".kt")
|
|
24
|
+
has_ast_support: ClassVar[bool] = True
|
|
25
|
+
is_supported: ClassVar[bool] = True
|
|
26
|
+
|
|
27
|
+
def parse(self, path: Path, source: str) -> ParseResult:
|
|
28
|
+
"""Parse Java or Kotlin source code into ParseResult.
|
|
29
|
+
Example:
|
|
30
|
+
>>> analyzer = JavaLanguageAnalyzer()
|
|
31
|
+
>>> res = analyzer.parse(Path("App.java"), "package com.example;")
|
|
32
|
+
>>> res.errors
|
|
33
|
+
[]
|
|
34
|
+
"""
|
|
35
|
+
content_hash = hashlib.sha256(source.encode("utf-8")).hexdigest()
|
|
36
|
+
return ParseResult(
|
|
37
|
+
file_path=str(path),
|
|
38
|
+
source=source,
|
|
39
|
+
ast=source,
|
|
40
|
+
content_hash=content_hash,
|
|
41
|
+
errors=[],
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def extract_symbols(self, parse: ParseResult) -> Iterable[Symbol]:
|
|
45
|
+
"""Extract classes, interfaces, records, enums, and methods from Java/Kotlin source.
|
|
46
|
+
Example:
|
|
47
|
+
>>> analyzer = JavaLanguageAnalyzer()
|
|
48
|
+
>>> res = analyzer.parse(Path("User.java"), "public class User { public String getName() { return name; } }")
|
|
49
|
+
>>> syms = list(analyzer.extract_symbols(res))
|
|
50
|
+
>>> len(syms) >= 2
|
|
51
|
+
True
|
|
52
|
+
"""
|
|
53
|
+
source = parse.source
|
|
54
|
+
file_path = parse.file_path
|
|
55
|
+
lines = source.splitlines()
|
|
56
|
+
max_line = len(lines) if lines else 1
|
|
57
|
+
symbols: List[Symbol] = []
|
|
58
|
+
|
|
59
|
+
file_sym = Symbol(
|
|
60
|
+
id=file_path,
|
|
61
|
+
name=Path(file_path).name,
|
|
62
|
+
qualified_name=file_path,
|
|
63
|
+
kind=SymbolKind.FILE,
|
|
64
|
+
language=self.language,
|
|
65
|
+
location=Location(file_path, 1, 0, max_line, 0),
|
|
66
|
+
parent_id=None,
|
|
67
|
+
signature=None,
|
|
68
|
+
visibility=Visibility.PUBLIC,
|
|
69
|
+
modifiers=(),
|
|
70
|
+
content_hash=parse.content_hash,
|
|
71
|
+
)
|
|
72
|
+
symbols.append(file_sym)
|
|
73
|
+
|
|
74
|
+
import_pattern = re.compile(r'import\s+(?:static\s+)?([A-Za-z0-9_.*]+)\s*;')
|
|
75
|
+
class_pattern = re.compile(r'(?:public|protected|private)?\s*(?:abstract|final|static)?\s*(?:class|interface|record|enum)\s+([A-Za-z0-9_]+)(?:<[^>]+>)?(?:\s+extends\s+([A-Za-z0-9_$.]+))?(?:\s+implements\s+([A-Za-z0-9_$,\s]+))?')
|
|
76
|
+
method_pattern = re.compile(r'(?:public|protected|private|static|final|synchronized|abstract|\s)+[\w<>\[\]]+\s+([A-Za-z0-9_]+)\s*\(([^)]*)\)')
|
|
77
|
+
kt_fun_pattern = re.compile(r'(?:public|private|protected|internal)?\s*fun\s+(?:<[^>]+>\s+)?([A-Za-z0-9_]+)\s*\(([^)]*)\)')
|
|
78
|
+
|
|
79
|
+
js_keywords = {"if", "for", "while", "switch", "catch", "return", "throw", "new", "class", "interface", "package", "import", "else", "try", "finally"}
|
|
80
|
+
class_ranges: List[tuple[str, str, int, int, str, str]] = []
|
|
81
|
+
|
|
82
|
+
for idx, line in enumerate(lines, start=1):
|
|
83
|
+
line_str = line.strip()
|
|
84
|
+
if not line_str or line_str.startswith("//") or line_str.startswith("*"):
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
for match in import_pattern.finditer(line_str):
|
|
88
|
+
imp_path = match.group(1)
|
|
89
|
+
sym_id = f"import:{imp_path}"
|
|
90
|
+
symbols.append(Symbol(
|
|
91
|
+
id=sym_id,
|
|
92
|
+
name=imp_path.split(".")[-1],
|
|
93
|
+
qualified_name=f"import:{imp_path}",
|
|
94
|
+
kind=SymbolKind.IMPORT,
|
|
95
|
+
language=self.language,
|
|
96
|
+
location=Location(file_path, idx, line.find(imp_path), idx, line.find(imp_path) + len(imp_path)),
|
|
97
|
+
parent_id=file_path,
|
|
98
|
+
signature=f"import {imp_path}",
|
|
99
|
+
visibility=Visibility.PUBLIC,
|
|
100
|
+
modifiers=(),
|
|
101
|
+
content_hash=self._extract_body_hash(lines, idx - 1),
|
|
102
|
+
))
|
|
103
|
+
|
|
104
|
+
for match in class_pattern.finditer(line_str):
|
|
105
|
+
cls_name = match.group(1)
|
|
106
|
+
if cls_name in js_keywords:
|
|
107
|
+
continue
|
|
108
|
+
ext_name = match.group(2) or ""
|
|
109
|
+
impl_names = match.group(3) or ""
|
|
110
|
+
sym_id = f"{file_path}::{cls_name}"
|
|
111
|
+
end_line = self._find_block_end(lines, idx - 1)
|
|
112
|
+
class_ranges.append((cls_name, sym_id, idx, end_line, ext_name, impl_names))
|
|
113
|
+
loc = Location(file_path, idx, line.find(cls_name), end_line, line.find(cls_name) + len(cls_name))
|
|
114
|
+
vis = Visibility.PUBLIC if "public" in line_str else (Visibility.PRIVATE if "private" in line_str else Visibility.INTERNAL)
|
|
115
|
+
symbols.append(Symbol(
|
|
116
|
+
id=sym_id,
|
|
117
|
+
name=cls_name,
|
|
118
|
+
qualified_name=cls_name,
|
|
119
|
+
kind=SymbolKind.CLASS,
|
|
120
|
+
language=self.language,
|
|
121
|
+
location=loc,
|
|
122
|
+
parent_id=file_path,
|
|
123
|
+
signature=f"class {cls_name}",
|
|
124
|
+
visibility=vis,
|
|
125
|
+
modifiers=("public",) if "public" in line_str else (),
|
|
126
|
+
content_hash=self._extract_body_hash(lines, idx - 1),
|
|
127
|
+
))
|
|
128
|
+
|
|
129
|
+
for match in method_pattern.finditer(line_str):
|
|
130
|
+
m_name = match.group(1)
|
|
131
|
+
params = match.group(2)
|
|
132
|
+
if m_name in js_keywords or m_name in ("if", "for", "while", "switch", "catch", "return"):
|
|
133
|
+
continue
|
|
134
|
+
enclosing = next((c for c in reversed(class_ranges) if c[2] <= idx <= c[3]), None)
|
|
135
|
+
if enclosing:
|
|
136
|
+
cls_name, cls_sym_id, _, _, _, _ = enclosing
|
|
137
|
+
sym_id = f"{file_path}::{cls_name}::{m_name}"
|
|
138
|
+
qual_name = f"{cls_name}.{m_name}"
|
|
139
|
+
parent_sym_id = cls_sym_id
|
|
140
|
+
else:
|
|
141
|
+
sym_id = f"{file_path}::{m_name}"
|
|
142
|
+
qual_name = m_name
|
|
143
|
+
parent_sym_id = file_path
|
|
144
|
+
end_line = self._find_block_end(lines, idx - 1)
|
|
145
|
+
loc = Location(file_path, idx, line.find(m_name), end_line, line.find(m_name) + len(m_name))
|
|
146
|
+
vis = Visibility.PUBLIC if "public" in line_str else (Visibility.PRIVATE if "private" in line_str else Visibility.INTERNAL)
|
|
147
|
+
symbols.append(Symbol(
|
|
148
|
+
id=sym_id,
|
|
149
|
+
name=m_name,
|
|
150
|
+
qualified_name=qual_name,
|
|
151
|
+
kind=SymbolKind.METHOD,
|
|
152
|
+
language=self.language,
|
|
153
|
+
location=loc,
|
|
154
|
+
parent_id=parent_sym_id,
|
|
155
|
+
signature=f"{m_name}({params})",
|
|
156
|
+
visibility=vis,
|
|
157
|
+
modifiers=("public",) if "public" in line_str else (),
|
|
158
|
+
content_hash=self._extract_body_hash(lines, idx - 1),
|
|
159
|
+
))
|
|
160
|
+
|
|
161
|
+
for match in kt_fun_pattern.finditer(line_str):
|
|
162
|
+
m_name = match.group(1)
|
|
163
|
+
params = match.group(2)
|
|
164
|
+
if m_name in js_keywords:
|
|
165
|
+
continue
|
|
166
|
+
enclosing = next((c for c in reversed(class_ranges) if c[2] <= idx <= c[3]), None)
|
|
167
|
+
if enclosing:
|
|
168
|
+
cls_name, cls_sym_id, _, _, _, _ = enclosing
|
|
169
|
+
sym_id = f"{file_path}::{cls_name}::{m_name}"
|
|
170
|
+
qual_name = f"{cls_name}.{m_name}"
|
|
171
|
+
parent_sym_id = cls_sym_id
|
|
172
|
+
else:
|
|
173
|
+
sym_id = f"{file_path}::{m_name}"
|
|
174
|
+
qual_name = m_name
|
|
175
|
+
parent_sym_id = file_path
|
|
176
|
+
end_line = self._find_block_end(lines, idx - 1)
|
|
177
|
+
loc = Location(file_path, idx, line.find(m_name), end_line, line.find(m_name) + len(m_name))
|
|
178
|
+
symbols.append(Symbol(
|
|
179
|
+
id=sym_id,
|
|
180
|
+
name=m_name,
|
|
181
|
+
qualified_name=qual_name,
|
|
182
|
+
kind=SymbolKind.FUNCTION,
|
|
183
|
+
language=self.language,
|
|
184
|
+
location=loc,
|
|
185
|
+
parent_id=parent_sym_id,
|
|
186
|
+
signature=f"fun {m_name}({params})",
|
|
187
|
+
visibility=Visibility.PUBLIC,
|
|
188
|
+
modifiers=(),
|
|
189
|
+
content_hash=self._extract_body_hash(lines, idx - 1),
|
|
190
|
+
))
|
|
191
|
+
|
|
192
|
+
return symbols
|
|
193
|
+
|
|
194
|
+
def extract_edges(self, parse: ParseResult, symbols: Sequence[Symbol]) -> Iterable[Edge]:
|
|
195
|
+
"""Extract relationship edges for Java / Kotlin AST.
|
|
196
|
+
Example:
|
|
197
|
+
>>> analyzer = JavaLanguageAnalyzer()
|
|
198
|
+
>>> res = analyzer.parse(Path("App.java"), "import com.example.Helper;")
|
|
199
|
+
>>> syms = list(analyzer.extract_symbols(res))
|
|
200
|
+
>>> edges = list(analyzer.extract_edges(res, syms))
|
|
201
|
+
>>> len(edges) >= 1
|
|
202
|
+
True
|
|
203
|
+
"""
|
|
204
|
+
edges: List[Edge] = []
|
|
205
|
+
file_path = parse.file_path
|
|
206
|
+
sym_map = {s.id: s for s in symbols}
|
|
207
|
+
class_pattern = re.compile(r'(?:class|interface|record|enum)\s+([A-Za-z0-9_]+)(?:<[^>]+>)?(?:\s+extends\s+([A-Za-z0-9_$.]+))?(?:\s+implements\s+([A-Za-z0-9_$,\s]+))?')
|
|
208
|
+
call_pattern = re.compile(r'(?<![A-Za-z0-9_$-])\b([A-Za-z_][A-Za-z0-9_]*)\s*\(')
|
|
209
|
+
java_keywords = {"if", "for", "while", "switch", "catch", "return", "throw", "new", "super", "this"}
|
|
210
|
+
|
|
211
|
+
for s in symbols:
|
|
212
|
+
if s.parent_id and s.parent_id in sym_map:
|
|
213
|
+
edges.append(Edge(
|
|
214
|
+
source_id=s.parent_id,
|
|
215
|
+
target_id=s.id,
|
|
216
|
+
kind=EdgeKind.CONTAINS,
|
|
217
|
+
weight=1.0,
|
|
218
|
+
confidence=1.0,
|
|
219
|
+
location=s.location,
|
|
220
|
+
))
|
|
221
|
+
if s.kind == SymbolKind.IMPORT:
|
|
222
|
+
target_mod = s.qualified_name.replace("import:", "")
|
|
223
|
+
edges.append(Edge(
|
|
224
|
+
source_id=file_path,
|
|
225
|
+
target_id=f"module::{target_mod}",
|
|
226
|
+
kind=EdgeKind.IMPORTS,
|
|
227
|
+
weight=1.0,
|
|
228
|
+
confidence=1.0,
|
|
229
|
+
location=s.location,
|
|
230
|
+
))
|
|
231
|
+
|
|
232
|
+
lines = parse.source.splitlines()
|
|
233
|
+
for idx, line in enumerate(lines, start=1):
|
|
234
|
+
line_str = line.strip()
|
|
235
|
+
if not line_str or line_str.startswith("//") or line_str.startswith("*"):
|
|
236
|
+
continue
|
|
237
|
+
for match in class_pattern.finditer(line_str):
|
|
238
|
+
cls_name = match.group(1)
|
|
239
|
+
ext_name = match.group(2)
|
|
240
|
+
impl_names = match.group(3)
|
|
241
|
+
cls_sym_id = f"{file_path}::{cls_name}"
|
|
242
|
+
if ext_name:
|
|
243
|
+
base_clean = ext_name.strip().split("<")[0].strip()
|
|
244
|
+
loc = Location(file_path, idx, line.find(base_clean), idx, line.find(base_clean) + len(base_clean))
|
|
245
|
+
edges.append(Edge(
|
|
246
|
+
source_id=cls_sym_id,
|
|
247
|
+
target_id=f"module::{base_clean}",
|
|
248
|
+
kind=EdgeKind.INHERITS,
|
|
249
|
+
weight=1.0,
|
|
250
|
+
confidence=0.9,
|
|
251
|
+
location=loc,
|
|
252
|
+
))
|
|
253
|
+
if impl_names:
|
|
254
|
+
for iface in impl_names.split(","):
|
|
255
|
+
iface_clean = iface.strip().split("<")[0].strip()
|
|
256
|
+
if iface_clean and iface_clean not in java_keywords:
|
|
257
|
+
loc = Location(file_path, idx, line.find(iface_clean), idx, line.find(iface_clean) + len(iface_clean))
|
|
258
|
+
edges.append(Edge(
|
|
259
|
+
source_id=cls_sym_id,
|
|
260
|
+
target_id=f"module::{iface_clean}",
|
|
261
|
+
kind=EdgeKind.IMPLEMENTS,
|
|
262
|
+
weight=1.0,
|
|
263
|
+
confidence=0.9,
|
|
264
|
+
location=loc,
|
|
265
|
+
))
|
|
266
|
+
|
|
267
|
+
method_syms = [s for s in symbols if s.kind in (SymbolKind.METHOD, SymbolKind.FUNCTION)]
|
|
268
|
+
name_to_syms: dict[str, list[Symbol]] = {}
|
|
269
|
+
for s in method_syms:
|
|
270
|
+
name_to_syms.setdefault(s.name, []).append(s)
|
|
271
|
+
|
|
272
|
+
for m in method_syms:
|
|
273
|
+
if not m.location:
|
|
274
|
+
continue
|
|
275
|
+
m_lines = lines[m.location.start_line - 1 : m.location.end_line]
|
|
276
|
+
for m_line in m_lines:
|
|
277
|
+
for match in call_pattern.finditer(m_line):
|
|
278
|
+
callee_name = match.group(1)
|
|
279
|
+
if callee_name in java_keywords or callee_name == m.name:
|
|
280
|
+
continue
|
|
281
|
+
if callee_name in name_to_syms:
|
|
282
|
+
target = name_to_syms[callee_name][0]
|
|
283
|
+
edges.append(Edge(
|
|
284
|
+
source_id=m.id,
|
|
285
|
+
target_id=target.id,
|
|
286
|
+
kind=EdgeKind.CALLS,
|
|
287
|
+
weight=1.0,
|
|
288
|
+
confidence=0.85,
|
|
289
|
+
location=m.location,
|
|
290
|
+
))
|
|
291
|
+
|
|
292
|
+
return edges
|
|
293
|
+
|
|
294
|
+
def resolve_import(self, ref: ImportRef, ctx: ResolveContext) -> Iterable[str]:
|
|
295
|
+
"""Resolve Java import references to project files.
|
|
296
|
+
Example:
|
|
297
|
+
>>> analyzer = JavaLanguageAnalyzer()
|
|
298
|
+
>>> ctx = ResolveContext(Path("."))
|
|
299
|
+
>>> list(analyzer.resolve_import(ImportRef("App.java", "com.example.Helper", None, False), ctx))
|
|
300
|
+
[]
|
|
301
|
+
"""
|
|
302
|
+
clean_path = ref.module_name.replace(".", "/")
|
|
303
|
+
return ctx.resolve_path(ref.file_path, clean_path, self.language)
|
|
304
|
+
|
|
305
|
+
def _find_block_end(self, lines: List[str], start_idx: int) -> int:
|
|
306
|
+
open_braces = 0
|
|
307
|
+
found_brace = False
|
|
308
|
+
for i in range(start_idx, len(lines)):
|
|
309
|
+
line = lines[i]
|
|
310
|
+
open_braces += line.count("{") - line.count("}")
|
|
311
|
+
if "{" in line:
|
|
312
|
+
found_brace = True
|
|
313
|
+
if found_brace and open_braces <= 0:
|
|
314
|
+
return i + 1
|
|
315
|
+
return min(start_idx + 30, len(lines))
|
|
316
|
+
|
|
317
|
+
def _extract_body_hash(self, lines: List[str], start_idx: int) -> str:
|
|
318
|
+
end_idx = self._find_block_end(lines, start_idx)
|
|
319
|
+
snippet = "\n".join(lines[start_idx:end_idx])
|
|
320
|
+
return hashlib.sha256(snippet.encode("utf-8")).hexdigest()
|