codeui-python 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codeui/__init__.py +23 -0
- codeui/agent/__init__.py +15 -0
- codeui/agent/context.py +207 -0
- codeui/agent/contribution.py +224 -0
- codeui/agent/edit_tools.py +280 -0
- codeui/agent/integrations.py +243 -0
- codeui/agent/mcp_server.py +94 -0
- codeui/analysis/__init__.py +1 -0
- codeui/analysis/api_drift.py +79 -0
- codeui/analysis/base.py +42 -0
- codeui/analysis/circular.py +116 -0
- codeui/analysis/dead_code.py +63 -0
- codeui/analysis/duplicates.py +69 -0
- codeui/analysis/runner.py +57 -0
- codeui/analysis/shadowing.py +49 -0
- codeui/analysis/undefined.py +97 -0
- codeui/analysis/unreachable.py +28 -0
- codeui/analysis/unresolved_import.py +153 -0
- codeui/cli/__init__.py +1 -0
- codeui/cli/main.py +437 -0
- codeui/core/__init__.py +1 -0
- codeui/core/cache.py +135 -0
- codeui/core/graph.py +435 -0
- codeui/core/ir.py +218 -0
- codeui/core/override.py +230 -0
- codeui/core/repo.py +85 -0
- codeui/core/resolver.py +281 -0
- codeui/errors.py +98 -0
- codeui/lang/__init__.py +1 -0
- codeui/lang/base.py +117 -0
- codeui/lang/generic.py +90 -0
- codeui/lang/go.py +167 -0
- codeui/lang/python.py +294 -0
- codeui/lang/registry.py +83 -0
- codeui/lang/rust.py +185 -0
- codeui/lang/ts.py +396 -0
- codeui/plugins/__init__.py +1 -0
- codeui/plugins/registry.py +31 -0
- codeui/report/__init__.py +1 -0
- codeui/report/json_emitter.py +24 -0
- codeui/report/markdown_emitter.py +33 -0
- codeui/report/sarif_emitter.py +56 -0
- codeui/server/__init__.py +1 -0
- codeui/server/server.py +705 -0
- codeui/tracer/__init__.py +1 -0
- codeui/tracer/tracer.py +147 -0
- codeui_python-0.1.0.dist-info/METADATA +191 -0
- codeui_python-0.1.0.dist-info/RECORD +52 -0
- codeui_python-0.1.0.dist-info/WHEEL +5 -0
- codeui_python-0.1.0.dist-info/entry_points.txt +2 -0
- codeui_python-0.1.0.dist-info/licenses/LICENSE +21 -0
- codeui_python-0.1.0.dist-info/top_level.txt +1 -0
codeui/lang/rust.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""Rust language adapter for codeui."""
|
|
2
|
+
import hashlib
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import ClassVar, Iterable, List, Sequence
|
|
6
|
+
from codeui.core.ir import Edge, EdgeKind, Location, Symbol, SymbolKind, Visibility
|
|
7
|
+
from codeui.core.resolver import ResolveContext
|
|
8
|
+
from codeui.lang.base import ImportRef, LanguageAnalyzer, ParseResult
|
|
9
|
+
|
|
10
|
+
class RustLanguageAnalyzer(LanguageAnalyzer):
|
|
11
|
+
"""Rust language analyzer.
|
|
12
|
+
Example:
|
|
13
|
+
>>> analyzer = RustLanguageAnalyzer()
|
|
14
|
+
>>> res = analyzer.parse(Path("main.rs"), 'fn main() { println!("Hello"); }')
|
|
15
|
+
>>> syms = list(analyzer.extract_symbols(res))
|
|
16
|
+
>>> len(syms) >= 1
|
|
17
|
+
True
|
|
18
|
+
"""
|
|
19
|
+
language: ClassVar[str] = "rust"
|
|
20
|
+
extensions: ClassVar[tuple[str, ...]] = (".rs",)
|
|
21
|
+
|
|
22
|
+
def parse(self, path: Path, source: str) -> ParseResult:
|
|
23
|
+
"""Parse Rust source file.
|
|
24
|
+
Example:
|
|
25
|
+
>>> analyzer = RustLanguageAnalyzer()
|
|
26
|
+
>>> res = analyzer.parse(Path("lib.rs"), "pub fn add(a: i32, b: i32) -> i32 { a + b }")
|
|
27
|
+
>>> res.errors
|
|
28
|
+
[]
|
|
29
|
+
"""
|
|
30
|
+
content_hash = hashlib.sha256(source.encode("utf-8")).hexdigest()
|
|
31
|
+
return ParseResult(
|
|
32
|
+
file_path=str(path),
|
|
33
|
+
source=source,
|
|
34
|
+
ast=source,
|
|
35
|
+
content_hash=content_hash,
|
|
36
|
+
errors=[],
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def extract_symbols(self, parse: ParseResult) -> Iterable[Symbol]:
|
|
40
|
+
"""Extract fn, struct, enum, trait, and mod symbols from Rust source.
|
|
41
|
+
Example:
|
|
42
|
+
>>> analyzer = RustLanguageAnalyzer()
|
|
43
|
+
>>> res = analyzer.parse(Path("main.rs"), "pub struct Config { pub port: u16 }")
|
|
44
|
+
>>> syms = list(analyzer.extract_symbols(res))
|
|
45
|
+
>>> any(s.kind == SymbolKind.STRUCT for s in syms)
|
|
46
|
+
True
|
|
47
|
+
"""
|
|
48
|
+
source = parse.source
|
|
49
|
+
file_path = parse.file_path
|
|
50
|
+
lines = source.splitlines()
|
|
51
|
+
max_line = len(lines) if lines else 1
|
|
52
|
+
symbols: List[Symbol] = []
|
|
53
|
+
file_sym = Symbol(
|
|
54
|
+
id=file_path,
|
|
55
|
+
name=Path(file_path).name,
|
|
56
|
+
qualified_name=file_path,
|
|
57
|
+
kind=SymbolKind.FILE,
|
|
58
|
+
language=self.language,
|
|
59
|
+
location=Location(file_path, 1, 0, max_line, 0),
|
|
60
|
+
parent_id=None,
|
|
61
|
+
signature=None,
|
|
62
|
+
visibility=Visibility.PUBLIC,
|
|
63
|
+
modifiers=(),
|
|
64
|
+
content_hash=parse.content_hash,
|
|
65
|
+
)
|
|
66
|
+
symbols.append(file_sym)
|
|
67
|
+
fn_pattern = re.compile(r'(?:pub(?:\([^)]+\))?\s+)?(?:async\s+)?fn\s+([A-Za-z0-9_]+)\s*(?:<[^>]+>)?\s*\(([^)]*)\)')
|
|
68
|
+
struct_pattern = re.compile(r'(?:pub(?:\([^)]+\))?\s+)?struct\s+([A-Za-z0-9_]+)(?:<[^>]+>)?')
|
|
69
|
+
enum_pattern = re.compile(r'(?:pub(?:\([^)]+\))?\s+)?enum\s+([A-Za-z0-9_]+)(?:<[^>]+>)?')
|
|
70
|
+
trait_pattern = re.compile(r'(?:pub(?:\([^)]+\))?\s+)?trait\s+([A-Za-z0-9_]+)(?:<[^>]+>)?')
|
|
71
|
+
for idx, line in enumerate(lines, start=1):
|
|
72
|
+
line_str = line.strip()
|
|
73
|
+
for match in fn_pattern.finditer(line_str):
|
|
74
|
+
name = match.group(1)
|
|
75
|
+
params = match.group(2)
|
|
76
|
+
sym_id = f"{file_path}::{name}"
|
|
77
|
+
end_line = self._find_block_end(lines, idx - 1)
|
|
78
|
+
loc = Location(file_path, idx, line.find(name), end_line, line.find(name) + len(name))
|
|
79
|
+
vis = Visibility.PUBLIC if "pub" in line_str else Visibility.PRIVATE
|
|
80
|
+
symbols.append(Symbol(
|
|
81
|
+
id=sym_id,
|
|
82
|
+
name=name,
|
|
83
|
+
qualified_name=name,
|
|
84
|
+
kind=SymbolKind.FUNCTION,
|
|
85
|
+
language=self.language,
|
|
86
|
+
location=loc,
|
|
87
|
+
parent_id=file_path,
|
|
88
|
+
signature=f"fn {name}({params})",
|
|
89
|
+
visibility=vis,
|
|
90
|
+
modifiers=("pub",) if "pub" in line_str else (),
|
|
91
|
+
content_hash=self._extract_body_hash(lines, idx - 1),
|
|
92
|
+
))
|
|
93
|
+
for match in struct_pattern.finditer(line_str):
|
|
94
|
+
name = match.group(1)
|
|
95
|
+
sym_id = f"{file_path}::{name}"
|
|
96
|
+
end_line = self._find_block_end(lines, idx - 1)
|
|
97
|
+
loc = Location(file_path, idx, line.find(name), end_line, line.find(name) + len(name))
|
|
98
|
+
vis = Visibility.PUBLIC if "pub" in line_str else Visibility.PRIVATE
|
|
99
|
+
symbols.append(Symbol(
|
|
100
|
+
id=sym_id,
|
|
101
|
+
name=name,
|
|
102
|
+
qualified_name=name,
|
|
103
|
+
kind=SymbolKind.STRUCT,
|
|
104
|
+
language=self.language,
|
|
105
|
+
location=loc,
|
|
106
|
+
parent_id=file_path,
|
|
107
|
+
signature=f"struct {name}",
|
|
108
|
+
visibility=vis,
|
|
109
|
+
modifiers=("pub",) if "pub" in line_str else (),
|
|
110
|
+
content_hash=self._extract_body_hash(lines, idx - 1),
|
|
111
|
+
))
|
|
112
|
+
for match in enum_pattern.finditer(line_str):
|
|
113
|
+
name = match.group(1)
|
|
114
|
+
sym_id = f"{file_path}::{name}"
|
|
115
|
+
end_line = self._find_block_end(lines, idx - 1)
|
|
116
|
+
loc = Location(file_path, idx, line.find(name), end_line, line.find(name) + len(name))
|
|
117
|
+
vis = Visibility.PUBLIC if "pub" in line_str else Visibility.PRIVATE
|
|
118
|
+
symbols.append(Symbol(
|
|
119
|
+
id=sym_id,
|
|
120
|
+
name=name,
|
|
121
|
+
qualified_name=name,
|
|
122
|
+
kind=SymbolKind.ENUM,
|
|
123
|
+
language=self.language,
|
|
124
|
+
location=loc,
|
|
125
|
+
parent_id=file_path,
|
|
126
|
+
signature=f"enum {name}",
|
|
127
|
+
visibility=vis,
|
|
128
|
+
modifiers=("pub",) if "pub" in line_str else (),
|
|
129
|
+
content_hash=self._extract_body_hash(lines, idx - 1),
|
|
130
|
+
))
|
|
131
|
+
for match in trait_pattern.finditer(line_str):
|
|
132
|
+
name = match.group(1)
|
|
133
|
+
sym_id = f"{file_path}::{name}"
|
|
134
|
+
end_line = self._find_block_end(lines, idx - 1)
|
|
135
|
+
loc = Location(file_path, idx, line.find(name), end_line, line.find(name) + len(name))
|
|
136
|
+
vis = Visibility.PUBLIC if "pub" in line_str else Visibility.PRIVATE
|
|
137
|
+
symbols.append(Symbol(
|
|
138
|
+
id=sym_id,
|
|
139
|
+
name=name,
|
|
140
|
+
qualified_name=name,
|
|
141
|
+
kind=SymbolKind.INTERFACE,
|
|
142
|
+
language=self.language,
|
|
143
|
+
location=loc,
|
|
144
|
+
parent_id=file_path,
|
|
145
|
+
signature=f"trait {name}",
|
|
146
|
+
visibility=vis,
|
|
147
|
+
modifiers=("pub",) if "pub" in line_str else (),
|
|
148
|
+
content_hash=self._extract_body_hash(lines, idx - 1),
|
|
149
|
+
))
|
|
150
|
+
return symbols
|
|
151
|
+
|
|
152
|
+
def extract_edges(self, parse: ParseResult, symbols: Sequence[Symbol]) -> Iterable[Edge]:
|
|
153
|
+
"""Extract relationship edges in Rust source.
|
|
154
|
+
Example:
|
|
155
|
+
>>> analyzer = RustLanguageAnalyzer()
|
|
156
|
+
>>> res = analyzer.parse(Path("lib.rs"), "fn foo() {}")
|
|
157
|
+
>>> syms = list(analyzer.extract_symbols(res))
|
|
158
|
+
>>> edges = list(analyzer.extract_edges(res, syms))
|
|
159
|
+
>>> len(edges) >= 1
|
|
160
|
+
True
|
|
161
|
+
"""
|
|
162
|
+
edges: List[Edge] = []
|
|
163
|
+
sym_map = {s.id: s for s in symbols}
|
|
164
|
+
for s in symbols:
|
|
165
|
+
if s.parent_id and s.parent_id in sym_map:
|
|
166
|
+
edges.append(Edge(
|
|
167
|
+
source_id=s.parent_id,
|
|
168
|
+
target_id=s.id,
|
|
169
|
+
kind=EdgeKind.CONTAINS,
|
|
170
|
+
weight=1.0,
|
|
171
|
+
confidence=1.0,
|
|
172
|
+
location=s.location,
|
|
173
|
+
))
|
|
174
|
+
return edges
|
|
175
|
+
|
|
176
|
+
def resolve_import(self, ref: ImportRef, ctx: ResolveContext) -> Iterable[str]:
|
|
177
|
+
"""Resolve Rust module references.
|
|
178
|
+
Example:
|
|
179
|
+
>>> analyzer = RustLanguageAnalyzer()
|
|
180
|
+
>>> ctx = ResolveContext(Path("."))
|
|
181
|
+
>>> ref = ImportRef("main.rs", "crate::utils", None, False)
|
|
182
|
+
>>> list(analyzer.resolve_import(ref, ctx))
|
|
183
|
+
[]
|
|
184
|
+
"""
|
|
185
|
+
return []
|
codeui/lang/ts.py
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
"""TypeScript/JavaScript language adapter for codeui."""
|
|
2
|
+
import hashlib
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import ClassVar, Iterable, List, Sequence
|
|
6
|
+
from codeui.core.ir import Edge, EdgeKind, Location, Symbol, SymbolKind, Visibility
|
|
7
|
+
from codeui.core.resolver import ResolveContext
|
|
8
|
+
from codeui.lang.base import ImportRef, LanguageAnalyzer, ParseResult
|
|
9
|
+
|
|
10
|
+
class TSLanguageAnalyzer(LanguageAnalyzer):
|
|
11
|
+
"""TypeScript and JavaScript language adapter.
|
|
12
|
+
Example:
|
|
13
|
+
>>> analyzer = TSLanguageAnalyzer()
|
|
14
|
+
>>> res = analyzer.parse(Path("app.ts"), "export function greet(name: string) { return 'hello ' + name; }")
|
|
15
|
+
>>> syms = list(analyzer.extract_symbols(res))
|
|
16
|
+
>>> len(syms) >= 2
|
|
17
|
+
True
|
|
18
|
+
"""
|
|
19
|
+
language: ClassVar[str] = "typescript"
|
|
20
|
+
extensions: ClassVar[tuple[str, ...]] = (".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs")
|
|
21
|
+
|
|
22
|
+
def parse(self, path: Path, source: str) -> ParseResult:
|
|
23
|
+
"""Parse TypeScript / JavaScript file source.
|
|
24
|
+
Example:
|
|
25
|
+
>>> analyzer = TSLanguageAnalyzer()
|
|
26
|
+
>>> res = analyzer.parse(Path("main.ts"), "const x = 10;")
|
|
27
|
+
>>> res.errors
|
|
28
|
+
[]
|
|
29
|
+
"""
|
|
30
|
+
content_hash = hashlib.sha256(source.encode("utf-8")).hexdigest()
|
|
31
|
+
return ParseResult(
|
|
32
|
+
file_path=str(path),
|
|
33
|
+
source=source,
|
|
34
|
+
ast=source,
|
|
35
|
+
content_hash=content_hash,
|
|
36
|
+
errors=[],
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def extract_symbols(self, parse: ParseResult) -> Iterable[Symbol]:
|
|
40
|
+
"""Extract functions, classes, interfaces, types, imports, and variables.
|
|
41
|
+
Example:
|
|
42
|
+
>>> analyzer = TSLanguageAnalyzer()
|
|
43
|
+
>>> res = analyzer.parse(Path("index.ts"), "class User { id: number; }")
|
|
44
|
+
>>> syms = list(analyzer.extract_symbols(res))
|
|
45
|
+
>>> any(s.kind == SymbolKind.CLASS for s in syms)
|
|
46
|
+
True
|
|
47
|
+
"""
|
|
48
|
+
source = parse.source
|
|
49
|
+
file_path = parse.file_path
|
|
50
|
+
lines = source.splitlines()
|
|
51
|
+
max_line = len(lines) if lines else 1
|
|
52
|
+
symbols: List[Symbol] = []
|
|
53
|
+
file_sym = Symbol(
|
|
54
|
+
id=file_path,
|
|
55
|
+
name=Path(file_path).name,
|
|
56
|
+
qualified_name=file_path,
|
|
57
|
+
kind=SymbolKind.FILE,
|
|
58
|
+
language=self.language,
|
|
59
|
+
location=Location(file_path, 1, 0, max_line, 0),
|
|
60
|
+
parent_id=None,
|
|
61
|
+
signature=None,
|
|
62
|
+
visibility=Visibility.PUBLIC,
|
|
63
|
+
modifiers=(),
|
|
64
|
+
content_hash=parse.content_hash,
|
|
65
|
+
)
|
|
66
|
+
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_$]+)(?:<[^>]+>)?')
|
|
69
|
+
type_pattern = re.compile(r'(?:export\s+)?type\s+([A-Za-z0-9_$]+)(?:<[^>]+>)?\s*=')
|
|
70
|
+
func_pattern = re.compile(r'(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s+([A-Za-z0-9_$]+)\s*(?:<[^>]+>)?\s*\(')
|
|
71
|
+
const_func_pattern = re.compile(r'(?:export\s+)?(?:default\s+)?(?:const|let|var)\s+([A-Za-z0-9_$]+)\s*=\s*(?:async\s*)?(?:<[^>]+>)?\s*\(')
|
|
72
|
+
const_var_pattern = re.compile(r'(?:export\s+)?(?:const|let|var)\s+([A-Za-z0-9_$]+)\s*=')
|
|
73
|
+
method_pattern = re.compile(r'^\s*(?:static\s+)?(?:async\s+)?([A-Za-z0-9_$]+)\s*(?:<[^>]+>)?\s*\(([^)]*)\)\s*\{')
|
|
74
|
+
import_pattern = re.compile(r'import\s+(?:\{([^}]+)\}|([A-Za-z0-9_$]+))\s+from\s+[\'"]([^\'"]+)[\'"]')
|
|
75
|
+
require_pattern = re.compile(r'(?:const|let|var)\s+(?:\{([^}]+)\}|([A-Za-z0-9_$]+))\s*=\s*require\s*\(\s*[\'"]([^\'"]+)[\'"]\s*\)')
|
|
76
|
+
exports_func_pattern = re.compile(r'(?:module\.)?exports(?:\.([A-Za-z0-9_$]+))?\s*=\s*(?:async\s*)?(?:function\s*)?(?:([A-Za-z0-9_$]+)\s*)?\(([^)]*)\)')
|
|
77
|
+
in_multiline = False
|
|
78
|
+
for idx, line in enumerate(lines, start=1):
|
|
79
|
+
line_str = line.strip()
|
|
80
|
+
clean_str_line = re.sub(r"'(?:\\.|[^'\\])*'", "''", line_str)
|
|
81
|
+
clean_str_line = re.sub(r'"(?:\\.|[^"\\])*"', '""', clean_str_line)
|
|
82
|
+
clean_str_line = re.sub(r'`(?:\\.|[^`\\])*`', '``', clean_str_line)
|
|
83
|
+
if in_multiline:
|
|
84
|
+
if "*/" in clean_str_line:
|
|
85
|
+
in_multiline = False
|
|
86
|
+
line_str = line_str.split("*/", 1)[1].strip()
|
|
87
|
+
else:
|
|
88
|
+
continue
|
|
89
|
+
if "/*" in clean_str_line:
|
|
90
|
+
if "*/" in clean_str_line:
|
|
91
|
+
line_str = re.sub(r'/\*.*?\*/', '', line_str).strip()
|
|
92
|
+
else:
|
|
93
|
+
in_multiline = True
|
|
94
|
+
line_str = line_str.split("/*", 1)[0].strip()
|
|
95
|
+
if not line_str or line_str.startswith("//") or line_str.startswith("*"):
|
|
96
|
+
continue
|
|
97
|
+
for match in class_pattern.finditer(line_str):
|
|
98
|
+
name = match.group(1)
|
|
99
|
+
sym_id = f"{file_path}::{name}"
|
|
100
|
+
end_line = self._find_block_end(lines, idx - 1)
|
|
101
|
+
loc = Location(file_path, idx, line.find(name), end_line, line.find(name) + len(name))
|
|
102
|
+
symbols.append(Symbol(
|
|
103
|
+
id=sym_id,
|
|
104
|
+
name=name,
|
|
105
|
+
qualified_name=name,
|
|
106
|
+
kind=SymbolKind.CLASS,
|
|
107
|
+
language=self.language,
|
|
108
|
+
location=loc,
|
|
109
|
+
parent_id=file_path,
|
|
110
|
+
signature=f"class {name}",
|
|
111
|
+
visibility=Visibility.PUBLIC if "export" in line_str else Visibility.INTERNAL,
|
|
112
|
+
modifiers=("export",) if "export" in line_str else (),
|
|
113
|
+
content_hash=self._extract_body_hash(lines, idx - 1),
|
|
114
|
+
))
|
|
115
|
+
for match in interface_pattern.finditer(line_str):
|
|
116
|
+
name = match.group(1)
|
|
117
|
+
sym_id = f"{file_path}::{name}"
|
|
118
|
+
end_line = self._find_block_end(lines, idx - 1)
|
|
119
|
+
loc = Location(file_path, idx, line.find(name), end_line, line.find(name) + len(name))
|
|
120
|
+
symbols.append(Symbol(
|
|
121
|
+
id=sym_id,
|
|
122
|
+
name=name,
|
|
123
|
+
qualified_name=name,
|
|
124
|
+
kind=SymbolKind.INTERFACE,
|
|
125
|
+
language=self.language,
|
|
126
|
+
location=loc,
|
|
127
|
+
parent_id=file_path,
|
|
128
|
+
signature=f"interface {name}",
|
|
129
|
+
visibility=Visibility.PUBLIC if "export" in line_str else Visibility.INTERNAL,
|
|
130
|
+
modifiers=("export",) if "export" in line_str else (),
|
|
131
|
+
content_hash=self._extract_body_hash(lines, idx - 1),
|
|
132
|
+
))
|
|
133
|
+
for match in type_pattern.finditer(line_str):
|
|
134
|
+
name = match.group(1)
|
|
135
|
+
sym_id = f"{file_path}::{name}"
|
|
136
|
+
loc = Location(file_path, idx, line.find(name), idx, line.find(name) + len(name))
|
|
137
|
+
symbols.append(Symbol(
|
|
138
|
+
id=sym_id,
|
|
139
|
+
name=name,
|
|
140
|
+
qualified_name=name,
|
|
141
|
+
kind=SymbolKind.TYPE_ALIAS,
|
|
142
|
+
language=self.language,
|
|
143
|
+
location=loc,
|
|
144
|
+
parent_id=file_path,
|
|
145
|
+
signature=f"type {name}",
|
|
146
|
+
visibility=Visibility.PUBLIC if "export" in line_str else Visibility.INTERNAL,
|
|
147
|
+
modifiers=("export",) if "export" in line_str else (),
|
|
148
|
+
content_hash=self._extract_body_hash(lines, idx - 1),
|
|
149
|
+
))
|
|
150
|
+
for match in func_pattern.finditer(line_str):
|
|
151
|
+
name = match.group(1)
|
|
152
|
+
if name:
|
|
153
|
+
sym_id = f"{file_path}::{name}"
|
|
154
|
+
end_line = self._find_block_end(lines, idx - 1)
|
|
155
|
+
loc = Location(file_path, idx, line.find(name), end_line, line.find(name) + len(name))
|
|
156
|
+
symbols.append(Symbol(
|
|
157
|
+
id=sym_id,
|
|
158
|
+
name=name,
|
|
159
|
+
qualified_name=name,
|
|
160
|
+
kind=SymbolKind.FUNCTION,
|
|
161
|
+
language=self.language,
|
|
162
|
+
location=loc,
|
|
163
|
+
parent_id=file_path,
|
|
164
|
+
signature=f"function {name}(...)",
|
|
165
|
+
visibility=Visibility.PUBLIC if "export" in line_str else Visibility.INTERNAL,
|
|
166
|
+
modifiers=("export",) if "export" in line_str else (),
|
|
167
|
+
content_hash=self._extract_body_hash(lines, idx - 1),
|
|
168
|
+
))
|
|
169
|
+
for match in const_func_pattern.finditer(line_str):
|
|
170
|
+
name = match.group(1)
|
|
171
|
+
if name:
|
|
172
|
+
sym_id = f"{file_path}::{name}"
|
|
173
|
+
end_line = self._find_block_end(lines, idx - 1)
|
|
174
|
+
loc = Location(file_path, idx, line.find(name), end_line, line.find(name) + len(name))
|
|
175
|
+
symbols.append(Symbol(
|
|
176
|
+
id=sym_id,
|
|
177
|
+
name=name,
|
|
178
|
+
qualified_name=name,
|
|
179
|
+
kind=SymbolKind.FUNCTION,
|
|
180
|
+
language=self.language,
|
|
181
|
+
location=loc,
|
|
182
|
+
parent_id=file_path,
|
|
183
|
+
signature=f"const {name} = (...)",
|
|
184
|
+
visibility=Visibility.PUBLIC if "export" in line_str else Visibility.INTERNAL,
|
|
185
|
+
modifiers=("export",) if "export" in line_str else (),
|
|
186
|
+
content_hash=self._extract_body_hash(lines, idx - 1),
|
|
187
|
+
))
|
|
188
|
+
for match in const_var_pattern.finditer(line_str):
|
|
189
|
+
name = match.group(1)
|
|
190
|
+
if name and name not in ("if", "for", "while", "switch", "catch", "function"):
|
|
191
|
+
sym_id = f"{file_path}::{name}"
|
|
192
|
+
loc = Location(file_path, idx, line.find(name), idx, line.find(name) + len(name))
|
|
193
|
+
symbols.append(Symbol(
|
|
194
|
+
id=sym_id,
|
|
195
|
+
name=name,
|
|
196
|
+
qualified_name=name,
|
|
197
|
+
kind=SymbolKind.VARIABLE,
|
|
198
|
+
language=self.language,
|
|
199
|
+
location=loc,
|
|
200
|
+
parent_id=file_path,
|
|
201
|
+
signature=f"var {name}",
|
|
202
|
+
visibility=Visibility.PUBLIC if "export" in line_str else Visibility.INTERNAL,
|
|
203
|
+
modifiers=("export",) if "export" in line_str else (),
|
|
204
|
+
content_hash=hashlib.sha256(line_str.encode()).hexdigest()[:16],
|
|
205
|
+
))
|
|
206
|
+
for match in method_pattern.finditer(line_str):
|
|
207
|
+
name = match.group(1)
|
|
208
|
+
params = match.group(2)
|
|
209
|
+
if name not in ("if", "for", "while", "switch", "catch", "function", "constructor"):
|
|
210
|
+
sym_id = f"{file_path}::{name}"
|
|
211
|
+
loc = Location(file_path, idx, line.find(name), idx, line.find(name) + len(name))
|
|
212
|
+
symbols.append(Symbol(
|
|
213
|
+
id=sym_id,
|
|
214
|
+
name=name,
|
|
215
|
+
qualified_name=name,
|
|
216
|
+
kind=SymbolKind.METHOD,
|
|
217
|
+
language=self.language,
|
|
218
|
+
location=loc,
|
|
219
|
+
parent_id=file_path,
|
|
220
|
+
signature=f"{name}({params})",
|
|
221
|
+
visibility=Visibility.PUBLIC if "export" in line_str or "static" in line_str else Visibility.INTERNAL,
|
|
222
|
+
modifiers=("static",) if "static" in line_str else (),
|
|
223
|
+
content_hash=self._extract_body_hash(lines, idx - 1),
|
|
224
|
+
))
|
|
225
|
+
for match in import_pattern.finditer(line_str):
|
|
226
|
+
spec = match.group(1) or match.group(2) or ""
|
|
227
|
+
target = match.group(3)
|
|
228
|
+
sym_id = f"{file_path}::import::{target}"
|
|
229
|
+
loc = Location(file_path, idx, 0, idx, len(line_str))
|
|
230
|
+
symbols.append(Symbol(
|
|
231
|
+
id=sym_id,
|
|
232
|
+
name=spec.strip(),
|
|
233
|
+
qualified_name=f"import:{target}",
|
|
234
|
+
kind=SymbolKind.IMPORT,
|
|
235
|
+
language=self.language,
|
|
236
|
+
location=loc,
|
|
237
|
+
parent_id=file_path,
|
|
238
|
+
signature=line_str,
|
|
239
|
+
visibility=Visibility.INTERNAL,
|
|
240
|
+
modifiers=(),
|
|
241
|
+
content_hash=hashlib.sha256(line_str.encode()).hexdigest()[:16],
|
|
242
|
+
))
|
|
243
|
+
for match in require_pattern.finditer(line_str):
|
|
244
|
+
spec = match.group(1) or match.group(2) or ""
|
|
245
|
+
target = match.group(3)
|
|
246
|
+
sym_id = f"{file_path}::import::{target}"
|
|
247
|
+
loc = Location(file_path, idx, 0, idx, len(line_str))
|
|
248
|
+
symbols.append(Symbol(
|
|
249
|
+
id=sym_id,
|
|
250
|
+
name=spec.strip(),
|
|
251
|
+
qualified_name=f"import:{target}",
|
|
252
|
+
kind=SymbolKind.IMPORT,
|
|
253
|
+
language=self.language,
|
|
254
|
+
location=loc,
|
|
255
|
+
parent_id=file_path,
|
|
256
|
+
signature=line_str,
|
|
257
|
+
visibility=Visibility.INTERNAL,
|
|
258
|
+
modifiers=(),
|
|
259
|
+
content_hash=hashlib.sha256(line_str.encode()).hexdigest()[:16],
|
|
260
|
+
))
|
|
261
|
+
for match in exports_func_pattern.finditer(line_str):
|
|
262
|
+
name = match.group(1) or match.group(2)
|
|
263
|
+
if name and name not in ("if", "for", "while", "switch", "catch"):
|
|
264
|
+
params = match.group(3) or ""
|
|
265
|
+
sym_id = f"{file_path}::{name}"
|
|
266
|
+
loc = Location(file_path, idx, line.find(name), idx, line.find(name) + len(name))
|
|
267
|
+
symbols.append(Symbol(
|
|
268
|
+
id=sym_id,
|
|
269
|
+
name=name,
|
|
270
|
+
qualified_name=name,
|
|
271
|
+
kind=SymbolKind.FUNCTION,
|
|
272
|
+
language=self.language,
|
|
273
|
+
location=loc,
|
|
274
|
+
parent_id=file_path,
|
|
275
|
+
signature=f"exports.{name} = ({params})",
|
|
276
|
+
visibility=Visibility.PUBLIC,
|
|
277
|
+
modifiers=("export",),
|
|
278
|
+
content_hash=self._extract_body_hash(lines, idx - 1),
|
|
279
|
+
))
|
|
280
|
+
return symbols
|
|
281
|
+
|
|
282
|
+
def extract_edges(self, parse: ParseResult, symbols: Sequence[Symbol]) -> Iterable[Edge]:
|
|
283
|
+
"""Extract relationship edges for TypeScript / JavaScript AST.
|
|
284
|
+
Example:
|
|
285
|
+
>>> analyzer = TSLanguageAnalyzer()
|
|
286
|
+
>>> res = analyzer.parse(Path("a.ts"), "import { b } from './b'; function a() { b(); }")
|
|
287
|
+
>>> syms = list(analyzer.extract_symbols(res))
|
|
288
|
+
>>> edges = list(analyzer.extract_edges(res, syms))
|
|
289
|
+
>>> len(edges) >= 1
|
|
290
|
+
True
|
|
291
|
+
"""
|
|
292
|
+
edges: List[Edge] = []
|
|
293
|
+
file_path = parse.file_path
|
|
294
|
+
sym_map = {s.id: s for s in symbols}
|
|
295
|
+
for s in symbols:
|
|
296
|
+
if s.parent_id and s.parent_id in sym_map:
|
|
297
|
+
edges.append(Edge(
|
|
298
|
+
source_id=s.parent_id,
|
|
299
|
+
target_id=s.id,
|
|
300
|
+
kind=EdgeKind.CONTAINS,
|
|
301
|
+
weight=1.0,
|
|
302
|
+
confidence=1.0,
|
|
303
|
+
location=s.location,
|
|
304
|
+
))
|
|
305
|
+
if s.kind == SymbolKind.IMPORT:
|
|
306
|
+
target_mod = s.qualified_name.replace("import:", "")
|
|
307
|
+
weight = float(self._count_symbol_usages(parse.source, s.name))
|
|
308
|
+
edges.append(Edge(
|
|
309
|
+
source_id=file_path,
|
|
310
|
+
target_id=f"module::{target_mod}",
|
|
311
|
+
kind=EdgeKind.IMPORTS,
|
|
312
|
+
weight=weight,
|
|
313
|
+
confidence=1.0,
|
|
314
|
+
location=s.location,
|
|
315
|
+
))
|
|
316
|
+
call_pattern = re.compile(r'(?<![A-Za-z0-9_$-])\b([A-Za-z_$][A-Za-z0-9_$]*)\s*\(')
|
|
317
|
+
js_keywords = {
|
|
318
|
+
"if", "for", "while", "switch", "catch", "function", "var", "let", "const",
|
|
319
|
+
"return", "throw", "new", "typeof", "instanceof", "yield", "await", "async",
|
|
320
|
+
"import", "export", "constructor", "super", "class", "case", "default",
|
|
321
|
+
"else", "try", "finally", "break", "continue", "in", "of", "void", "delete", "require"
|
|
322
|
+
}
|
|
323
|
+
lines = parse.source.splitlines()
|
|
324
|
+
func_syms = [s for s in symbols if s.kind in (SymbolKind.FUNCTION, SymbolKind.METHOD, SymbolKind.CLASS)]
|
|
325
|
+
in_multiline_edges = False
|
|
326
|
+
for idx, line in enumerate(lines, start=1):
|
|
327
|
+
line_str = line.strip()
|
|
328
|
+
clean_str_line = re.sub(r"'(?:\\.|[^'\\])*'", "''", line_str)
|
|
329
|
+
clean_str_line = re.sub(r'"(?:\\.|[^"\\])*"', '""', clean_str_line)
|
|
330
|
+
clean_str_line = re.sub(r'`(?:\\.|[^`\\])*`', '``', clean_str_line)
|
|
331
|
+
if in_multiline_edges:
|
|
332
|
+
if "*/" in clean_str_line:
|
|
333
|
+
in_multiline_edges = False
|
|
334
|
+
line_str = line_str.split("*/", 1)[1].strip()
|
|
335
|
+
else:
|
|
336
|
+
continue
|
|
337
|
+
if "/*" in clean_str_line:
|
|
338
|
+
if "*/" in clean_str_line:
|
|
339
|
+
line_str = re.sub(r'/\*.*?\*/', '', line_str).strip()
|
|
340
|
+
else:
|
|
341
|
+
in_multiline_edges = True
|
|
342
|
+
line_str = line_str.split("/*", 1)[0].strip()
|
|
343
|
+
if not line_str or line_str.startswith("//") or line_str.startswith("*") or "var(" in line_str:
|
|
344
|
+
continue
|
|
345
|
+
enclosing = None
|
|
346
|
+
for s in func_syms:
|
|
347
|
+
if s.location and s.location.start_line <= idx <= (s.location.end_line or (s.location.start_line + 50)):
|
|
348
|
+
enclosing = s.id
|
|
349
|
+
caller_id = enclosing or file_path
|
|
350
|
+
clean_code_line = re.sub(r"'(?:\\.|[^'\\])*'", "''", line_str)
|
|
351
|
+
clean_code_line = re.sub(r'"(?:\\.|[^"\\])*"', '""', clean_code_line)
|
|
352
|
+
clean_code_line = re.sub(r'`(?:\\.|[^`\\])*`', '``', clean_code_line)
|
|
353
|
+
clean_code_line = re.sub(r'/(?:\\.|[^/\\])+/[gimsuy]*', '//', clean_code_line)
|
|
354
|
+
for match in call_pattern.finditer(clean_code_line):
|
|
355
|
+
called_name = match.group(1)
|
|
356
|
+
pre_str = clean_code_line[:match.start(1)].rstrip()
|
|
357
|
+
is_member_call = pre_str.endswith(".") or pre_str.endswith("?.")
|
|
358
|
+
if called_name[0].isupper() and not pre_str.endswith("new") and not pre_str.endswith("function"):
|
|
359
|
+
continue
|
|
360
|
+
if called_name not in js_keywords and not is_member_call and called_name not in ("translateY", "translateX", "scale", "rotate"):
|
|
361
|
+
col = line.find(called_name)
|
|
362
|
+
weight = float(self._count_symbol_usages(parse.source, called_name))
|
|
363
|
+
edges.append(Edge(
|
|
364
|
+
source_id=caller_id,
|
|
365
|
+
target_id=f"{file_path}::{called_name}",
|
|
366
|
+
kind=EdgeKind.CALLS,
|
|
367
|
+
weight=weight,
|
|
368
|
+
confidence=0.75,
|
|
369
|
+
location=Location(file_path, idx, col if col >= 0 else 0, idx, col + len(called_name) if col >= 0 else len(called_name)),
|
|
370
|
+
))
|
|
371
|
+
return edges
|
|
372
|
+
|
|
373
|
+
def _count_symbol_usages(self, source: str, symbol_name: str) -> int:
|
|
374
|
+
if not symbol_name or symbol_name == "*":
|
|
375
|
+
return 1
|
|
376
|
+
cleaned_symbols = [s.strip() for s in symbol_name.split(",") if s.strip()]
|
|
377
|
+
total = 0
|
|
378
|
+
for sym in (cleaned_symbols or [symbol_name]):
|
|
379
|
+
pattern = r"\b" + re.escape(sym) + r"\b"
|
|
380
|
+
for line in source.splitlines():
|
|
381
|
+
stripped = line.strip()
|
|
382
|
+
if stripped.startswith(("import ", "export ", "//", "/*")):
|
|
383
|
+
continue
|
|
384
|
+
total += len(re.findall(pattern, line))
|
|
385
|
+
return max(1, total)
|
|
386
|
+
|
|
387
|
+
def resolve_import(self, ref: ImportRef, ctx: ResolveContext) -> Iterable[str]:
|
|
388
|
+
"""Resolve JS/TS import reference paths via resolve_path.
|
|
389
|
+
Example:
|
|
390
|
+
>>> analyzer = TSLanguageAnalyzer()
|
|
391
|
+
>>> ctx = ResolveContext(Path("."))
|
|
392
|
+
>>> ref = ImportRef("src/a.ts", "./b", None, True)
|
|
393
|
+
>>> list(analyzer.resolve_import(ref, ctx))
|
|
394
|
+
[]
|
|
395
|
+
"""
|
|
396
|
+
return ctx.resolve_path(ref.file_path, ref.module_name, "typescript")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Plugin registry package for codeui."""
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Plugin entry-point registry for third-party adapters and analyzers."""
|
|
2
|
+
import importlib.metadata
|
|
3
|
+
from typing import Any, List
|
|
4
|
+
|
|
5
|
+
class PluginRegistry:
|
|
6
|
+
"""Manages discovery and loading of entrypoint-based codeui plugins.
|
|
7
|
+
Example:
|
|
8
|
+
>>> reg = PluginRegistry()
|
|
9
|
+
>>> isinstance(reg.load_plugins(), list)
|
|
10
|
+
True
|
|
11
|
+
"""
|
|
12
|
+
def load_plugins(self) -> List[Any]:
|
|
13
|
+
"""Load external plugins registered under 'codeui.plugins' entrypoint group.
|
|
14
|
+
Example:
|
|
15
|
+
>>> reg = PluginRegistry()
|
|
16
|
+
>>> plugins = reg.load_plugins()
|
|
17
|
+
>>> isinstance(plugins, list)
|
|
18
|
+
True
|
|
19
|
+
"""
|
|
20
|
+
loaded = []
|
|
21
|
+
try:
|
|
22
|
+
entry_points = importlib.metadata.entry_points()
|
|
23
|
+
if hasattr(entry_points, "select"):
|
|
24
|
+
eps = entry_points.select(group="codeui.plugins")
|
|
25
|
+
else:
|
|
26
|
+
eps = entry_points.get("codeui.plugins", [])
|
|
27
|
+
for ep in eps:
|
|
28
|
+
loaded.append(ep.load())
|
|
29
|
+
except Exception:
|
|
30
|
+
pass
|
|
31
|
+
return loaded
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Report emitters package for codeui."""
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Deterministic JSON report emitter."""
|
|
2
|
+
import json
|
|
3
|
+
from typing import Any, Dict
|
|
4
|
+
from codeui.core.graph import Graph
|
|
5
|
+
|
|
6
|
+
class JSONEmitter:
|
|
7
|
+
"""Emits byte-identical deterministic JSON graph reports.
|
|
8
|
+
Example:
|
|
9
|
+
>>> g = Graph()
|
|
10
|
+
>>> emitter = JSONEmitter()
|
|
11
|
+
>>> report = emitter.emit(g)
|
|
12
|
+
>>> '"symbols": []' in report
|
|
13
|
+
True
|
|
14
|
+
"""
|
|
15
|
+
def emit(self, graph: Graph) -> str:
|
|
16
|
+
"""Serialize graph to byte-identical sorted JSON string.
|
|
17
|
+
Example:
|
|
18
|
+
>>> g = Graph()
|
|
19
|
+
>>> emitter = JSONEmitter()
|
|
20
|
+
>>> isinstance(emitter.emit(g), str)
|
|
21
|
+
True
|
|
22
|
+
"""
|
|
23
|
+
data = graph.to_dict()
|
|
24
|
+
return json.dumps(data, sort_keys=True, indent=2)
|