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/swift.py
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"""Swift 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 SwiftLanguageAnalyzer(LanguageAnalyzer):
|
|
14
|
+
"""Swift language adapter extracting protocols, classes, structs, enums, actors, funcs, and vars.
|
|
15
|
+
Example:
|
|
16
|
+
>>> analyzer = SwiftLanguageAnalyzer()
|
|
17
|
+
>>> res = analyzer.parse(Path("Model.swift"), "struct User: Codable { let id: Int }")
|
|
18
|
+
>>> syms = list(analyzer.extract_symbols(res))
|
|
19
|
+
>>> any(s.kind == SymbolKind.CLASS for s in syms)
|
|
20
|
+
True
|
|
21
|
+
"""
|
|
22
|
+
language: ClassVar[str] = "swift"
|
|
23
|
+
extensions: ClassVar[tuple[str, ...]] = (".swift",)
|
|
24
|
+
|
|
25
|
+
def parse(self, path: Path, source: str) -> ParseResult:
|
|
26
|
+
"""Parse Swift source into AST-emulating representation.
|
|
27
|
+
Example:
|
|
28
|
+
>>> analyzer = SwiftLanguageAnalyzer()
|
|
29
|
+
>>> res = analyzer.parse(Path("App.swift"), "func run() {}")
|
|
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 Swift types, functions, properties, extensions, and imports.
|
|
44
|
+
Example:
|
|
45
|
+
>>> analyzer = SwiftLanguageAnalyzer()
|
|
46
|
+
>>> res = analyzer.parse(Path("Service.swift"), "class Service { func fetch() {} }")
|
|
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\s+([a-zA-Z0-9_]+)')
|
|
73
|
+
type_pattern = re.compile(r'^\s*(?:(public|private|fileprivate|internal|open)\s+)?(?:(final)\s+)?(class|struct|protocol|enum|actor)\s+([a-zA-Z0-9_]+)(?:<[^>]+>)?(?:\s*:\s*([a-zA-Z0-9_.,\s<>]+))?')
|
|
74
|
+
extension_pattern = re.compile(r'^\s*extension\s+([a-zA-Z0-9_]+)(?:\s*:\s*([a-zA-Z0-9_.,\s<>]+))?')
|
|
75
|
+
func_pattern = re.compile(r'^\s*(?:(public|private|fileprivate|internal|open)\s+)?(?:(static|class|override|mutating|async|throws)\s+)*(func|init)\s*(?:<[^>]+>\s*)?([a-zA-Z0-9_]+)?\s*\(([^)]*)\)(?:\s*(?:async\s*)?(?:throws\s*)?->\s*([a-zA-Z0-9_?<>,\s\[\]:]+))?')
|
|
76
|
+
var_pattern = re.compile(r'^\s*(?:(public|private|fileprivate|internal|open)\s+)?(?:(static|class|weak|unowned)\s+)*(let|var)\s+([a-zA-Z0-9_]+)(?:\s*:\s*([a-zA-Z0-9_?<>,\s\[\]:]+))?')
|
|
77
|
+
|
|
78
|
+
current_type: str | None = None
|
|
79
|
+
brace_depth = 0
|
|
80
|
+
type_depth = 0
|
|
81
|
+
|
|
82
|
+
for line_idx, line in enumerate(lines, start=1):
|
|
83
|
+
stripped = line.strip()
|
|
84
|
+
if not stripped or stripped.startswith("//") or stripped.startswith("/*") or stripped.startswith("*"):
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
imp_match = import_pattern.match(line)
|
|
88
|
+
if imp_match:
|
|
89
|
+
mod_name = imp_match.group(1)
|
|
90
|
+
symbols.append(Symbol(
|
|
91
|
+
id=f"{file_path}::import::{mod_name}",
|
|
92
|
+
name=mod_name,
|
|
93
|
+
qualified_name=mod_name,
|
|
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 {mod_name}",
|
|
99
|
+
visibility=Visibility.PUBLIC,
|
|
100
|
+
modifiers=(),
|
|
101
|
+
content_hash=hashlib.sha256(line.encode("utf-8")).hexdigest()[:16],
|
|
102
|
+
))
|
|
103
|
+
continue
|
|
104
|
+
|
|
105
|
+
ext_match = extension_pattern.match(line)
|
|
106
|
+
if ext_match:
|
|
107
|
+
ext_name = ext_match.group(1)
|
|
108
|
+
sym_id = f"{file_path}::extension_{ext_name}_{line_idx}"
|
|
109
|
+
current_type = sym_id
|
|
110
|
+
type_depth = brace_depth
|
|
111
|
+
symbols.append(Symbol(
|
|
112
|
+
id=sym_id,
|
|
113
|
+
name=ext_name,
|
|
114
|
+
qualified_name=ext_name,
|
|
115
|
+
kind=SymbolKind.CLASS,
|
|
116
|
+
language=self.language,
|
|
117
|
+
location=Location(file_path, line_idx, 0, line_idx, len(line)),
|
|
118
|
+
parent_id=file_path,
|
|
119
|
+
signature=f"extension {ext_name}",
|
|
120
|
+
visibility=Visibility.PUBLIC,
|
|
121
|
+
modifiers=("extension",),
|
|
122
|
+
content_hash=hashlib.sha256(line.encode("utf-8")).hexdigest()[:16],
|
|
123
|
+
))
|
|
124
|
+
|
|
125
|
+
type_match = type_pattern.match(line)
|
|
126
|
+
if type_match:
|
|
127
|
+
vis_str = type_match.group(1) or "internal"
|
|
128
|
+
final_mod = type_match.group(2)
|
|
129
|
+
type_kind = type_match.group(3)
|
|
130
|
+
type_name = type_match.group(4)
|
|
131
|
+
vis = Visibility.PRIVATE if vis_str in ("private", "fileprivate") else (Visibility.PROTECTED if vis_str == "internal" else Visibility.PUBLIC)
|
|
132
|
+
kind = SymbolKind.INTERFACE if type_kind == "protocol" else SymbolKind.CLASS
|
|
133
|
+
modifiers = tuple(filter(None, [vis_str, final_mod, type_kind]))
|
|
134
|
+
sym_id = f"{file_path}::{type_name}"
|
|
135
|
+
current_type = sym_id
|
|
136
|
+
type_depth = brace_depth
|
|
137
|
+
|
|
138
|
+
symbols.append(Symbol(
|
|
139
|
+
id=sym_id,
|
|
140
|
+
name=type_name,
|
|
141
|
+
qualified_name=type_name,
|
|
142
|
+
kind=kind,
|
|
143
|
+
language=self.language,
|
|
144
|
+
location=Location(file_path, line_idx, 0, line_idx, len(line)),
|
|
145
|
+
parent_id=file_path,
|
|
146
|
+
signature=f"{vis_str} {type_kind} {type_name}",
|
|
147
|
+
visibility=vis,
|
|
148
|
+
modifiers=modifiers,
|
|
149
|
+
content_hash=hashlib.sha256(line.encode("utf-8")).hexdigest()[:16],
|
|
150
|
+
))
|
|
151
|
+
|
|
152
|
+
func_match = func_pattern.match(line)
|
|
153
|
+
if func_match:
|
|
154
|
+
vis_str = func_match.group(1) or "internal"
|
|
155
|
+
func_mod = func_match.group(2)
|
|
156
|
+
is_init = func_match.group(3) == "init"
|
|
157
|
+
func_name = "init" if is_init else (func_match.group(4) or "anonymous")
|
|
158
|
+
func_args = func_match.group(5) or ""
|
|
159
|
+
func_ret = func_match.group(6) or "Void"
|
|
160
|
+
vis = Visibility.PRIVATE if vis_str in ("private", "fileprivate") else (Visibility.PROTECTED if vis_str == "internal" else Visibility.PUBLIC)
|
|
161
|
+
parent_id = current_type if current_type and brace_depth > type_depth else file_path
|
|
162
|
+
sym_id = f"{parent_id}::{func_name}"
|
|
163
|
+
symbols.append(Symbol(
|
|
164
|
+
id=sym_id,
|
|
165
|
+
name=func_name,
|
|
166
|
+
qualified_name=func_name,
|
|
167
|
+
kind=SymbolKind.FUNCTION,
|
|
168
|
+
language=self.language,
|
|
169
|
+
location=Location(file_path, line_idx, 0, line_idx, len(line)),
|
|
170
|
+
parent_id=parent_id,
|
|
171
|
+
signature=f"func {func_name}({func_args}) -> {func_ret}" if not is_init else f"init({func_args})",
|
|
172
|
+
visibility=vis,
|
|
173
|
+
modifiers=tuple(filter(None, [vis_str, func_mod])),
|
|
174
|
+
content_hash=hashlib.sha256(line.encode("utf-8")).hexdigest()[:16],
|
|
175
|
+
))
|
|
176
|
+
|
|
177
|
+
var_match = var_pattern.match(line)
|
|
178
|
+
if var_match:
|
|
179
|
+
vis_str = var_match.group(1) or "internal"
|
|
180
|
+
var_mod = var_match.group(2)
|
|
181
|
+
let_or_var = var_match.group(3)
|
|
182
|
+
var_name = var_match.group(4)
|
|
183
|
+
var_type = var_match.group(5) or "Any"
|
|
184
|
+
vis = Visibility.PRIVATE if vis_str in ("private", "fileprivate") else (Visibility.PROTECTED if vis_str == "internal" else Visibility.PUBLIC)
|
|
185
|
+
parent_id = current_type if current_type and brace_depth > type_depth else file_path
|
|
186
|
+
sym_id = f"{parent_id}::{var_name}"
|
|
187
|
+
symbols.append(Symbol(
|
|
188
|
+
id=sym_id,
|
|
189
|
+
name=var_name,
|
|
190
|
+
qualified_name=var_name,
|
|
191
|
+
kind=SymbolKind.VARIABLE,
|
|
192
|
+
language=self.language,
|
|
193
|
+
location=Location(file_path, line_idx, 0, line_idx, len(line)),
|
|
194
|
+
parent_id=parent_id,
|
|
195
|
+
signature=f"{let_or_var} {var_name}: {var_type}",
|
|
196
|
+
visibility=vis,
|
|
197
|
+
modifiers=tuple(filter(None, [vis_str, var_mod, let_or_var])),
|
|
198
|
+
content_hash=hashlib.sha256(line.encode("utf-8")).hexdigest()[:16],
|
|
199
|
+
))
|
|
200
|
+
|
|
201
|
+
brace_depth += line.count("{") - line.count("}")
|
|
202
|
+
if current_type and brace_depth <= type_depth:
|
|
203
|
+
current_type = None
|
|
204
|
+
|
|
205
|
+
return sorted(symbols, key=lambda s: (s.location.start_line if s.location else 0, s.name))
|
|
206
|
+
|
|
207
|
+
def extract_edges(self, parse: ParseResult, symbols: Sequence[Symbol]) -> Iterable[Edge]:
|
|
208
|
+
"""Extract containment, protocol adoption, class inheritance, and import edges for Swift.
|
|
209
|
+
Example:
|
|
210
|
+
>>> analyzer = SwiftLanguageAnalyzer()
|
|
211
|
+
>>> res = analyzer.parse(Path("App.swift"), "class Child: Parent {}")
|
|
212
|
+
>>> syms = list(analyzer.extract_symbols(res))
|
|
213
|
+
>>> edges = list(analyzer.extract_edges(res, syms))
|
|
214
|
+
>>> len(edges) >= 1
|
|
215
|
+
True
|
|
216
|
+
"""
|
|
217
|
+
edges: List[Edge] = []
|
|
218
|
+
file_path = parse.file_path
|
|
219
|
+
symbol_map = {s.name: s for s in symbols}
|
|
220
|
+
type_inherit_pattern = re.compile(r'^\s*(?:[a-zA-Z0-9_\s<>]+)?(?:class|struct|protocol|enum|actor|extension)\s+([a-zA-Z0-9_]+)(?:<[^>]+>)?\s*:\s*([a-zA-Z0-9_.,\s<>]+)')
|
|
221
|
+
|
|
222
|
+
for s in symbols:
|
|
223
|
+
if s.parent_id and s.parent_id != s.id:
|
|
224
|
+
edges.append(Edge(
|
|
225
|
+
source_id=s.parent_id,
|
|
226
|
+
target_id=s.id,
|
|
227
|
+
kind=EdgeKind.CONTAINS,
|
|
228
|
+
weight=1.0,
|
|
229
|
+
confidence=1.0,
|
|
230
|
+
location=s.location,
|
|
231
|
+
))
|
|
232
|
+
|
|
233
|
+
for line_idx, line in enumerate(parse.source.splitlines(), start=1):
|
|
234
|
+
type_match = type_inherit_pattern.match(line)
|
|
235
|
+
if type_match:
|
|
236
|
+
type_name = type_match.group(1)
|
|
237
|
+
super_types_raw = type_match.group(2)
|
|
238
|
+
type_sym = symbol_map.get(type_name)
|
|
239
|
+
if type_sym:
|
|
240
|
+
for super_raw in super_types_raw.split(","):
|
|
241
|
+
clean_super = super_raw.strip().split("<")[0].strip()
|
|
242
|
+
if clean_super and clean_super not in ("AnyObject", "Codable", "Equatable", "Hashable"):
|
|
243
|
+
target_id = symbol_map[clean_super].id if clean_super in symbol_map else clean_super
|
|
244
|
+
edges.append(Edge(
|
|
245
|
+
source_id=type_sym.id,
|
|
246
|
+
target_id=target_id,
|
|
247
|
+
kind=EdgeKind.INHERITS,
|
|
248
|
+
weight=1.0,
|
|
249
|
+
confidence=0.9,
|
|
250
|
+
location=Location(file_path, line_idx, 0, line_idx, len(line)),
|
|
251
|
+
))
|
|
252
|
+
|
|
253
|
+
if line.strip().startswith("import "):
|
|
254
|
+
imp_mod = line.strip().replace("import ", "").split()[0]
|
|
255
|
+
edges.append(Edge(
|
|
256
|
+
source_id=file_path,
|
|
257
|
+
target_id=imp_mod,
|
|
258
|
+
kind=EdgeKind.IMPORTS,
|
|
259
|
+
weight=1.0,
|
|
260
|
+
confidence=1.0,
|
|
261
|
+
location=Location(file_path, line_idx, 0, line_idx, len(line)),
|
|
262
|
+
))
|
|
263
|
+
|
|
264
|
+
return sorted(edges, key=lambda e: (e.source_id, e.target_id, e.kind.value))
|
|
265
|
+
|
|
266
|
+
def resolve_import(self, ref: ImportRef, ctx: ResolveContext) -> Iterable[str]:
|
|
267
|
+
"""Resolve Swift module imports against workspace packages or files.
|
|
268
|
+
Example:
|
|
269
|
+
>>> analyzer = SwiftLanguageAnalyzer()
|
|
270
|
+
>>> from codeui.core.resolver import ResolveContext
|
|
271
|
+
>>> ctx = ResolveContext(Path("."))
|
|
272
|
+
>>> ref = ImportRef("Sources/App/main.swift", "SharedKit", None, False)
|
|
273
|
+
>>> list(analyzer.resolve_import(ref, ctx))
|
|
274
|
+
[]
|
|
275
|
+
"""
|
|
276
|
+
return ctx.resolve_path(ref.from_file, ref.module_name, "swift")
|
codeui/lang/ts.py
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
"""TypeScript/JavaScript 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 TSLanguageAnalyzer(LanguageAnalyzer):
|
|
11
14
|
"""TypeScript and JavaScript language adapter.
|
|
12
15
|
Example:
|
|
@@ -64,8 +67,8 @@ class TSLanguageAnalyzer(LanguageAnalyzer):
|
|
|
64
67
|
content_hash=parse.content_hash,
|
|
65
68
|
)
|
|
66
69
|
symbols.append(file_sym)
|
|
67
|
-
class_pattern = re.compile(r'(?:export\s+)?(?:default\s+)?class\s+([A-Za-z0-9_$]+)(?:<[^>]+>)?(?:\s+extends\s+([A-Za-z0-9_
|
|
68
|
-
interface_pattern = re.compile(r'(?:export\s+)?interface\s+([A-Za-z0-9_$]+)(?:<[^>]+>)?')
|
|
70
|
+
class_pattern = re.compile(r'(?:export\s+)?(?:default\s+)?class\s+([A-Za-z0-9_$]+)(?:<[^>]+>)?(?:\s+extends\s+([A-Za-z0-9_$.]+))?(?:\s+implements\s+([A-Za-z0-9_$,\s]+))?')
|
|
71
|
+
interface_pattern = re.compile(r'(?:export\s+)?interface\s+([A-Za-z0-9_$]+)(?:<[^>]+>)?(?:\s+extends\s+([A-Za-z0-9_$,\s]+))?')
|
|
69
72
|
type_pattern = re.compile(r'(?:export\s+)?type\s+([A-Za-z0-9_$]+)(?:<[^>]+>)?\s*=')
|
|
70
73
|
func_pattern = re.compile(r'(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s+([A-Za-z0-9_$]+)\s*(?:<[^>]+>)?\s*\(')
|
|
71
74
|
const_func_pattern = re.compile(r'(?:export\s+)?(?:default\s+)?(?:const|let|var)\s+([A-Za-z0-9_$]+)\s*=\s*(?:async\s*)?(?:<[^>]+>)?\s*\(')
|
|
@@ -292,6 +295,8 @@ class TSLanguageAnalyzer(LanguageAnalyzer):
|
|
|
292
295
|
edges: List[Edge] = []
|
|
293
296
|
file_path = parse.file_path
|
|
294
297
|
sym_map = {s.id: s for s in symbols}
|
|
298
|
+
import_map: dict[str, str] = {}
|
|
299
|
+
|
|
295
300
|
for s in symbols:
|
|
296
301
|
if s.parent_id and s.parent_id in sym_map:
|
|
297
302
|
edges.append(Edge(
|
|
@@ -304,6 +309,21 @@ class TSLanguageAnalyzer(LanguageAnalyzer):
|
|
|
304
309
|
))
|
|
305
310
|
if s.kind == SymbolKind.IMPORT:
|
|
306
311
|
target_mod = s.qualified_name.replace("import:", "")
|
|
312
|
+
raw_spec = s.name.strip()
|
|
313
|
+
if raw_spec:
|
|
314
|
+
for part in raw_spec.split(","):
|
|
315
|
+
part_clean = part.strip()
|
|
316
|
+
if not part_clean:
|
|
317
|
+
continue
|
|
318
|
+
if " as " in part_clean:
|
|
319
|
+
orig, alias = part_clean.split(" as ", 1)
|
|
320
|
+
import_map[alias.strip()] = f"module::{target_mod}::{orig.strip()}"
|
|
321
|
+
elif part_clean.startswith("*"):
|
|
322
|
+
alias_name = part_clean.replace("*", "").replace("as", "").strip()
|
|
323
|
+
if alias_name:
|
|
324
|
+
import_map[alias_name] = f"module::{target_mod}"
|
|
325
|
+
else:
|
|
326
|
+
import_map[part_clean] = f"module::{target_mod}::{part_clean}"
|
|
307
327
|
weight = float(self._count_symbol_usages(parse.source, s.name))
|
|
308
328
|
edges.append(Edge(
|
|
309
329
|
source_id=file_path,
|
|
@@ -313,7 +333,11 @@ class TSLanguageAnalyzer(LanguageAnalyzer):
|
|
|
313
333
|
confidence=1.0,
|
|
314
334
|
location=s.location,
|
|
315
335
|
))
|
|
336
|
+
|
|
337
|
+
class_inherit_pattern = re.compile(r'(?:export\s+)?(?:default\s+)?class\s+([A-Za-z0-9_$]+)(?:<[^>]+>)?(?:\s+extends\s+([A-Za-z0-9_$.]+))?(?:\s+implements\s+([A-Za-z0-9_$,\s]+))?')
|
|
338
|
+
interface_inherit_pattern = re.compile(r'(?:export\s+)?interface\s+([A-Za-z0-9_$]+)(?:<[^>]+>)?(?:\s+extends\s+([A-Za-z0-9_$,\s]+))?')
|
|
316
339
|
call_pattern = re.compile(r'(?<![A-Za-z0-9_$-])\b([A-Za-z_$][A-Za-z0-9_$]*)\s*\(')
|
|
340
|
+
jsx_pattern = re.compile(r'<([A-Za-z_$][A-Za-z0-9_$]*)(?:[\s/>]|\.[A-Za-z0-9_$]+)')
|
|
317
341
|
js_keywords = {
|
|
318
342
|
"if", "for", "while", "switch", "catch", "function", "var", "let", "const",
|
|
319
343
|
"return", "throw", "new", "typeof", "instanceof", "yield", "await", "async",
|
|
@@ -321,8 +345,9 @@ class TSLanguageAnalyzer(LanguageAnalyzer):
|
|
|
321
345
|
"else", "try", "finally", "break", "continue", "in", "of", "void", "delete", "require"
|
|
322
346
|
}
|
|
323
347
|
lines = parse.source.splitlines()
|
|
324
|
-
func_syms = [s for s in symbols if s.kind in (SymbolKind.FUNCTION, SymbolKind.METHOD, SymbolKind.CLASS)]
|
|
348
|
+
func_syms = [s for s in symbols if s.kind in (SymbolKind.FUNCTION, SymbolKind.METHOD, SymbolKind.CLASS, SymbolKind.INTERFACE)]
|
|
325
349
|
in_multiline_edges = False
|
|
350
|
+
|
|
326
351
|
for idx, line in enumerate(lines, start=1):
|
|
327
352
|
line_str = line.strip()
|
|
328
353
|
clean_str_line = re.sub(r"'(?:\\.|[^'\\])*'", "''", line_str)
|
|
@@ -342,6 +367,54 @@ class TSLanguageAnalyzer(LanguageAnalyzer):
|
|
|
342
367
|
line_str = line_str.split("/*", 1)[0].strip()
|
|
343
368
|
if not line_str or line_str.startswith("//") or line_str.startswith("*") or "var(" in line_str:
|
|
344
369
|
continue
|
|
370
|
+
|
|
371
|
+
for match in class_inherit_pattern.finditer(line_str):
|
|
372
|
+
cls_name = match.group(1)
|
|
373
|
+
base_name = match.group(2)
|
|
374
|
+
impl_names = match.group(3)
|
|
375
|
+
cls_sym_id = f"{file_path}::{cls_name}"
|
|
376
|
+
if base_name:
|
|
377
|
+
target_id = import_map.get(base_name, f"{file_path}::{base_name}")
|
|
378
|
+
edges.append(Edge(
|
|
379
|
+
source_id=cls_sym_id,
|
|
380
|
+
target_id=target_id,
|
|
381
|
+
kind=EdgeKind.INHERITS,
|
|
382
|
+
weight=1.0,
|
|
383
|
+
confidence=1.0,
|
|
384
|
+
location=Location(file_path, idx, line.find(base_name), idx, line.find(base_name) + len(base_name)),
|
|
385
|
+
))
|
|
386
|
+
if impl_names:
|
|
387
|
+
for iface in impl_names.split(","):
|
|
388
|
+
iface_clean = iface.strip()
|
|
389
|
+
if iface_clean and iface_clean not in js_keywords:
|
|
390
|
+
target_id = import_map.get(iface_clean, f"{file_path}::{iface_clean}")
|
|
391
|
+
edges.append(Edge(
|
|
392
|
+
source_id=cls_sym_id,
|
|
393
|
+
target_id=target_id,
|
|
394
|
+
kind=EdgeKind.INHERITS,
|
|
395
|
+
weight=1.0,
|
|
396
|
+
confidence=0.9,
|
|
397
|
+
location=Location(file_path, idx, line.find(iface_clean), idx, line.find(iface_clean) + len(iface_clean)),
|
|
398
|
+
))
|
|
399
|
+
|
|
400
|
+
for match in interface_inherit_pattern.finditer(line_str):
|
|
401
|
+
iface_name = match.group(1)
|
|
402
|
+
ext_names = match.group(2)
|
|
403
|
+
iface_sym_id = f"{file_path}::{iface_name}"
|
|
404
|
+
if ext_names:
|
|
405
|
+
for ext_target in ext_names.split(","):
|
|
406
|
+
ext_clean = ext_target.strip()
|
|
407
|
+
if ext_clean and ext_clean not in js_keywords:
|
|
408
|
+
target_id = import_map.get(ext_clean, f"{file_path}::{ext_clean}")
|
|
409
|
+
edges.append(Edge(
|
|
410
|
+
source_id=iface_sym_id,
|
|
411
|
+
target_id=target_id,
|
|
412
|
+
kind=EdgeKind.INHERITS,
|
|
413
|
+
weight=1.0,
|
|
414
|
+
confidence=0.9,
|
|
415
|
+
location=Location(file_path, idx, line.find(ext_clean), idx, line.find(ext_clean) + len(ext_clean)),
|
|
416
|
+
))
|
|
417
|
+
|
|
345
418
|
enclosing = None
|
|
346
419
|
for s in func_syms:
|
|
347
420
|
if s.location and s.location.start_line <= idx <= (s.location.end_line or (s.location.start_line + 50)):
|
|
@@ -351,23 +424,41 @@ class TSLanguageAnalyzer(LanguageAnalyzer):
|
|
|
351
424
|
clean_code_line = re.sub(r'"(?:\\.|[^"\\])*"', '""', clean_code_line)
|
|
352
425
|
clean_code_line = re.sub(r'`(?:\\.|[^`\\])*`', '``', clean_code_line)
|
|
353
426
|
clean_code_line = re.sub(r'/(?:\\.|[^/\\])+/[gimsuy]*', '//', clean_code_line)
|
|
427
|
+
|
|
354
428
|
for match in call_pattern.finditer(clean_code_line):
|
|
355
429
|
called_name = match.group(1)
|
|
356
430
|
pre_str = clean_code_line[:match.start(1)].rstrip()
|
|
357
431
|
is_member_call = pre_str.endswith(".") or pre_str.endswith("?.")
|
|
358
|
-
if
|
|
432
|
+
if is_member_call:
|
|
359
433
|
continue
|
|
360
|
-
if called_name not in js_keywords and
|
|
434
|
+
if called_name not in js_keywords and called_name not in ("translateY", "translateX", "scale", "rotate"):
|
|
361
435
|
col = line.find(called_name)
|
|
362
436
|
weight = float(self._count_symbol_usages(parse.source, called_name))
|
|
437
|
+
target_id = import_map.get(called_name, f"{file_path}::{called_name}")
|
|
363
438
|
edges.append(Edge(
|
|
364
439
|
source_id=caller_id,
|
|
365
|
-
target_id=
|
|
440
|
+
target_id=target_id,
|
|
366
441
|
kind=EdgeKind.CALLS,
|
|
367
442
|
weight=weight,
|
|
368
443
|
confidence=0.75,
|
|
369
444
|
location=Location(file_path, idx, col if col >= 0 else 0, idx, col + len(called_name) if col >= 0 else len(called_name)),
|
|
370
445
|
))
|
|
446
|
+
|
|
447
|
+
for match in jsx_pattern.finditer(clean_code_line):
|
|
448
|
+
tag_name = match.group(1)
|
|
449
|
+
if tag_name and tag_name[0].isupper() and tag_name not in js_keywords:
|
|
450
|
+
col = line.find(tag_name)
|
|
451
|
+
weight = float(self._count_symbol_usages(parse.source, tag_name))
|
|
452
|
+
target_id = import_map.get(tag_name, f"{file_path}::{tag_name}")
|
|
453
|
+
edges.append(Edge(
|
|
454
|
+
source_id=caller_id,
|
|
455
|
+
target_id=target_id,
|
|
456
|
+
kind=EdgeKind.CALLS,
|
|
457
|
+
weight=weight,
|
|
458
|
+
confidence=0.85,
|
|
459
|
+
location=Location(file_path, idx, col if col >= 0 else 0, idx, col + len(tag_name) if col >= 0 else len(tag_name)),
|
|
460
|
+
))
|
|
461
|
+
|
|
371
462
|
return edges
|
|
372
463
|
|
|
373
464
|
def _count_symbol_usages(self, source: str, symbol_name: str) -> int:
|
codeui/report/sarif_emitter.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""SARIF 2.1.0 format report emitter."""
|
|
2
2
|
import json
|
|
3
3
|
from typing import Any, Dict
|
|
4
|
+
import codeui
|
|
4
5
|
from codeui.core.graph import Graph
|
|
5
6
|
|
|
6
7
|
class SARIFEmitter:
|
|
@@ -46,7 +47,7 @@ class SARIFEmitter:
|
|
|
46
47
|
"tool": {
|
|
47
48
|
"driver": {
|
|
48
49
|
"name": "codeui",
|
|
49
|
-
"version":
|
|
50
|
+
"version": codeui.__version__,
|
|
50
51
|
"informationUri": "https://codeui.dev",
|
|
51
52
|
}
|
|
52
53
|
},
|