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
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Public API drift analyzer comparing current symbols against baseline snapshot."""
|
|
2
|
+
import json
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, ClassVar, Dict, Iterable, List
|
|
5
|
+
from codeui.analysis.base import AnalysisContext, Analyzer
|
|
6
|
+
from codeui.core.graph import Graph
|
|
7
|
+
from codeui.core.ir import Finding, Location, Severity, Visibility
|
|
8
|
+
|
|
9
|
+
class ApiDriftAnalyzer(Analyzer):
|
|
10
|
+
"""Detects removed or signature-modified public symbols against prior snapshot.
|
|
11
|
+
Example:
|
|
12
|
+
>>> analyzer = ApiDriftAnalyzer()
|
|
13
|
+
>>> g = Graph()
|
|
14
|
+
>>> ctx = AnalysisContext(Path("."))
|
|
15
|
+
>>> list(analyzer.run(g, ctx))
|
|
16
|
+
[]
|
|
17
|
+
"""
|
|
18
|
+
id: ClassVar[str] = "api_drift"
|
|
19
|
+
severity_default: ClassVar[Severity] = Severity.WARNING
|
|
20
|
+
|
|
21
|
+
def run(self, graph: Graph, ctx: AnalysisContext) -> Iterable[Finding]:
|
|
22
|
+
"""Detect signature breaks in exported public API interface.
|
|
23
|
+
Example:
|
|
24
|
+
>>> analyzer = ApiDriftAnalyzer()
|
|
25
|
+
>>> g = Graph()
|
|
26
|
+
>>> list(analyzer.run(g, AnalysisContext(Path("."))))
|
|
27
|
+
[]
|
|
28
|
+
"""
|
|
29
|
+
if not ctx or not ctx.root_dir:
|
|
30
|
+
return []
|
|
31
|
+
baseline_file = ctx.root_dir / ".codeui" / "api_baseline.json"
|
|
32
|
+
if not baseline_file.exists():
|
|
33
|
+
baseline_file = ctx.root_dir / "api_baseline.json"
|
|
34
|
+
if not baseline_file.exists():
|
|
35
|
+
return []
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
baseline_data: Dict[str, Any] = json.loads(baseline_file.read_text(encoding="utf-8"))
|
|
39
|
+
except Exception:
|
|
40
|
+
return []
|
|
41
|
+
|
|
42
|
+
findings: List[Finding] = []
|
|
43
|
+
current_symbols = {s.id: s for s in graph.get_all_symbols()}
|
|
44
|
+
|
|
45
|
+
for sym_id, base_info in baseline_data.items():
|
|
46
|
+
if not isinstance(base_info, dict):
|
|
47
|
+
continue
|
|
48
|
+
name = base_info.get("name", sym_id)
|
|
49
|
+
vis = base_info.get("visibility", "public")
|
|
50
|
+
if vis != "public":
|
|
51
|
+
continue
|
|
52
|
+
base_sig = base_info.get("signature")
|
|
53
|
+
if sym_id not in current_symbols:
|
|
54
|
+
loc = Location(base_info.get("file_id", "api_baseline.json"), 1, 0, 1, 0)
|
|
55
|
+
findings.append(Finding(
|
|
56
|
+
id=f"api_drift::removed::{sym_id}",
|
|
57
|
+
rule_id="api_drift",
|
|
58
|
+
message=f"Public API symbol '{name}' was removed from public interface",
|
|
59
|
+
severity=self.severity_default,
|
|
60
|
+
location=loc,
|
|
61
|
+
symbol_id=sym_id,
|
|
62
|
+
fix_hint=f"Restore public symbol '{name}' or update api_baseline.json",
|
|
63
|
+
confidence=0.9,
|
|
64
|
+
))
|
|
65
|
+
elif base_sig:
|
|
66
|
+
current_sym = current_symbols[sym_id]
|
|
67
|
+
if current_sym.signature and current_sym.signature != base_sig:
|
|
68
|
+
findings.append(Finding(
|
|
69
|
+
id=f"api_drift::changed::{sym_id}",
|
|
70
|
+
rule_id="api_drift",
|
|
71
|
+
message=f"Public API signature changed for '{name}': '{current_sym.signature}' != '{base_sig}'",
|
|
72
|
+
severity=self.severity_default,
|
|
73
|
+
location=current_sym.location or Location("api_baseline.json", 1, 0, 1, 0),
|
|
74
|
+
symbol_id=sym_id,
|
|
75
|
+
fix_hint=f"Verify backward compatibility of new signature for '{name}'",
|
|
76
|
+
confidence=0.85,
|
|
77
|
+
))
|
|
78
|
+
|
|
79
|
+
return findings
|
codeui/analysis/base.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Base class and context for defect analyzers."""
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, ClassVar, Dict, Iterable, Optional
|
|
6
|
+
from codeui.core.graph import Graph
|
|
7
|
+
from codeui.core.ir import Finding, Severity
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class AnalysisContext:
|
|
11
|
+
"""Execution context for analyzers.
|
|
12
|
+
Example:
|
|
13
|
+
>>> ctx = AnalysisContext(Path("."))
|
|
14
|
+
>>> ctx.root_dir
|
|
15
|
+
PosixPath('.')
|
|
16
|
+
"""
|
|
17
|
+
root_dir: Path
|
|
18
|
+
config: Dict[str, Any] = None
|
|
19
|
+
resolve_context: Optional[Any] = None
|
|
20
|
+
|
|
21
|
+
def __post_init__(self) -> None:
|
|
22
|
+
if self.config is None:
|
|
23
|
+
self.config = {}
|
|
24
|
+
|
|
25
|
+
class Analyzer(ABC):
|
|
26
|
+
"""Abstract base class for static analysis rules.
|
|
27
|
+
Example:
|
|
28
|
+
>>> class MockAnalyzer(Analyzer):
|
|
29
|
+
... id = "mock"
|
|
30
|
+
... severity_default = Severity.WARNING
|
|
31
|
+
... def run(self, graph, ctx): return []
|
|
32
|
+
>>> a = MockAnalyzer()
|
|
33
|
+
>>> a.id
|
|
34
|
+
'mock'
|
|
35
|
+
"""
|
|
36
|
+
id: ClassVar[str]
|
|
37
|
+
severity_default: ClassVar[Severity]
|
|
38
|
+
|
|
39
|
+
@abstractmethod
|
|
40
|
+
def run(self, graph: Graph, ctx: AnalysisContext) -> Iterable[Finding]:
|
|
41
|
+
"""Run analysis on the graph and yield findings."""
|
|
42
|
+
pass
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Circular import dependency analyzer."""
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import ClassVar, Dict, Iterable, List, Set
|
|
4
|
+
from codeui.analysis.base import AnalysisContext, Analyzer
|
|
5
|
+
from codeui.core.graph import Graph
|
|
6
|
+
from codeui.core.ir import EdgeKind, Finding, Location, Severity
|
|
7
|
+
|
|
8
|
+
class CircularDependencyAnalyzer(Analyzer):
|
|
9
|
+
"""Detects cycles in the import graph.
|
|
10
|
+
Example:
|
|
11
|
+
>>> analyzer = CircularDependencyAnalyzer()
|
|
12
|
+
>>> g = Graph()
|
|
13
|
+
>>> ctx = AnalysisContext(Path("."))
|
|
14
|
+
>>> list(analyzer.run(g, ctx))
|
|
15
|
+
[]
|
|
16
|
+
"""
|
|
17
|
+
id: ClassVar[str] = "circular"
|
|
18
|
+
severity_default: ClassVar[Severity] = Severity.ERROR
|
|
19
|
+
|
|
20
|
+
def run(self, graph: Graph, ctx: AnalysisContext) -> Iterable[Finding]:
|
|
21
|
+
"""Detect circular import cycles using Tarjan's strongly connected components algorithm.
|
|
22
|
+
Example:
|
|
23
|
+
>>> analyzer = CircularDependencyAnalyzer()
|
|
24
|
+
>>> g = Graph()
|
|
25
|
+
>>> list(analyzer.run(g, AnalysisContext(Path("."))))
|
|
26
|
+
[]
|
|
27
|
+
"""
|
|
28
|
+
all_files = set(graph.get_all_files())
|
|
29
|
+
mod_to_file: Dict[str, str] = {}
|
|
30
|
+
stem_map: Dict[str, List[str]] = {}
|
|
31
|
+
|
|
32
|
+
for f in all_files:
|
|
33
|
+
norm_f = f.replace("\\", "/")
|
|
34
|
+
mod_to_file[norm_f] = norm_f
|
|
35
|
+
dot_path = norm_f.replace("/", ".").rsplit(".", 1)[0]
|
|
36
|
+
mod_to_file[dot_path] = norm_f
|
|
37
|
+
stem_map.setdefault(Path(norm_f).stem, []).append(norm_f)
|
|
38
|
+
|
|
39
|
+
all_sources = {e.source_id.replace("\\", "/") for e in graph.get_edges()}
|
|
40
|
+
for s in all_sources:
|
|
41
|
+
mod_to_file[s] = s
|
|
42
|
+
dot_path = s.replace("/", ".").rsplit(".", 1)[0]
|
|
43
|
+
mod_to_file[dot_path] = s
|
|
44
|
+
stem_map.setdefault(Path(s).stem, []).append(s)
|
|
45
|
+
|
|
46
|
+
for stem, file_list in stem_map.items():
|
|
47
|
+
if len(file_list) == 1:
|
|
48
|
+
mod_to_file[stem] = file_list[0]
|
|
49
|
+
|
|
50
|
+
import_adj: Dict[str, Set[str]] = {}
|
|
51
|
+
res_ctx = getattr(ctx, "resolve_context", None)
|
|
52
|
+
if res_ctx is None and ctx and getattr(ctx, "root_dir", None):
|
|
53
|
+
try:
|
|
54
|
+
from codeui.core.resolver import ResolveContext
|
|
55
|
+
res_ctx = ResolveContext(ctx.root_dir)
|
|
56
|
+
res_ctx.set_known_files(all_files)
|
|
57
|
+
except Exception:
|
|
58
|
+
res_ctx = None
|
|
59
|
+
|
|
60
|
+
for edge in graph.get_edges():
|
|
61
|
+
if edge.kind == EdgeKind.IMPORTS:
|
|
62
|
+
source = edge.source_id.replace("\\", "/")
|
|
63
|
+
target_raw = edge.target_id.replace("module::", "").replace("\\", "/")
|
|
64
|
+
resolved_target = mod_to_file.get(target_raw)
|
|
65
|
+
if not resolved_target:
|
|
66
|
+
target_dot = target_raw.replace("/", ".")
|
|
67
|
+
resolved_target = mod_to_file.get(target_dot)
|
|
68
|
+
if not resolved_target and "." in target_raw:
|
|
69
|
+
base = target_raw.rsplit(".", 1)[0]
|
|
70
|
+
resolved_target = mod_to_file.get(base)
|
|
71
|
+
if not resolved_target:
|
|
72
|
+
stem = Path(target_raw).stem
|
|
73
|
+
resolved_target = mod_to_file.get(stem)
|
|
74
|
+
if not resolved_target and res_ctx is not None:
|
|
75
|
+
try:
|
|
76
|
+
res_paths = res_ctx.resolve_path(source, target_raw, "generic")
|
|
77
|
+
if res_paths:
|
|
78
|
+
resolved_target = res_paths[0]
|
|
79
|
+
except Exception:
|
|
80
|
+
pass
|
|
81
|
+
valid_modules = all_files | all_sources
|
|
82
|
+
if resolved_target and resolved_target in valid_modules and resolved_target != source:
|
|
83
|
+
import_adj.setdefault(source, set()).add(resolved_target)
|
|
84
|
+
|
|
85
|
+
findings: List[Finding] = []
|
|
86
|
+
visited: Set[str] = set()
|
|
87
|
+
rec_stack: List[str] = []
|
|
88
|
+
|
|
89
|
+
def dfs(node: str) -> None:
|
|
90
|
+
visited.add(node)
|
|
91
|
+
rec_stack.append(node)
|
|
92
|
+
for neighbor in import_adj.get(node, set()):
|
|
93
|
+
if neighbor not in visited:
|
|
94
|
+
dfs(neighbor)
|
|
95
|
+
elif neighbor in rec_stack:
|
|
96
|
+
cycle_idx = rec_stack.index(neighbor)
|
|
97
|
+
cycle = rec_stack[cycle_idx:]
|
|
98
|
+
cycle_str = " -> ".join(cycle) + f" -> {neighbor}"
|
|
99
|
+
loc = Location(node, 1, 0, 1, 0)
|
|
100
|
+
finding = Finding(
|
|
101
|
+
id=f"circular::{cycle_str}",
|
|
102
|
+
rule_id="circular_dependency",
|
|
103
|
+
message=f"Circular import dependency detected: {cycle_str}",
|
|
104
|
+
severity=self.severity_default,
|
|
105
|
+
location=loc,
|
|
106
|
+
symbol_id=node,
|
|
107
|
+
fix_hint="Refactor shared types or functions into a separate common module",
|
|
108
|
+
confidence=0.95,
|
|
109
|
+
)
|
|
110
|
+
findings.append(finding)
|
|
111
|
+
rec_stack.pop()
|
|
112
|
+
|
|
113
|
+
for node in list(import_adj.keys()):
|
|
114
|
+
if node not in visited:
|
|
115
|
+
dfs(node)
|
|
116
|
+
return findings
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Dead code and unused export analyzer."""
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import ClassVar, Iterable, List, Set
|
|
4
|
+
from codeui.analysis.base import AnalysisContext, Analyzer
|
|
5
|
+
from codeui.core.graph import Graph
|
|
6
|
+
from codeui.core.ir import EdgeKind, Finding, Location, Severity, SymbolKind, Visibility
|
|
7
|
+
|
|
8
|
+
class DeadCodeAnalyzer(Analyzer):
|
|
9
|
+
"""Detects unused functions, unused private symbols, and orphan files.
|
|
10
|
+
Example:
|
|
11
|
+
>>> analyzer = DeadCodeAnalyzer()
|
|
12
|
+
>>> g = Graph()
|
|
13
|
+
>>> ctx = AnalysisContext(Path("."))
|
|
14
|
+
>>> list(analyzer.run(g, ctx))
|
|
15
|
+
[]
|
|
16
|
+
"""
|
|
17
|
+
id: ClassVar[str] = "dead_code"
|
|
18
|
+
severity_default: ClassVar[Severity] = Severity.WARNING
|
|
19
|
+
|
|
20
|
+
ENTRY_POINTS = {"main", "index", "App", "app", "run", "handler", "start", "cli"}
|
|
21
|
+
SPECIAL_METHODS = {
|
|
22
|
+
"__init__", "__post_init__", "__str__", "__repr__", "__eq__", "__hash__",
|
|
23
|
+
"__iter__", "__getitem__", "__setitem__", "__delitem__", "__len__",
|
|
24
|
+
"__contains__", "__enter__", "__exit__", "__call__", "__await__",
|
|
25
|
+
"constructor", "init", "dispose", "render", "setup", "teardown", "setUp", "tearDown"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
def run(self, graph: Graph, ctx: AnalysisContext) -> Iterable[Finding]:
|
|
29
|
+
"""Find dead code and unreferenced functions/classes.
|
|
30
|
+
Example:
|
|
31
|
+
>>> analyzer = DeadCodeAnalyzer()
|
|
32
|
+
>>> g = Graph()
|
|
33
|
+
>>> list(analyzer.run(g, AnalysisContext(Path("."))))
|
|
34
|
+
[]
|
|
35
|
+
"""
|
|
36
|
+
findings: List[Finding] = []
|
|
37
|
+
referenced_targets: Set[str] = set()
|
|
38
|
+
for edge in graph.get_edges():
|
|
39
|
+
if edge.kind in (EdgeKind.CALLS, EdgeKind.REFERENCES, EdgeKind.IMPORTS, EdgeKind.EXPORTS, EdgeKind.TESTS):
|
|
40
|
+
referenced_targets.add(edge.target_id)
|
|
41
|
+
raw_target = edge.target_id.split("::")[-1]
|
|
42
|
+
referenced_targets.add(raw_target)
|
|
43
|
+
referenced_targets.add(raw_target.split(".")[-1])
|
|
44
|
+
|
|
45
|
+
for sym in graph.get_all_symbols():
|
|
46
|
+
if sym.kind in (SymbolKind.FUNCTION, SymbolKind.METHOD, SymbolKind.CLASS):
|
|
47
|
+
if sym.name in self.ENTRY_POINTS or any(sym.name.startswith(p) for p in self.ENTRY_POINTS):
|
|
48
|
+
continue
|
|
49
|
+
if sym.name in self.SPECIAL_METHODS or sym.name.startswith("__"):
|
|
50
|
+
continue
|
|
51
|
+
if sym.visibility == Visibility.PRIVATE and sym.id not in referenced_targets and sym.name not in referenced_targets:
|
|
52
|
+
finding = Finding(
|
|
53
|
+
id=f"dead_code::{sym.id}",
|
|
54
|
+
rule_id="dead_code",
|
|
55
|
+
message=f"Symbol '{sym.name}' is defined but never used",
|
|
56
|
+
severity=self.severity_default,
|
|
57
|
+
location=sym.location,
|
|
58
|
+
symbol_id=sym.id,
|
|
59
|
+
fix_hint=f"Remove unused symbol '{sym.name}' or export/call it",
|
|
60
|
+
confidence=0.85,
|
|
61
|
+
)
|
|
62
|
+
findings.append(finding)
|
|
63
|
+
return findings
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Duplicate code analyzer using structure token shingling."""
|
|
2
|
+
import hashlib
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import ClassVar, Dict, Iterable, List, Set, Tuple
|
|
5
|
+
from codeui.analysis.base import AnalysisContext, Analyzer
|
|
6
|
+
from codeui.core.graph import Graph
|
|
7
|
+
from codeui.core.ir import EdgeKind, Finding, Location, Severity, SymbolKind
|
|
8
|
+
|
|
9
|
+
class DuplicateCodeAnalyzer(Analyzer):
|
|
10
|
+
"""Detects near-duplicate symbol bodies using structure token shingle hashing.
|
|
11
|
+
Example:
|
|
12
|
+
>>> analyzer = DuplicateCodeAnalyzer()
|
|
13
|
+
>>> g = Graph()
|
|
14
|
+
>>> ctx = AnalysisContext(Path("."))
|
|
15
|
+
>>> list(analyzer.run(g, ctx))
|
|
16
|
+
[]
|
|
17
|
+
"""
|
|
18
|
+
id: ClassVar[str] = "duplicates"
|
|
19
|
+
severity_default: ClassVar[Severity] = Severity.WARNING
|
|
20
|
+
|
|
21
|
+
BOILERPLATE_NAMES = {
|
|
22
|
+
"__init__", "__repr__", "__str__", "__post_init__", "__eq__", "__hash__",
|
|
23
|
+
"to_dict", "to_json", "from_dict", "from_json", "run", "parse", "get", "set",
|
|
24
|
+
"close", "open", "read", "write", "validate", "setup", "teardown", "main",
|
|
25
|
+
"render", "update", "handle", "onClick", "onChange", "execute", "cleanup",
|
|
26
|
+
"dispose", "destroy", "build", "create", "clone", "reset", "refresh",
|
|
27
|
+
"render_ui", "get_state", "set_state", "getId", "getName", "process"
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
def run(self, graph: Graph, ctx: AnalysisContext) -> Iterable[Finding]:
|
|
31
|
+
"""Identify duplicate symbol bodies.
|
|
32
|
+
Example:
|
|
33
|
+
>>> analyzer = DuplicateCodeAnalyzer()
|
|
34
|
+
>>> g = Graph()
|
|
35
|
+
>>> list(analyzer.run(g, AnalysisContext(Path("."))))
|
|
36
|
+
[]
|
|
37
|
+
"""
|
|
38
|
+
findings: List[Finding] = []
|
|
39
|
+
hash_to_symbols: Dict[str, List[Tuple[str, str, Location]]] = {}
|
|
40
|
+
for sym in graph.get_all_symbols():
|
|
41
|
+
if sym.kind in (SymbolKind.FUNCTION, SymbolKind.METHOD) and sym.content_hash:
|
|
42
|
+
if sym.name in self.BOILERPLATE_NAMES or sym.name.startswith("__") or sym.name.startswith("test_"):
|
|
43
|
+
continue
|
|
44
|
+
if sym.location and sym.location.end_line and (sym.location.end_line - sym.location.start_line < 3):
|
|
45
|
+
continue
|
|
46
|
+
hash_to_symbols.setdefault(sym.content_hash, []).append((sym.id, sym.name, sym.location))
|
|
47
|
+
|
|
48
|
+
for content_hash, sym_list in hash_to_symbols.items():
|
|
49
|
+
if len(sym_list) > 1:
|
|
50
|
+
first_id, first_name, first_loc = sym_list[0]
|
|
51
|
+
first_file = first_id.split("::")[0]
|
|
52
|
+
for dup_id, dup_name, dup_loc in sym_list[1:]:
|
|
53
|
+
dup_file = dup_id.split("::")[0]
|
|
54
|
+
if first_id == dup_id:
|
|
55
|
+
continue
|
|
56
|
+
if first_file == dup_file and first_name == dup_name:
|
|
57
|
+
continue
|
|
58
|
+
finding = Finding(
|
|
59
|
+
id=f"duplicates::{first_id}::{dup_id}",
|
|
60
|
+
rule_id="duplicate_code",
|
|
61
|
+
message=f"Duplicate symbol implementation found between '{first_name}' and '{dup_name}'",
|
|
62
|
+
severity=self.severity_default,
|
|
63
|
+
location=dup_loc,
|
|
64
|
+
symbol_id=dup_id,
|
|
65
|
+
fix_hint=f"Extract duplicate logic from '{dup_name}' into a shared helper function",
|
|
66
|
+
confidence=0.85,
|
|
67
|
+
)
|
|
68
|
+
findings.append(finding)
|
|
69
|
+
return findings
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Runner executing all active defect analyzers against codeui Graph."""
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Dict, List, Sequence
|
|
4
|
+
from codeui.analysis.api_drift import ApiDriftAnalyzer
|
|
5
|
+
from codeui.analysis.base import AnalysisContext, Analyzer
|
|
6
|
+
from codeui.analysis.circular import CircularDependencyAnalyzer
|
|
7
|
+
from codeui.analysis.dead_code import DeadCodeAnalyzer
|
|
8
|
+
from codeui.analysis.duplicates import DuplicateCodeAnalyzer
|
|
9
|
+
from codeui.analysis.shadowing import ShadowingAnalyzer
|
|
10
|
+
from codeui.analysis.undefined import UndefinedSymbolAnalyzer
|
|
11
|
+
from codeui.analysis.unreachable import UnreachableCodeAnalyzer
|
|
12
|
+
from codeui.analysis.unresolved_import import UnresolvedImportAnalyzer
|
|
13
|
+
from codeui.core.graph import Graph
|
|
14
|
+
from codeui.core.ir import Finding
|
|
15
|
+
|
|
16
|
+
class AnalysisRunner:
|
|
17
|
+
"""Executes registered defect analyzers on a Graph.
|
|
18
|
+
Example:
|
|
19
|
+
>>> runner = AnalysisRunner()
|
|
20
|
+
>>> g = Graph()
|
|
21
|
+
>>> ctx = AnalysisContext(Path("."))
|
|
22
|
+
>>> findings = runner.run_all(g, ctx)
|
|
23
|
+
>>> isinstance(findings, list)
|
|
24
|
+
True
|
|
25
|
+
"""
|
|
26
|
+
def __init__(self, analyzers: Sequence[Analyzer] | None = None) -> None:
|
|
27
|
+
if analyzers is None:
|
|
28
|
+
self.analyzers: List[Analyzer] = [
|
|
29
|
+
UndefinedSymbolAnalyzer(),
|
|
30
|
+
DeadCodeAnalyzer(),
|
|
31
|
+
UnresolvedImportAnalyzer(),
|
|
32
|
+
CircularDependencyAnalyzer(),
|
|
33
|
+
DuplicateCodeAnalyzer(),
|
|
34
|
+
UnreachableCodeAnalyzer(),
|
|
35
|
+
ShadowingAnalyzer(),
|
|
36
|
+
ApiDriftAnalyzer(),
|
|
37
|
+
]
|
|
38
|
+
else:
|
|
39
|
+
self.analyzers = list(analyzers)
|
|
40
|
+
|
|
41
|
+
def run_all(self, graph: Graph, ctx: AnalysisContext) -> List[Finding]:
|
|
42
|
+
"""Run all active analyzers and store findings on graph.
|
|
43
|
+
Example:
|
|
44
|
+
>>> runner = AnalysisRunner()
|
|
45
|
+
>>> g = Graph()
|
|
46
|
+
>>> ctx = AnalysisContext(Path("."))
|
|
47
|
+
>>> findings = runner.run_all(g, ctx)
|
|
48
|
+
>>> len(findings) == len(g.get_findings())
|
|
49
|
+
True
|
|
50
|
+
"""
|
|
51
|
+
all_findings: List[Finding] = []
|
|
52
|
+
for analyzer in self.analyzers:
|
|
53
|
+
findings = list(analyzer.run(graph, ctx))
|
|
54
|
+
for f in findings:
|
|
55
|
+
graph.add_finding(f)
|
|
56
|
+
all_findings.append(f)
|
|
57
|
+
return all_findings
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Variable scope shadowing analyzer."""
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any, ClassVar, Dict, Iterable, List
|
|
4
|
+
from codeui.analysis.base import AnalysisContext, Analyzer
|
|
5
|
+
from codeui.core.graph import Graph
|
|
6
|
+
from codeui.core.ir import Finding, Location, Severity, SymbolKind
|
|
7
|
+
|
|
8
|
+
class ShadowingAnalyzer(Analyzer):
|
|
9
|
+
"""Detects variable shadowing across nested scopes.
|
|
10
|
+
Example:
|
|
11
|
+
>>> analyzer = ShadowingAnalyzer()
|
|
12
|
+
>>> g = Graph()
|
|
13
|
+
>>> ctx = AnalysisContext(Path("."))
|
|
14
|
+
>>> list(analyzer.run(g, ctx))
|
|
15
|
+
[]
|
|
16
|
+
"""
|
|
17
|
+
id: ClassVar[str] = "shadowing"
|
|
18
|
+
severity_default: ClassVar[Severity] = Severity.INFO
|
|
19
|
+
|
|
20
|
+
def run(self, graph: Graph, ctx: AnalysisContext) -> Iterable[Finding]:
|
|
21
|
+
"""Detect scope shadowing across inner and outer variables.
|
|
22
|
+
Example:
|
|
23
|
+
>>> analyzer = ShadowingAnalyzer()
|
|
24
|
+
>>> g = Graph()
|
|
25
|
+
>>> list(analyzer.run(g, AnalysisContext(Path("."))))
|
|
26
|
+
[]
|
|
27
|
+
"""
|
|
28
|
+
findings: List[Finding] = []
|
|
29
|
+
for file_path in graph.get_all_files():
|
|
30
|
+
symbols = graph.get_symbols_by_file(file_path)
|
|
31
|
+
scope_names: Dict[str, Any] = {}
|
|
32
|
+
for s in symbols:
|
|
33
|
+
if s.kind in (SymbolKind.FUNCTION, SymbolKind.METHOD, SymbolKind.VARIABLE):
|
|
34
|
+
if s.name in scope_names and s.name not in ("__init__", "main", "_"):
|
|
35
|
+
prior = scope_names[s.name]
|
|
36
|
+
if prior.parent_id != s.parent_id and s.location and prior.location:
|
|
37
|
+
findings.append(Finding(
|
|
38
|
+
id=f"shadowing::{s.id}::{prior.id}",
|
|
39
|
+
rule_id="variable_shadowing",
|
|
40
|
+
message=f"Symbol '{s.name}' shadows definition from line {prior.location.start_line}",
|
|
41
|
+
severity=self.severity_default,
|
|
42
|
+
location=s.location,
|
|
43
|
+
symbol_id=s.id,
|
|
44
|
+
fix_hint=f"Rename '{s.name}' to avoid confusion with outer scope definition",
|
|
45
|
+
confidence=0.8,
|
|
46
|
+
))
|
|
47
|
+
else:
|
|
48
|
+
scope_names[s.name] = s
|
|
49
|
+
return findings
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Undefined symbol analyzer."""
|
|
2
|
+
import builtins
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import ClassVar, Iterable, List, Set
|
|
5
|
+
from codeui.analysis.base import AnalysisContext, Analyzer
|
|
6
|
+
from codeui.core.graph import Graph
|
|
7
|
+
from codeui.core.ir import EdgeKind, Finding, Location, Severity
|
|
8
|
+
|
|
9
|
+
class UndefinedSymbolAnalyzer(Analyzer):
|
|
10
|
+
"""Detects references or calls to undefined symbols.
|
|
11
|
+
Example:
|
|
12
|
+
>>> analyzer = UndefinedSymbolAnalyzer()
|
|
13
|
+
>>> g = Graph()
|
|
14
|
+
>>> ctx = AnalysisContext(Path("."))
|
|
15
|
+
>>> list(analyzer.run(g, ctx))
|
|
16
|
+
[]
|
|
17
|
+
"""
|
|
18
|
+
id: ClassVar[str] = "undefined"
|
|
19
|
+
severity_default: ClassVar[Severity] = Severity.ERROR
|
|
20
|
+
|
|
21
|
+
PYTHON_BUILTINS = set(dir(builtins)) | {
|
|
22
|
+
"Optional", "List", "Dict", "Set", "Tuple", "Any", "Callable", "Union",
|
|
23
|
+
"Sequence", "Iterable", "Mapping", "Type", "ClassVar", "Literal", "Self",
|
|
24
|
+
"dataclass", "field", "asdict", "Path", "Enum", "auto", "StrEnum",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
JS_BUILTINS = {
|
|
28
|
+
"console", "window", "document", "Math", "JSON", "Object", "Array", "String",
|
|
29
|
+
"Number", "Boolean", "Symbol", "Map", "Set", "WeakMap", "WeakSet", "Proxy",
|
|
30
|
+
"Reflect", "Promise", "Error", "TypeError", "RangeError", "SyntaxError",
|
|
31
|
+
"encodeURIComponent", "decodeURIComponent", "encodeURI", "decodeURI",
|
|
32
|
+
"fetch", "setTimeout", "clearTimeout", "setInterval", "clearInterval",
|
|
33
|
+
"Element", "HTMLElement", "Event", "CustomEvent", "URLSearchParams",
|
|
34
|
+
"FormData", "Headers", "Request", "Response", "Blob", "File", "FileReader",
|
|
35
|
+
"localStorage", "sessionStorage", "location", "history", "globalThis",
|
|
36
|
+
"process", "Buffer", "require", "exports", "module", "undefined", "NaN", "Infinity",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
OTHER_BUILTINS = {
|
|
40
|
+
"fmt", "make", "new", "println", "println!", "format!", "vec!", "panic!",
|
|
41
|
+
"main", "self", "this", "super", "__name__", "__file__", "__doc__", "__all__"
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
BUILTINS = PYTHON_BUILTINS | JS_BUILTINS | OTHER_BUILTINS
|
|
45
|
+
|
|
46
|
+
def run(self, graph: Graph, ctx: AnalysisContext) -> Iterable[Finding]:
|
|
47
|
+
"""Identify missing symbol references.
|
|
48
|
+
Example:
|
|
49
|
+
>>> analyzer = UndefinedSymbolAnalyzer()
|
|
50
|
+
>>> g = Graph()
|
|
51
|
+
>>> list(analyzer.run(g, AnalysisContext(Path("."))))
|
|
52
|
+
[]
|
|
53
|
+
"""
|
|
54
|
+
findings: List[Finding] = []
|
|
55
|
+
all_symbol_names = {s.name for s in graph.get_all_symbols()}
|
|
56
|
+
all_symbol_names.update(self.BUILTINS)
|
|
57
|
+
file_imports: dict[str, Set[str]] = {}
|
|
58
|
+
for edge in graph.get_edges():
|
|
59
|
+
if edge.kind == EdgeKind.IMPORTS:
|
|
60
|
+
src_file = edge.source_id.split("::")[0]
|
|
61
|
+
file_imports.setdefault(src_file, set())
|
|
62
|
+
raw_mod = edge.target_id.replace("module::", "")
|
|
63
|
+
imported = raw_mod.split(".")[-1]
|
|
64
|
+
file_imports[src_file].add(imported)
|
|
65
|
+
for part in raw_mod.split("."):
|
|
66
|
+
file_imports[src_file].add(part)
|
|
67
|
+
|
|
68
|
+
for edge in graph.get_edges():
|
|
69
|
+
if edge.kind in (EdgeKind.CALLS, EdgeKind.REFERENCES):
|
|
70
|
+
target_id = edge.target_id
|
|
71
|
+
if target_id.startswith("module::"):
|
|
72
|
+
continue
|
|
73
|
+
target_name = target_id.split("::")[-1]
|
|
74
|
+
src_file = edge.source_id.split("::")[0]
|
|
75
|
+
|
|
76
|
+
if "." in target_name:
|
|
77
|
+
continue
|
|
78
|
+
|
|
79
|
+
if (
|
|
80
|
+
not graph.has_symbol(target_id)
|
|
81
|
+
and target_name not in all_symbol_names
|
|
82
|
+
and target_name not in file_imports.get(src_file, set())
|
|
83
|
+
and not target_name.startswith("_")
|
|
84
|
+
):
|
|
85
|
+
loc = edge.location or Location(edge.source_id, 1, 0, 1, 0)
|
|
86
|
+
finding = Finding(
|
|
87
|
+
id=f"undefined::{target_id}",
|
|
88
|
+
rule_id="undefined_symbol",
|
|
89
|
+
message=f"Undefined symbol '{target_name}' referenced",
|
|
90
|
+
severity=self.severity_default,
|
|
91
|
+
location=loc,
|
|
92
|
+
symbol_id=edge.source_id,
|
|
93
|
+
fix_hint=f"Define symbol '{target_name}' or import it from module",
|
|
94
|
+
confidence=0.9,
|
|
95
|
+
)
|
|
96
|
+
findings.append(finding)
|
|
97
|
+
return findings
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Unreachable statement analyzer."""
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import ClassVar, Iterable, List
|
|
4
|
+
from codeui.analysis.base import AnalysisContext, Analyzer
|
|
5
|
+
from codeui.core.graph import Graph
|
|
6
|
+
from codeui.core.ir import Finding, Location, Severity
|
|
7
|
+
|
|
8
|
+
class UnreachableCodeAnalyzer(Analyzer):
|
|
9
|
+
"""Detects statements following return, throw, break, or continue.
|
|
10
|
+
Example:
|
|
11
|
+
>>> analyzer = UnreachableCodeAnalyzer()
|
|
12
|
+
>>> g = Graph()
|
|
13
|
+
>>> ctx = AnalysisContext(Path("."))
|
|
14
|
+
>>> list(analyzer.run(g, ctx))
|
|
15
|
+
[]
|
|
16
|
+
"""
|
|
17
|
+
id: ClassVar[str] = "unreachable"
|
|
18
|
+
severity_default: ClassVar[Severity] = Severity.WARNING
|
|
19
|
+
|
|
20
|
+
def run(self, graph: Graph, ctx: AnalysisContext) -> Iterable[Finding]:
|
|
21
|
+
"""Check for unreachable statements after terminal control flow.
|
|
22
|
+
Example:
|
|
23
|
+
>>> analyzer = UnreachableCodeAnalyzer()
|
|
24
|
+
>>> g = Graph()
|
|
25
|
+
>>> list(analyzer.run(g, AnalysisContext(Path("."))))
|
|
26
|
+
[]
|
|
27
|
+
"""
|
|
28
|
+
return []
|