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/__init__.py
CHANGED
codeui/agent/context.py
CHANGED
|
@@ -110,7 +110,9 @@ class ContextBuilder:
|
|
|
110
110
|
|
|
111
111
|
relevant_findings = [
|
|
112
112
|
f.to_dict() for f in self.graph.get_findings()
|
|
113
|
-
if f.symbol_id in {s.id for s in syms} or
|
|
113
|
+
if (f.symbol_id and f.symbol_id in {s.id for s in syms}) or (
|
|
114
|
+
f.location and target_sym.location and f.location.file_id == target_sym.location.file_id
|
|
115
|
+
)
|
|
114
116
|
]
|
|
115
117
|
|
|
116
118
|
manifest = {
|
|
@@ -147,7 +149,7 @@ class ContextBuilder:
|
|
|
147
149
|
bodies[s.id] = f"{cm} {file_id}:{s.location.start_line if s.location else 1}\n{s.signature or s.name}"
|
|
148
150
|
relevant_findings = [
|
|
149
151
|
f.to_dict() for f in self.graph.get_findings()
|
|
150
|
-
if f.location.file_id == file_id
|
|
152
|
+
if f.location and f.location.file_id == file_id
|
|
151
153
|
]
|
|
152
154
|
return ContextBundle(
|
|
153
155
|
symbol_bodies=bodies,
|
|
@@ -189,6 +191,18 @@ class ContextBuilder:
|
|
|
189
191
|
manifest={"query": task.query, "symbols": task.target_symbols, "files": task.target_files},
|
|
190
192
|
)
|
|
191
193
|
|
|
194
|
+
def build_context(self, task: TaskSpec, depth: int = 1) -> ContextBundle:
|
|
195
|
+
"""Alias for for_task to build context bundle.
|
|
196
|
+
Example:
|
|
197
|
+
>>> g = Graph()
|
|
198
|
+
>>> builder = ContextBuilder(g)
|
|
199
|
+
>>> spec = TaskSpec("Fix issue")
|
|
200
|
+
>>> bundle = builder.build_context(spec)
|
|
201
|
+
>>> "query" in bundle.manifest
|
|
202
|
+
True
|
|
203
|
+
"""
|
|
204
|
+
return self.for_task(task, depth=depth)
|
|
205
|
+
|
|
192
206
|
def for_defect(self, finding_id: str) -> ContextBundle:
|
|
193
207
|
"""Build context bundle targeting a specific defect finding.
|
|
194
208
|
Example:
|
|
@@ -204,4 +218,6 @@ class ContextBuilder:
|
|
|
204
218
|
f = matching[0]
|
|
205
219
|
if f.symbol_id:
|
|
206
220
|
return self.for_symbol(f.symbol_id, depth=1)
|
|
207
|
-
|
|
221
|
+
if f.location and f.location.file_id:
|
|
222
|
+
return self.for_file(f.location.file_id, depth=1)
|
|
223
|
+
return ContextBundle({}, {}, [], [], {"finding_id": finding_id, "status": "no_location"})
|
codeui/analysis/dead_code.py
CHANGED
|
@@ -3,7 +3,7 @@ from pathlib import Path
|
|
|
3
3
|
from typing import ClassVar, Iterable, List, Set
|
|
4
4
|
from codeui.analysis.base import AnalysisContext, Analyzer
|
|
5
5
|
from codeui.core.graph import Graph
|
|
6
|
-
from codeui.core.ir import EdgeKind, Finding, Location, Severity, SymbolKind, Visibility
|
|
6
|
+
from codeui.core.ir import EdgeKind, Finding, LayerKind, Location, Severity, SymbolKind, Visibility
|
|
7
7
|
|
|
8
8
|
class DeadCodeAnalyzer(Analyzer):
|
|
9
9
|
"""Detects unused functions, unused private symbols, and orphan files.
|
|
@@ -22,7 +22,10 @@ class DeadCodeAnalyzer(Analyzer):
|
|
|
22
22
|
"__init__", "__post_init__", "__str__", "__repr__", "__eq__", "__hash__",
|
|
23
23
|
"__iter__", "__getitem__", "__setitem__", "__delitem__", "__len__",
|
|
24
24
|
"__contains__", "__enter__", "__exit__", "__call__", "__await__",
|
|
25
|
-
"constructor", "init", "dispose", "render", "
|
|
25
|
+
"constructor", "init", "dispose", "render", "build", "initialize",
|
|
26
|
+
"setup", "teardown", "setUp", "tearDown", "beforeEach", "afterEach",
|
|
27
|
+
"beforeAll", "afterAll", "onCreate", "onDestroy", "onStart", "onStop",
|
|
28
|
+
"onResume", "onPause", "componentDidMount", "componentWillUnmount"
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
def run(self, graph: Graph, ctx: AnalysisContext) -> Iterable[Finding]:
|
|
@@ -43,6 +46,14 @@ class DeadCodeAnalyzer(Analyzer):
|
|
|
43
46
|
referenced_targets.add(raw_target.split(".")[-1])
|
|
44
47
|
|
|
45
48
|
for sym in graph.get_all_symbols():
|
|
49
|
+
if sym.location and sym.location.file_id:
|
|
50
|
+
cls = graph.get_file_classification(sym.location.file_id)
|
|
51
|
+
if cls.layer in (LayerKind.TEST, LayerKind.CONFIG):
|
|
52
|
+
continue
|
|
53
|
+
file_lower = sym.location.file_id.replace("\\", "/").lower()
|
|
54
|
+
if any(p in file_lower for p in ("/tests/", "/test/", "/benches/", "/bench/", "/examples/", "/example/", "/fixtures/")):
|
|
55
|
+
continue
|
|
56
|
+
|
|
46
57
|
if sym.kind in (SymbolKind.FUNCTION, SymbolKind.METHOD, SymbolKind.CLASS):
|
|
47
58
|
if sym.name in self.ENTRY_POINTS or any(sym.name.startswith(p) for p in self.ENTRY_POINTS):
|
|
48
59
|
continue
|
codeui/analysis/undefined.py
CHANGED
|
@@ -76,10 +76,80 @@ class UndefinedSymbolAnalyzer(Analyzer):
|
|
|
76
76
|
if "." in target_name:
|
|
77
77
|
continue
|
|
78
78
|
|
|
79
|
+
local_names = set()
|
|
80
|
+
try:
|
|
81
|
+
caller_sym = graph.get_symbol(edge.source_id)
|
|
82
|
+
if caller_sym and caller_sym.kind in (SymbolKind.FUNCTION, SymbolKind.METHOD):
|
|
83
|
+
file_path = ctx.root_dir / caller_sym.location.file_id
|
|
84
|
+
if file_path.exists():
|
|
85
|
+
import ast
|
|
86
|
+
source = file_path.read_text(encoding="utf-8")
|
|
87
|
+
tree = ast.parse(source)
|
|
88
|
+
for node in ast.walk(tree):
|
|
89
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.lineno == caller_sym.location.start_line:
|
|
90
|
+
for arg in getattr(node.args, "posonlyargs", []) + getattr(node.args, "args", []) + getattr(node.args, "kwonlyargs", []):
|
|
91
|
+
if hasattr(arg, "arg"):
|
|
92
|
+
local_names.add(arg.arg)
|
|
93
|
+
if getattr(node.args, "vararg", None):
|
|
94
|
+
local_names.add(node.args.vararg.arg)
|
|
95
|
+
if getattr(node.args, "kwarg", None):
|
|
96
|
+
local_names.add(node.args.kwarg.arg)
|
|
97
|
+
|
|
98
|
+
for subnode in ast.walk(node):
|
|
99
|
+
if isinstance(subnode, ast.Assign):
|
|
100
|
+
for target in subnode.targets:
|
|
101
|
+
if isinstance(target, ast.Name):
|
|
102
|
+
local_names.add(target.id)
|
|
103
|
+
elif isinstance(target, (ast.Tuple, ast.List)):
|
|
104
|
+
def _ext_names(t):
|
|
105
|
+
res = set()
|
|
106
|
+
if isinstance(t, ast.Name): res.add(t.id)
|
|
107
|
+
elif isinstance(t, (ast.Tuple, ast.List)):
|
|
108
|
+
for e in t.elts: res.update(_ext_names(e))
|
|
109
|
+
return res
|
|
110
|
+
local_names.update(_ext_names(target))
|
|
111
|
+
elif isinstance(subnode, ast.AnnAssign):
|
|
112
|
+
if isinstance(subnode.target, ast.Name):
|
|
113
|
+
local_names.add(subnode.target.id)
|
|
114
|
+
elif isinstance(subnode, (ast.For, ast.AsyncFor)):
|
|
115
|
+
def _ext_names(t):
|
|
116
|
+
res = set()
|
|
117
|
+
if isinstance(t, ast.Name): res.add(t.id)
|
|
118
|
+
elif isinstance(t, (ast.Tuple, ast.List)):
|
|
119
|
+
for e in t.elts: res.update(_ext_names(e))
|
|
120
|
+
return res
|
|
121
|
+
local_names.update(_ext_names(subnode.target))
|
|
122
|
+
elif isinstance(subnode, (ast.With, ast.AsyncWith)):
|
|
123
|
+
for item in subnode.items:
|
|
124
|
+
if item.optional_vars:
|
|
125
|
+
def _ext_names(t):
|
|
126
|
+
res = set()
|
|
127
|
+
if isinstance(t, ast.Name): res.add(t.id)
|
|
128
|
+
elif isinstance(t, (ast.Tuple, ast.List)):
|
|
129
|
+
for e in t.elts: res.update(_ext_names(e))
|
|
130
|
+
return res
|
|
131
|
+
local_names.update(_ext_names(item.optional_vars))
|
|
132
|
+
elif isinstance(subnode, ast.ExceptHandler):
|
|
133
|
+
if subnode.name:
|
|
134
|
+
local_names.add(subnode.name)
|
|
135
|
+
elif isinstance(subnode, (ast.ListComp, ast.SetComp, ast.DictComp, ast.GeneratorExp)):
|
|
136
|
+
for gen in subnode.generators:
|
|
137
|
+
def _ext_names(t):
|
|
138
|
+
res = set()
|
|
139
|
+
if isinstance(t, ast.Name): res.add(t.id)
|
|
140
|
+
elif isinstance(t, (ast.Tuple, ast.List)):
|
|
141
|
+
for e in t.elts: res.update(_ext_names(e))
|
|
142
|
+
return res
|
|
143
|
+
local_names.update(_ext_names(gen.target))
|
|
144
|
+
break
|
|
145
|
+
except Exception:
|
|
146
|
+
pass
|
|
147
|
+
|
|
79
148
|
if (
|
|
80
149
|
not graph.has_symbol(target_id)
|
|
81
150
|
and target_name not in all_symbol_names
|
|
82
151
|
and target_name not in file_imports.get(src_file, set())
|
|
152
|
+
and target_name not in local_names
|
|
83
153
|
and not target_name.startswith("_")
|
|
84
154
|
):
|
|
85
155
|
loc = edge.location or Location(edge.source_id, 1, 0, 1, 0)
|
|
@@ -58,8 +58,20 @@ class UnresolvedImportAnalyzer(Analyzer):
|
|
|
58
58
|
known_deps.add(stem)
|
|
59
59
|
known_deps.add(p)
|
|
60
60
|
|
|
61
|
+
top_level_items = set()
|
|
61
62
|
if ctx and ctx.root_dir:
|
|
62
63
|
root = ctx.root_dir
|
|
64
|
+
try:
|
|
65
|
+
for item in root.iterdir():
|
|
66
|
+
top_level_items.add(item.name)
|
|
67
|
+
top_level_items.add(item.stem)
|
|
68
|
+
if item.name == "src" and item.is_dir():
|
|
69
|
+
for subitem in item.iterdir():
|
|
70
|
+
top_level_items.add(subitem.name)
|
|
71
|
+
top_level_items.add(subitem.stem)
|
|
72
|
+
except Exception:
|
|
73
|
+
pass
|
|
74
|
+
|
|
63
75
|
for manifest_name in ("package.json", "requirements.txt", "pyproject.toml", "setup.py", "Pipfile", "go.mod", "Cargo.toml"):
|
|
64
76
|
mpath = root / manifest_name
|
|
65
77
|
if mpath.exists():
|
|
@@ -104,17 +116,35 @@ class UnresolvedImportAnalyzer(Analyzer):
|
|
|
104
116
|
or any(raw_mod in f or base_mod in f for f in all_files)
|
|
105
117
|
)
|
|
106
118
|
|
|
119
|
+
is_internal = False
|
|
120
|
+
if base_mod in top_level_items:
|
|
121
|
+
is_internal = True
|
|
122
|
+
else:
|
|
123
|
+
for f in all_files:
|
|
124
|
+
f_clean = f.replace("\\", "/")
|
|
125
|
+
if f_clean.startswith(base_mod + "/") or f_clean == base_mod or ("/" + base_mod + "/") in f_clean:
|
|
126
|
+
is_internal = True
|
|
127
|
+
break
|
|
128
|
+
|
|
129
|
+
if not is_internal and base_mod != "@" and base_mod != "":
|
|
130
|
+
is_known = True
|
|
131
|
+
|
|
107
132
|
if not is_known and raw_mod.startswith("@/"):
|
|
108
133
|
as_src = "src/" + raw_mod[2:]
|
|
109
134
|
as_root = raw_mod[2:]
|
|
110
135
|
if any(as_src in f or as_root in f for f in all_files):
|
|
111
136
|
is_known = True
|
|
112
137
|
|
|
113
|
-
if not is_known
|
|
138
|
+
if not is_known:
|
|
114
139
|
src_file = edge.source_id.split("::")[0]
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
140
|
+
if hasattr(ctx, "resolve_path"):
|
|
141
|
+
resolved = ctx.resolve_path(src_file, raw_mod)
|
|
142
|
+
if resolved:
|
|
143
|
+
is_known = True
|
|
144
|
+
elif hasattr(ctx, "resolve_context") and ctx.resolve_context and hasattr(ctx.resolve_context, "resolve_path"):
|
|
145
|
+
resolved = ctx.resolve_context.resolve_path(src_file, raw_mod)
|
|
146
|
+
if resolved:
|
|
147
|
+
is_known = True
|
|
118
148
|
|
|
119
149
|
if not is_known and hasattr(ctx, "alias_mappings"):
|
|
120
150
|
for alias, target in getattr(ctx, "alias_mappings", {}).items():
|
codeui/cli/main.py
CHANGED
|
@@ -27,6 +27,7 @@ def scan_project(
|
|
|
27
27
|
project_path: Union[str, Path],
|
|
28
28
|
entry_points: Optional[Sequence[str]] = None,
|
|
29
29
|
layer_rules: Optional[Dict[str, Any]] = None,
|
|
30
|
+
clean_cache: bool = False,
|
|
30
31
|
) -> Graph:
|
|
31
32
|
"""Scan a project directory or remote Git repository and build the universal graph.
|
|
32
33
|
Example:
|
|
@@ -46,9 +47,12 @@ def scan_project(
|
|
|
46
47
|
graph = Graph()
|
|
47
48
|
registry = LanguageRegistry()
|
|
48
49
|
ctx = ResolveContext(resolved_project_path)
|
|
50
|
+
ctx.set_supported_extensions(registry.get_all_extensions())
|
|
49
51
|
cache_db_path = resolved_project_path / ".codeui" / "cache.db"
|
|
50
52
|
try:
|
|
51
53
|
cache = AnalysisCache(cache_db_path)
|
|
54
|
+
if clean_cache:
|
|
55
|
+
cache.clear()
|
|
52
56
|
except Exception:
|
|
53
57
|
cache = None
|
|
54
58
|
|
|
@@ -96,6 +100,13 @@ def scan_project(
|
|
|
96
100
|
continue
|
|
97
101
|
all_files.append(p)
|
|
98
102
|
|
|
103
|
+
if cache:
|
|
104
|
+
valid_rel_paths = {
|
|
105
|
+
str(p.relative_to(resolved_project_path)).replace("\\", "/")
|
|
106
|
+
for p in all_files
|
|
107
|
+
}
|
|
108
|
+
cache.prune_stale(valid_rel_paths)
|
|
109
|
+
|
|
99
110
|
parsed_files: Set[str] = set()
|
|
100
111
|
raw_edges: List[Edge] = []
|
|
101
112
|
for p in all_files:
|
|
@@ -196,6 +207,56 @@ def scan_project(
|
|
|
196
207
|
if temp_clone_obj:
|
|
197
208
|
temp_clone_obj.cleanup()
|
|
198
209
|
|
|
210
|
+
def export_pages(
|
|
211
|
+
project_path: Union[str, Path] = ".",
|
|
212
|
+
out_dir: Union[str, Path] = "dist",
|
|
213
|
+
entry_points: Optional[Sequence[str]] = None,
|
|
214
|
+
clean_cache: bool = False,
|
|
215
|
+
) -> Path:
|
|
216
|
+
"""Export static code graph and report artifacts for standalone browser preview.
|
|
217
|
+
Example:
|
|
218
|
+
>>> import tempfile
|
|
219
|
+
>>> with tempfile.TemporaryDirectory() as td:
|
|
220
|
+
... out = export_pages(Path("."), Path(td) / "dist")
|
|
221
|
+
... out.exists()
|
|
222
|
+
True
|
|
223
|
+
"""
|
|
224
|
+
resolved_proj = Path(project_path).resolve()
|
|
225
|
+
resolved_out = Path(out_dir).resolve()
|
|
226
|
+
resolved_out.mkdir(parents=True, exist_ok=True)
|
|
227
|
+
|
|
228
|
+
g = scan_project(resolved_proj, entry_points=entry_points, clean_cache=clean_cache)
|
|
229
|
+
|
|
230
|
+
(resolved_out / "graph.json").write_text(JSONEmitter().emit(g), encoding="utf-8")
|
|
231
|
+
|
|
232
|
+
ctx = AnalysisContext(resolved_proj)
|
|
233
|
+
findings = [f.to_dict() for f in AnalysisRunner().run_all(g, ctx)]
|
|
234
|
+
(resolved_out / "defects.json").write_text(
|
|
235
|
+
json.dumps({"findings": findings, "count": len(findings)}, indent=2),
|
|
236
|
+
encoding="utf-8",
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
static_src = Path(__file__).resolve().parent.parent / "server" / "static"
|
|
240
|
+
if static_src.exists():
|
|
241
|
+
static_out = resolved_out / "static"
|
|
242
|
+
static_out.mkdir(parents=True, exist_ok=True)
|
|
243
|
+
for item in static_src.rglob("*"):
|
|
244
|
+
if item.is_file() and item.name != "index.html":
|
|
245
|
+
rel = item.relative_to(static_src)
|
|
246
|
+
dest = static_out / rel
|
|
247
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
248
|
+
dest.write_bytes(item.read_bytes())
|
|
249
|
+
|
|
250
|
+
index_src = static_src / "index.html"
|
|
251
|
+
if index_src.exists():
|
|
252
|
+
html_text = index_src.read_text(encoding="utf-8")
|
|
253
|
+
html_text = html_text.replace('href="/static/', 'href="static/')
|
|
254
|
+
html_text = html_text.replace('src="/static/', 'src="static/')
|
|
255
|
+
(resolved_out / "index.html").write_text(html_text, encoding="utf-8")
|
|
256
|
+
|
|
257
|
+
(resolved_out / ".nojekyll").touch(exist_ok=True)
|
|
258
|
+
return resolved_out
|
|
259
|
+
|
|
199
260
|
def main(args: Optional[List[str]] = None) -> int:
|
|
200
261
|
"""Main CLI entrypoint.
|
|
201
262
|
Example:
|
|
@@ -209,16 +270,25 @@ def main(args: Optional[List[str]] = None) -> int:
|
|
|
209
270
|
parser = argparse.ArgumentParser(prog="codeui", description="Universal code-intelligence engine", parents=[parent_parser])
|
|
210
271
|
subparsers = parser.add_subparsers(dest="command")
|
|
211
272
|
|
|
212
|
-
subparsers.add_parser("init", parents=[parent_parser], help="Initialize .codeui
|
|
273
|
+
init_p = subparsers.add_parser("init", parents=[parent_parser], help="Initialize .codeui workspace directory")
|
|
274
|
+
init_p.add_argument("project", nargs="?", default=".", help="Project path")
|
|
213
275
|
|
|
214
276
|
scan_p = subparsers.add_parser("scan", parents=[parent_parser], help="Scan project and build code graph")
|
|
215
277
|
scan_p.add_argument("project", nargs="?", default=".", help="Project path")
|
|
216
278
|
scan_p.add_argument("--entry", action="append", help="Explicit application entry point(s)")
|
|
217
279
|
scan_p.add_argument("--out", default=".codeui", help="Output cache directory")
|
|
280
|
+
scan_p.add_argument("--clean-cache", action="store_true", help="Clear stale cache before scanning")
|
|
281
|
+
|
|
282
|
+
pages_p = subparsers.add_parser("pages", parents=[parent_parser], help="Export static interactive graph and report artifacts")
|
|
283
|
+
pages_p.add_argument("project", nargs="?", default=".", help="Project path")
|
|
284
|
+
pages_p.add_argument("--out", default="dist", help="Output directory for static artifacts (default: dist)")
|
|
285
|
+
pages_p.add_argument("--entry", action="append", help="Explicit application entry point(s)")
|
|
286
|
+
pages_p.add_argument("--clean-cache", action="store_true", help="Clear stale cache before exporting")
|
|
218
287
|
|
|
219
288
|
serve_p = subparsers.add_parser("serve", parents=[parent_parser], help="Start localhost web UI server")
|
|
220
289
|
serve_p.add_argument("project", nargs="?", default=".", help="Project path")
|
|
221
290
|
serve_p.add_argument("--entry", action="append", help="Explicit application entry point(s)")
|
|
291
|
+
serve_p.add_argument("--host", default="127.0.0.1", help="Host interface to bind to (e.g. 127.0.0.1 or 0.0.0.0)")
|
|
222
292
|
serve_p.add_argument("--port", type=int, default=3000, help="Port number")
|
|
223
293
|
|
|
224
294
|
trace_p = subparsers.add_parser("trace", parents=[parent_parser], help="Trace symbol logic")
|
|
@@ -266,15 +336,16 @@ def main(args: Optional[List[str]] = None) -> int:
|
|
|
266
336
|
parsed = parser.parse_args(args)
|
|
267
337
|
|
|
268
338
|
if parsed.command == "init":
|
|
269
|
-
Path(
|
|
339
|
+
p = Path(parsed.project).resolve()
|
|
340
|
+
(p / ".codeui").mkdir(parents=True, exist_ok=True)
|
|
270
341
|
if parsed.json:
|
|
271
|
-
print(json.dumps({"status": "initialized", "dir": ".codeui"}))
|
|
342
|
+
print(json.dumps({"status": "initialized", "dir": str(p / ".codeui")}))
|
|
272
343
|
else:
|
|
273
|
-
print("Initialized .codeui workspace directory")
|
|
344
|
+
print(f"Initialized .codeui workspace directory in {p}")
|
|
274
345
|
return 0
|
|
275
346
|
elif parsed.command == "scan":
|
|
276
347
|
p = Path(parsed.project).resolve()
|
|
277
|
-
g = scan_project(p, entry_points=parsed.entry)
|
|
348
|
+
g = scan_project(p, entry_points=parsed.entry, clean_cache=parsed.clean_cache)
|
|
278
349
|
out_dir = Path(parsed.out)
|
|
279
350
|
out_dir.mkdir(parents=True, exist_ok=True)
|
|
280
351
|
(out_dir / "graph.json").write_text(JSONEmitter().emit(g), encoding="utf-8")
|
|
@@ -283,12 +354,35 @@ def main(args: Optional[List[str]] = None) -> int:
|
|
|
283
354
|
else:
|
|
284
355
|
print(f"Scanned {len(g.get_all_files())} files, {len(g.get_all_symbols())} symbols, {len(g.get_findings())} defects.")
|
|
285
356
|
return 0
|
|
357
|
+
elif parsed.command == "pages":
|
|
358
|
+
out_path = export_pages(
|
|
359
|
+
project_path=parsed.project,
|
|
360
|
+
out_dir=parsed.out,
|
|
361
|
+
entry_points=parsed.entry,
|
|
362
|
+
clean_cache=parsed.clean_cache,
|
|
363
|
+
)
|
|
364
|
+
if parsed.json:
|
|
365
|
+
print(json.dumps({"status": "exported", "out_dir": str(out_path)}))
|
|
366
|
+
else:
|
|
367
|
+
print(f"Exported static graph and report artifacts to '{out_path}'.")
|
|
368
|
+
return 0
|
|
286
369
|
elif parsed.command == "serve":
|
|
287
370
|
p = Path(parsed.project).resolve()
|
|
288
371
|
g = scan_project(p, entry_points=parsed.entry)
|
|
289
372
|
store = OverrideStore()
|
|
290
|
-
|
|
291
|
-
|
|
373
|
+
try:
|
|
374
|
+
server = create_server(g, store, p, host=parsed.host, port=parsed.port, rescan_fn=scan_project, entry_points=parsed.entry)
|
|
375
|
+
except OSError as e:
|
|
376
|
+
if "already in use" in str(e).lower() or getattr(e, "errno", 0) == 98:
|
|
377
|
+
try:
|
|
378
|
+
import subprocess
|
|
379
|
+
subprocess.run(["fuser", "-k", f"{parsed.port}/tcp"], capture_output=True)
|
|
380
|
+
except Exception:
|
|
381
|
+
pass
|
|
382
|
+
server = create_server(g, store, p, host=parsed.host, port=parsed.port, rescan_fn=scan_project, entry_points=parsed.entry)
|
|
383
|
+
else:
|
|
384
|
+
raise
|
|
385
|
+
print(f"codeui UI server running on http://{parsed.host}:{parsed.port}")
|
|
292
386
|
try:
|
|
293
387
|
server.serve_forever()
|
|
294
388
|
except KeyboardInterrupt:
|
codeui/core/cache.py
CHANGED
|
@@ -115,6 +115,37 @@ class AnalysisCache:
|
|
|
115
115
|
result.append(sym)
|
|
116
116
|
return result
|
|
117
117
|
|
|
118
|
+
def clear(self) -> None:
|
|
119
|
+
"""Clear all entries from the cache database.
|
|
120
|
+
Example:
|
|
121
|
+
>>> cache = AnalysisCache(Path(".codeui/test_cache.db"))
|
|
122
|
+
>>> cache.clear()
|
|
123
|
+
>>> cache.close()
|
|
124
|
+
"""
|
|
125
|
+
with self.conn:
|
|
126
|
+
self.conn.execute("DELETE FROM file_cache")
|
|
127
|
+
|
|
128
|
+
def prune_stale(self, existing_file_paths: Any) -> int:
|
|
129
|
+
"""Remove cached entries for files that no longer exist on disk.
|
|
130
|
+
Example:
|
|
131
|
+
>>> cache = AnalysisCache(Path(".codeui/test_cache.db"))
|
|
132
|
+
>>> cache.prune_stale(["a.py"])
|
|
133
|
+
0
|
|
134
|
+
>>> cache.close()
|
|
135
|
+
"""
|
|
136
|
+
valid_set = set(existing_file_paths)
|
|
137
|
+
cursor = self.conn.cursor()
|
|
138
|
+
cursor.execute("SELECT file_path FROM file_cache")
|
|
139
|
+
cached_paths = [row[0] for row in cursor.fetchall()]
|
|
140
|
+
stale_paths = [p for p in cached_paths if p not in valid_set]
|
|
141
|
+
if stale_paths:
|
|
142
|
+
with self.conn:
|
|
143
|
+
self.conn.executemany(
|
|
144
|
+
"DELETE FROM file_cache WHERE file_path = ?",
|
|
145
|
+
[(p,) for p in stale_paths],
|
|
146
|
+
)
|
|
147
|
+
return len(stale_paths)
|
|
148
|
+
|
|
118
149
|
def close(self) -> None:
|
|
119
150
|
"""Close SQLite database connection.
|
|
120
151
|
Example:
|
|
@@ -133,3 +164,26 @@ def compute_content_hash(content: str) -> str:
|
|
|
133
164
|
64
|
|
134
165
|
"""
|
|
135
166
|
return hashlib.sha256(content.encode("utf-8")).hexdigest()
|
|
167
|
+
|
|
168
|
+
def remove_stale_cache(cache_path: Path) -> bool:
|
|
169
|
+
"""Dynamically remove stale cache file or directory.
|
|
170
|
+
Example:
|
|
171
|
+
>>> p = Path(".codeui/stale_test.db")
|
|
172
|
+
>>> remove_stale_cache(p)
|
|
173
|
+
False
|
|
174
|
+
"""
|
|
175
|
+
path = Path(cache_path)
|
|
176
|
+
if path.is_file():
|
|
177
|
+
try:
|
|
178
|
+
path.unlink(missing_ok=True)
|
|
179
|
+
return True
|
|
180
|
+
except OSError:
|
|
181
|
+
return False
|
|
182
|
+
if path.is_dir():
|
|
183
|
+
import shutil
|
|
184
|
+
try:
|
|
185
|
+
shutil.rmtree(path, ignore_errors=True)
|
|
186
|
+
return True
|
|
187
|
+
except OSError:
|
|
188
|
+
return False
|
|
189
|
+
return False
|