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/go.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Go 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 GoLanguageAnalyzer(LanguageAnalyzer):
|
|
11
|
+
"""Go language analyzer.
|
|
12
|
+
Example:
|
|
13
|
+
>>> analyzer = GoLanguageAnalyzer()
|
|
14
|
+
>>> res = analyzer.parse(Path("main.go"), "package main\\nfunc main() {}")
|
|
15
|
+
>>> syms = list(analyzer.extract_symbols(res))
|
|
16
|
+
>>> len(syms) >= 1
|
|
17
|
+
True
|
|
18
|
+
"""
|
|
19
|
+
language: ClassVar[str] = "go"
|
|
20
|
+
extensions: ClassVar[tuple[str, ...]] = (".go",)
|
|
21
|
+
|
|
22
|
+
def parse(self, path: Path, source: str) -> ParseResult:
|
|
23
|
+
"""Parse Go source text.
|
|
24
|
+
Example:
|
|
25
|
+
>>> analyzer = GoLanguageAnalyzer()
|
|
26
|
+
>>> res = analyzer.parse(Path("main.go"), "package main")
|
|
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 Go packages, functions, structs, interfaces, and imports.
|
|
41
|
+
Example:
|
|
42
|
+
>>> analyzer = GoLanguageAnalyzer()
|
|
43
|
+
>>> res = analyzer.parse(Path("main.go"), "package main\\ntype User struct{ Name string }")
|
|
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
|
+
func_pattern = re.compile(r'func\s+(?:\(([^)]+)\)\s+)?([A-Za-z0-9_]+)\s*(?:\[[^\]]+\])?\s*\(([^)]*)\)')
|
|
68
|
+
struct_pattern = re.compile(r'type\s+([A-Za-z0-9_]+)\s*(?:\[[^\]]+\])?\s+struct\b')
|
|
69
|
+
interface_pattern = re.compile(r'type\s+([A-Za-z0-9_]+)\s*(?:\[[^\]]+\])?\s+interface\b')
|
|
70
|
+
for idx, line in enumerate(lines, start=1):
|
|
71
|
+
line_str = line.strip()
|
|
72
|
+
for match in func_pattern.finditer(line_str):
|
|
73
|
+
receiver = match.group(1)
|
|
74
|
+
name = match.group(2)
|
|
75
|
+
params = match.group(3)
|
|
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 name[0].isupper() else Visibility.PRIVATE
|
|
80
|
+
kind = SymbolKind.METHOD if receiver else SymbolKind.FUNCTION
|
|
81
|
+
symbols.append(Symbol(
|
|
82
|
+
id=sym_id,
|
|
83
|
+
name=name,
|
|
84
|
+
qualified_name=f"{receiver}.{name}" if receiver else name,
|
|
85
|
+
kind=kind,
|
|
86
|
+
language=self.language,
|
|
87
|
+
location=loc,
|
|
88
|
+
parent_id=file_path,
|
|
89
|
+
signature=f"func ({receiver or ''}) {name}({params})",
|
|
90
|
+
visibility=vis,
|
|
91
|
+
modifiers=(),
|
|
92
|
+
content_hash=self._extract_body_hash(lines, idx - 1),
|
|
93
|
+
))
|
|
94
|
+
for match in struct_pattern.finditer(line_str):
|
|
95
|
+
name = match.group(1)
|
|
96
|
+
sym_id = f"{file_path}::{name}"
|
|
97
|
+
end_line = self._find_block_end(lines, idx - 1)
|
|
98
|
+
loc = Location(file_path, idx, line.find(name), end_line, line.find(name) + len(name))
|
|
99
|
+
vis = Visibility.PUBLIC if name[0].isupper() else Visibility.PRIVATE
|
|
100
|
+
symbols.append(Symbol(
|
|
101
|
+
id=sym_id,
|
|
102
|
+
name=name,
|
|
103
|
+
qualified_name=name,
|
|
104
|
+
kind=SymbolKind.STRUCT,
|
|
105
|
+
language=self.language,
|
|
106
|
+
location=loc,
|
|
107
|
+
parent_id=file_path,
|
|
108
|
+
signature=f"type {name} struct",
|
|
109
|
+
visibility=vis,
|
|
110
|
+
modifiers=(),
|
|
111
|
+
content_hash=self._extract_body_hash(lines, idx - 1),
|
|
112
|
+
))
|
|
113
|
+
for match in interface_pattern.finditer(line_str):
|
|
114
|
+
name = match.group(1)
|
|
115
|
+
sym_id = f"{file_path}::{name}"
|
|
116
|
+
end_line = self._find_block_end(lines, idx - 1)
|
|
117
|
+
loc = Location(file_path, idx, line.find(name), end_line, line.find(name) + len(name))
|
|
118
|
+
vis = Visibility.PUBLIC if name[0].isupper() else Visibility.PRIVATE
|
|
119
|
+
symbols.append(Symbol(
|
|
120
|
+
id=sym_id,
|
|
121
|
+
name=name,
|
|
122
|
+
qualified_name=name,
|
|
123
|
+
kind=SymbolKind.INTERFACE,
|
|
124
|
+
language=self.language,
|
|
125
|
+
location=loc,
|
|
126
|
+
parent_id=file_path,
|
|
127
|
+
signature=f"type {name} interface",
|
|
128
|
+
visibility=vis,
|
|
129
|
+
modifiers=(),
|
|
130
|
+
content_hash=self._extract_body_hash(lines, idx - 1),
|
|
131
|
+
))
|
|
132
|
+
return symbols
|
|
133
|
+
|
|
134
|
+
def extract_edges(self, parse: ParseResult, symbols: Sequence[Symbol]) -> Iterable[Edge]:
|
|
135
|
+
"""Extract containment and call relationships in Go.
|
|
136
|
+
Example:
|
|
137
|
+
>>> analyzer = GoLanguageAnalyzer()
|
|
138
|
+
>>> res = analyzer.parse(Path("main.go"), "package main\\nfunc main() {}")
|
|
139
|
+
>>> syms = list(analyzer.extract_symbols(res))
|
|
140
|
+
>>> edges = list(analyzer.extract_edges(res, syms))
|
|
141
|
+
>>> len(edges) >= 1
|
|
142
|
+
True
|
|
143
|
+
"""
|
|
144
|
+
edges: List[Edge] = []
|
|
145
|
+
sym_map = {s.id: s for s in symbols}
|
|
146
|
+
for s in symbols:
|
|
147
|
+
if s.parent_id and s.parent_id in sym_map:
|
|
148
|
+
edges.append(Edge(
|
|
149
|
+
source_id=s.parent_id,
|
|
150
|
+
target_id=s.id,
|
|
151
|
+
kind=EdgeKind.CONTAINS,
|
|
152
|
+
weight=1.0,
|
|
153
|
+
confidence=1.0,
|
|
154
|
+
location=s.location,
|
|
155
|
+
))
|
|
156
|
+
return edges
|
|
157
|
+
|
|
158
|
+
def resolve_import(self, ref: ImportRef, ctx: ResolveContext) -> Iterable[str]:
|
|
159
|
+
"""Resolve Go module import references.
|
|
160
|
+
Example:
|
|
161
|
+
>>> analyzer = GoLanguageAnalyzer()
|
|
162
|
+
>>> ctx = ResolveContext(Path("."))
|
|
163
|
+
>>> ref = ImportRef("main.go", "fmt", None, False)
|
|
164
|
+
>>> list(analyzer.resolve_import(ref, ctx))
|
|
165
|
+
[]
|
|
166
|
+
"""
|
|
167
|
+
return []
|
codeui/lang/python.py
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
"""Python language adapter using standard library ast."""
|
|
2
|
+
import ast
|
|
3
|
+
import hashlib
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, ClassVar, Dict, Iterable, List, Optional, Sequence, Tuple
|
|
7
|
+
from codeui.core.ir import Edge, EdgeKind, Location, Symbol, SymbolKind, Visibility
|
|
8
|
+
from codeui.core.resolver import ResolveContext
|
|
9
|
+
from codeui.lang.base import ImportRef, LanguageAnalyzer, ParseResult
|
|
10
|
+
|
|
11
|
+
class PythonLanguageAnalyzer(LanguageAnalyzer):
|
|
12
|
+
"""Python language analyzer using standard library ast.
|
|
13
|
+
Example:
|
|
14
|
+
>>> analyzer = PythonLanguageAnalyzer()
|
|
15
|
+
>>> res = analyzer.parse(Path("test.py"), "def hello(): pass")
|
|
16
|
+
>>> syms = list(analyzer.extract_symbols(res))
|
|
17
|
+
>>> len(syms) >= 1
|
|
18
|
+
True
|
|
19
|
+
"""
|
|
20
|
+
language: ClassVar[str] = "python"
|
|
21
|
+
extensions: ClassVar[tuple[str, ...]] = (".py", ".pyi")
|
|
22
|
+
|
|
23
|
+
def parse(self, path: Path, source: str) -> ParseResult:
|
|
24
|
+
"""Parse Python source text into ast.AST object.
|
|
25
|
+
Example:
|
|
26
|
+
>>> analyzer = PythonLanguageAnalyzer()
|
|
27
|
+
>>> res = analyzer.parse(Path("a.py"), "x = 1")
|
|
28
|
+
>>> res.errors
|
|
29
|
+
[]
|
|
30
|
+
"""
|
|
31
|
+
content_hash = hashlib.sha256(source.encode("utf-8")).hexdigest()
|
|
32
|
+
errors: List[str] = []
|
|
33
|
+
parsed_ast: Any = None
|
|
34
|
+
try:
|
|
35
|
+
parsed_ast = ast.parse(source, filename=str(path))
|
|
36
|
+
except SyntaxError as e:
|
|
37
|
+
errors.append(f"SyntaxError at line {e.lineno}, col {e.offset}: {e.msg}")
|
|
38
|
+
return ParseResult(
|
|
39
|
+
file_path=str(path),
|
|
40
|
+
source=source,
|
|
41
|
+
ast=parsed_ast,
|
|
42
|
+
content_hash=content_hash,
|
|
43
|
+
errors=errors,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def extract_symbols(self, parse: ParseResult) -> Iterable[Symbol]:
|
|
47
|
+
"""Extract all symbols from parsed Python AST.
|
|
48
|
+
Example:
|
|
49
|
+
>>> analyzer = PythonLanguageAnalyzer()
|
|
50
|
+
>>> res = analyzer.parse(Path("a.py"), "def foo(): pass")
|
|
51
|
+
>>> syms = list(analyzer.extract_symbols(res))
|
|
52
|
+
>>> syms[0].kind == SymbolKind.FILE
|
|
53
|
+
True
|
|
54
|
+
"""
|
|
55
|
+
if parse.ast is None:
|
|
56
|
+
return []
|
|
57
|
+
symbols: List[Symbol] = []
|
|
58
|
+
file_path = parse.file_path
|
|
59
|
+
lines = parse.source.splitlines()
|
|
60
|
+
max_line = len(lines) if lines else 1
|
|
61
|
+
file_sym = Symbol(
|
|
62
|
+
id=file_path,
|
|
63
|
+
name=Path(file_path).name,
|
|
64
|
+
qualified_name=file_path,
|
|
65
|
+
kind=SymbolKind.FILE,
|
|
66
|
+
language=self.language,
|
|
67
|
+
location=Location(file_path, 1, 0, max_line, 0),
|
|
68
|
+
parent_id=None,
|
|
69
|
+
signature=None,
|
|
70
|
+
visibility=Visibility.PUBLIC,
|
|
71
|
+
modifiers=(),
|
|
72
|
+
content_hash=parse.content_hash,
|
|
73
|
+
)
|
|
74
|
+
symbols.append(file_sym)
|
|
75
|
+
self._walk_ast(parse.ast, file_path, file_path, "", symbols)
|
|
76
|
+
return symbols
|
|
77
|
+
|
|
78
|
+
def _walk_ast(self, node: ast.AST, file_path: str, parent_id: str, scope_prefix: str, symbols: List[Symbol]) -> None:
|
|
79
|
+
"""Traverse AST nodes recursively to construct Symbol records."""
|
|
80
|
+
for child in ast.iter_child_nodes(node):
|
|
81
|
+
if isinstance(child, ast.ClassDef):
|
|
82
|
+
qname = f"{scope_prefix}.{child.name}" if scope_prefix else child.name
|
|
83
|
+
sym_id = f"{file_path}::{qname}"
|
|
84
|
+
loc = Location(
|
|
85
|
+
file_id=file_path,
|
|
86
|
+
start_line=child.lineno,
|
|
87
|
+
start_col=child.col_offset,
|
|
88
|
+
end_line=getattr(child, "end_lineno", child.lineno),
|
|
89
|
+
end_col=getattr(child, "end_col_offset", child.col_offset),
|
|
90
|
+
)
|
|
91
|
+
vis = Visibility.PRIVATE if child.name.startswith("_") else Visibility.PUBLIC
|
|
92
|
+
body_str = ast.dump(child)
|
|
93
|
+
s = Symbol(
|
|
94
|
+
id=sym_id,
|
|
95
|
+
name=child.name,
|
|
96
|
+
qualified_name=qname,
|
|
97
|
+
kind=SymbolKind.CLASS,
|
|
98
|
+
language=self.language,
|
|
99
|
+
location=loc,
|
|
100
|
+
parent_id=parent_id,
|
|
101
|
+
signature=f"class {child.name}",
|
|
102
|
+
visibility=vis,
|
|
103
|
+
modifiers=(),
|
|
104
|
+
content_hash=hashlib.sha256(body_str.encode()).hexdigest()[:16],
|
|
105
|
+
)
|
|
106
|
+
symbols.append(s)
|
|
107
|
+
self._walk_ast(child, file_path, sym_id, qname, symbols)
|
|
108
|
+
elif isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
109
|
+
qname = f"{scope_prefix}.{child.name}" if scope_prefix else child.name
|
|
110
|
+
sym_id = f"{file_path}::{qname}"
|
|
111
|
+
kind = SymbolKind.METHOD if scope_prefix and "." in scope_prefix or any(s.kind == SymbolKind.CLASS for s in symbols if s.id == parent_id) else SymbolKind.FUNCTION
|
|
112
|
+
loc = Location(
|
|
113
|
+
file_id=file_path,
|
|
114
|
+
start_line=child.lineno,
|
|
115
|
+
start_col=child.col_offset,
|
|
116
|
+
end_line=getattr(child, "end_lineno", child.lineno),
|
|
117
|
+
end_col=getattr(child, "end_col_offset", child.col_offset),
|
|
118
|
+
)
|
|
119
|
+
vis = Visibility.PRIVATE if child.name.startswith("_") else Visibility.PUBLIC
|
|
120
|
+
body_str = ast.dump(child)
|
|
121
|
+
s = Symbol(
|
|
122
|
+
id=sym_id,
|
|
123
|
+
name=child.name,
|
|
124
|
+
qualified_name=qname,
|
|
125
|
+
kind=kind,
|
|
126
|
+
language=self.language,
|
|
127
|
+
location=loc,
|
|
128
|
+
parent_id=parent_id,
|
|
129
|
+
signature=f"def {child.name}(...)",
|
|
130
|
+
visibility=vis,
|
|
131
|
+
modifiers=("async",) if isinstance(child, ast.AsyncFunctionDef) else (),
|
|
132
|
+
content_hash=hashlib.sha256(body_str.encode()).hexdigest()[:16],
|
|
133
|
+
)
|
|
134
|
+
symbols.append(s)
|
|
135
|
+
self._walk_ast(child, file_path, sym_id, qname, symbols)
|
|
136
|
+
|
|
137
|
+
def extract_edges(self, parse: ParseResult, symbols: Sequence[Symbol]) -> Iterable[Edge]:
|
|
138
|
+
"""Extract relationship edges from parse AST and symbols.
|
|
139
|
+
Example:
|
|
140
|
+
>>> analyzer = PythonLanguageAnalyzer()
|
|
141
|
+
>>> res = analyzer.parse(Path("a.py"), "def f(): pass\\ndef g(): f()")
|
|
142
|
+
>>> syms = list(analyzer.extract_symbols(res))
|
|
143
|
+
>>> edges = list(analyzer.extract_edges(res, syms))
|
|
144
|
+
>>> any(e.source_id == "a.py::g" and e.target_id == "a.py::f" for e in edges)
|
|
145
|
+
True
|
|
146
|
+
"""
|
|
147
|
+
if parse.ast is None:
|
|
148
|
+
return []
|
|
149
|
+
edges: List[Edge] = []
|
|
150
|
+
sym_map = {s.id: s for s in symbols}
|
|
151
|
+
for s in symbols:
|
|
152
|
+
if s.parent_id and s.parent_id in sym_map:
|
|
153
|
+
edges.append(Edge(
|
|
154
|
+
source_id=s.parent_id,
|
|
155
|
+
target_id=s.id,
|
|
156
|
+
kind=EdgeKind.CONTAINS,
|
|
157
|
+
weight=1.0,
|
|
158
|
+
confidence=1.0,
|
|
159
|
+
location=s.location,
|
|
160
|
+
))
|
|
161
|
+
import_map: Dict[str, str] = {}
|
|
162
|
+
for node in ast.walk(parse.ast):
|
|
163
|
+
if isinstance(node, ast.Import):
|
|
164
|
+
for alias in node.names:
|
|
165
|
+
local_name = alias.asname or alias.name
|
|
166
|
+
import_map[local_name] = f"module::{alias.name}"
|
|
167
|
+
weight = float(self._count_symbol_usages(parse.source, local_name))
|
|
168
|
+
edges.append(Edge(
|
|
169
|
+
source_id=parse.file_path,
|
|
170
|
+
target_id=f"module::{alias.name}",
|
|
171
|
+
kind=EdgeKind.IMPORTS,
|
|
172
|
+
weight=weight,
|
|
173
|
+
confidence=1.0,
|
|
174
|
+
location=Location(parse.file_path, node.lineno, node.col_offset, node.lineno, node.col_offset),
|
|
175
|
+
))
|
|
176
|
+
elif isinstance(node, ast.ImportFrom):
|
|
177
|
+
level = getattr(node, "level", 0) or 0
|
|
178
|
+
dots = "." * level if level > 0 else ""
|
|
179
|
+
raw_mod = node.module or ""
|
|
180
|
+
mod_name = f"{dots}{raw_mod}" if dots else raw_mod
|
|
181
|
+
for alias in node.names:
|
|
182
|
+
local_name = alias.asname or alias.name
|
|
183
|
+
target = f"module::{mod_name}.{alias.name}" if mod_name else f"module::{alias.name}"
|
|
184
|
+
import_map[local_name] = target
|
|
185
|
+
weight = float(self._count_symbol_usages(parse.source, local_name))
|
|
186
|
+
edges.append(Edge(
|
|
187
|
+
source_id=parse.file_path,
|
|
188
|
+
target_id=target,
|
|
189
|
+
kind=EdgeKind.IMPORTS,
|
|
190
|
+
weight=weight,
|
|
191
|
+
confidence=1.0,
|
|
192
|
+
location=Location(parse.file_path, node.lineno, node.col_offset, node.lineno, node.col_offset),
|
|
193
|
+
))
|
|
194
|
+
class_names = {s.name for s in symbols if s.kind == SymbolKind.CLASS}
|
|
195
|
+
def walk_calls(node: ast.AST, current_scope: str, class_scope: str, locals_in_scope: set[str]) -> None:
|
|
196
|
+
for child in ast.iter_child_nodes(node):
|
|
197
|
+
if isinstance(child, ast.ClassDef):
|
|
198
|
+
class_sym_id = f"{parse.file_path}::{child.name}"
|
|
199
|
+
for base in child.bases:
|
|
200
|
+
if isinstance(base, ast.Name):
|
|
201
|
+
target_id = f"{parse.file_path}::{base.id}"
|
|
202
|
+
edges.append(Edge(
|
|
203
|
+
source_id=class_sym_id,
|
|
204
|
+
target_id=target_id,
|
|
205
|
+
kind=EdgeKind.INHERITS,
|
|
206
|
+
weight=1.0,
|
|
207
|
+
confidence=0.9,
|
|
208
|
+
location=Location(parse.file_path, base.lineno, base.col_offset, base.lineno, base.col_offset + len(base.id)),
|
|
209
|
+
))
|
|
210
|
+
walk_calls(child, class_sym_id, child.name, set())
|
|
211
|
+
elif isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
212
|
+
qname = f"{class_scope}.{child.name}" if class_scope else child.name
|
|
213
|
+
func_sym_id = f"{parse.file_path}::{qname}"
|
|
214
|
+
new_locals = set(locals_in_scope)
|
|
215
|
+
for arg in getattr(child.args, "posonlyargs", []) + getattr(child.args, "args", []) + getattr(child.args, "kwonlyargs", []):
|
|
216
|
+
if hasattr(arg, "arg"):
|
|
217
|
+
new_locals.add(arg.arg)
|
|
218
|
+
if getattr(child.args, "vararg", None):
|
|
219
|
+
new_locals.add(child.args.vararg.arg)
|
|
220
|
+
if getattr(child.args, "kwarg", None):
|
|
221
|
+
new_locals.add(child.args.kwarg.arg)
|
|
222
|
+
for subnode in ast.walk(child):
|
|
223
|
+
if isinstance(subnode, ast.Assign):
|
|
224
|
+
for target in subnode.targets:
|
|
225
|
+
if isinstance(target, ast.Name):
|
|
226
|
+
new_locals.add(target.id)
|
|
227
|
+
elif isinstance(subnode, ast.AnnAssign):
|
|
228
|
+
if isinstance(subnode.target, ast.Name):
|
|
229
|
+
new_locals.add(subnode.target.id)
|
|
230
|
+
walk_calls(child, func_sym_id, class_scope, new_locals)
|
|
231
|
+
elif isinstance(child, ast.Call):
|
|
232
|
+
caller_id = current_scope if current_scope else parse.file_path
|
|
233
|
+
if isinstance(child.func, ast.Name):
|
|
234
|
+
callee_name = child.func.id
|
|
235
|
+
if callee_name not in locals_in_scope:
|
|
236
|
+
target_id = import_map.get(callee_name, f"{parse.file_path}::{callee_name}")
|
|
237
|
+
weight = float(self._count_symbol_usages(parse.source, callee_name))
|
|
238
|
+
edges.append(Edge(
|
|
239
|
+
source_id=caller_id,
|
|
240
|
+
target_id=target_id,
|
|
241
|
+
kind=EdgeKind.CALLS,
|
|
242
|
+
weight=weight,
|
|
243
|
+
confidence=0.85,
|
|
244
|
+
location=Location(parse.file_path, child.lineno, child.col_offset, child.lineno, child.col_offset + len(callee_name)),
|
|
245
|
+
))
|
|
246
|
+
elif isinstance(child.func, ast.Attribute):
|
|
247
|
+
attr_name = child.func.attr
|
|
248
|
+
target_id = None
|
|
249
|
+
if isinstance(child.func.value, ast.Name):
|
|
250
|
+
val_name = child.func.value.id
|
|
251
|
+
if val_name in ("self", "cls") and class_scope:
|
|
252
|
+
target_id = f"{parse.file_path}::{class_scope}.{attr_name}"
|
|
253
|
+
elif val_name in import_map:
|
|
254
|
+
target_id = f"{import_map[val_name]}.{attr_name}"
|
|
255
|
+
elif val_name in class_names:
|
|
256
|
+
target_id = f"{parse.file_path}::{val_name}.{attr_name}"
|
|
257
|
+
if target_id:
|
|
258
|
+
weight = float(self._count_symbol_usages(parse.source, attr_name))
|
|
259
|
+
edges.append(Edge(
|
|
260
|
+
source_id=caller_id,
|
|
261
|
+
target_id=target_id,
|
|
262
|
+
kind=EdgeKind.CALLS,
|
|
263
|
+
weight=weight,
|
|
264
|
+
confidence=0.8,
|
|
265
|
+
location=Location(parse.file_path, child.lineno, child.col_offset, child.lineno, child.col_offset + len(attr_name)),
|
|
266
|
+
))
|
|
267
|
+
walk_calls(child, current_scope, class_scope, locals_in_scope)
|
|
268
|
+
else:
|
|
269
|
+
walk_calls(child, current_scope, class_scope, locals_in_scope)
|
|
270
|
+
walk_calls(parse.ast, "", "", set())
|
|
271
|
+
return edges
|
|
272
|
+
|
|
273
|
+
def _count_symbol_usages(self, source: str, symbol_name: str) -> int:
|
|
274
|
+
if not symbol_name or symbol_name == "*":
|
|
275
|
+
return 1
|
|
276
|
+
pattern = r"\b" + re.escape(symbol_name) + r"\b"
|
|
277
|
+
count = 0
|
|
278
|
+
for line in source.splitlines():
|
|
279
|
+
stripped = line.strip()
|
|
280
|
+
if stripped.startswith(("import ", "from ", "#")):
|
|
281
|
+
continue
|
|
282
|
+
count += len(re.findall(pattern, line))
|
|
283
|
+
return max(1, count)
|
|
284
|
+
|
|
285
|
+
def resolve_import(self, ref: ImportRef, ctx: ResolveContext) -> Iterable[str]:
|
|
286
|
+
"""Resolve Python import module path to candidate files.
|
|
287
|
+
Example:
|
|
288
|
+
>>> analyzer = PythonLanguageAnalyzer()
|
|
289
|
+
>>> ctx = ResolveContext(Path("."))
|
|
290
|
+
>>> ref = ImportRef("app.py", "utils", None, False)
|
|
291
|
+
>>> isinstance(list(analyzer.resolve_import(ref, ctx)), list)
|
|
292
|
+
True
|
|
293
|
+
"""
|
|
294
|
+
return ctx.resolve_path(ref.file_path, ref.module_name, "python")
|
codeui/lang/registry.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Registry mapping file paths to appropriate language analyzers."""
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Dict, List, Optional, Type
|
|
4
|
+
from codeui.lang.base import LanguageAnalyzer
|
|
5
|
+
from codeui.lang.python import PythonLanguageAnalyzer
|
|
6
|
+
from codeui.lang.ts import TSLanguageAnalyzer
|
|
7
|
+
from codeui.lang.go import GoLanguageAnalyzer
|
|
8
|
+
from codeui.lang.rust import RustLanguageAnalyzer
|
|
9
|
+
from codeui.lang.generic import GenericLanguageAnalyzer
|
|
10
|
+
|
|
11
|
+
class LanguageRegistry:
|
|
12
|
+
"""Registry maintaining active language adapters.
|
|
13
|
+
Example:
|
|
14
|
+
>>> reg = LanguageRegistry()
|
|
15
|
+
>>> analyzer = reg.get_analyzer(Path("app.py"))
|
|
16
|
+
>>> analyzer.language
|
|
17
|
+
'python'
|
|
18
|
+
"""
|
|
19
|
+
def __init__(self) -> None:
|
|
20
|
+
self._analyzers: List[LanguageAnalyzer] = [
|
|
21
|
+
PythonLanguageAnalyzer(),
|
|
22
|
+
TSLanguageAnalyzer(),
|
|
23
|
+
GoLanguageAnalyzer(),
|
|
24
|
+
RustLanguageAnalyzer(),
|
|
25
|
+
GenericLanguageAnalyzer(),
|
|
26
|
+
]
|
|
27
|
+
self._ext_map: Dict[str, LanguageAnalyzer] = {}
|
|
28
|
+
for analyzer in self._analyzers:
|
|
29
|
+
for ext in analyzer.extensions:
|
|
30
|
+
self._ext_map[ext.lower()] = analyzer
|
|
31
|
+
|
|
32
|
+
def register(self, analyzer: LanguageAnalyzer) -> None:
|
|
33
|
+
"""Register a custom language analyzer.
|
|
34
|
+
Example:
|
|
35
|
+
>>> reg = LanguageRegistry()
|
|
36
|
+
>>> reg.register(GenericLanguageAnalyzer())
|
|
37
|
+
"""
|
|
38
|
+
self._analyzers.append(analyzer)
|
|
39
|
+
for ext in analyzer.extensions:
|
|
40
|
+
self._ext_map[ext.lower()] = analyzer
|
|
41
|
+
|
|
42
|
+
def get_analyzer(self, path: Path) -> LanguageAnalyzer:
|
|
43
|
+
"""Find language analyzer matching file extension or filename.
|
|
44
|
+
Example:
|
|
45
|
+
>>> reg = LanguageRegistry()
|
|
46
|
+
>>> reg.get_analyzer(Path("main.ts")).language
|
|
47
|
+
'typescript'
|
|
48
|
+
"""
|
|
49
|
+
ext = path.suffix.lower()
|
|
50
|
+
if not ext and path.name:
|
|
51
|
+
ext = path.name.lower()
|
|
52
|
+
return self._ext_map.get(ext, self._analyzers[-1])
|
|
53
|
+
|
|
54
|
+
def supported_languages(self) -> List[str]:
|
|
55
|
+
"""List registered supported languages.
|
|
56
|
+
Example:
|
|
57
|
+
>>> reg = LanguageRegistry()
|
|
58
|
+
>>> 'python' in reg.supported_languages()
|
|
59
|
+
True
|
|
60
|
+
"""
|
|
61
|
+
return sorted(list(set(a.language for a in self._analyzers)))
|
|
62
|
+
|
|
63
|
+
def get_supported_extensions(self) -> set[str]:
|
|
64
|
+
"""Return file extensions supported by first-class AST language analyzers.
|
|
65
|
+
Example:
|
|
66
|
+
>>> reg = LanguageRegistry()
|
|
67
|
+
>>> '.py' in reg.get_supported_extensions()
|
|
68
|
+
True
|
|
69
|
+
"""
|
|
70
|
+
exts: set[str] = set()
|
|
71
|
+
for a in self._analyzers:
|
|
72
|
+
if getattr(a, "has_ast_support", True) and getattr(a, "is_supported", True) and a.language != "generic":
|
|
73
|
+
exts.update(a.extensions)
|
|
74
|
+
return exts
|
|
75
|
+
|
|
76
|
+
def get_all_extensions(self) -> set[str]:
|
|
77
|
+
"""Return all registered file extensions including fallback analyzers.
|
|
78
|
+
Example:
|
|
79
|
+
>>> reg = LanguageRegistry()
|
|
80
|
+
>>> '.py' in reg.get_all_extensions()
|
|
81
|
+
True
|
|
82
|
+
"""
|
|
83
|
+
return set(self._ext_map.keys())
|