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.
Files changed (52) hide show
  1. codeui/__init__.py +23 -0
  2. codeui/agent/__init__.py +15 -0
  3. codeui/agent/context.py +207 -0
  4. codeui/agent/contribution.py +224 -0
  5. codeui/agent/edit_tools.py +280 -0
  6. codeui/agent/integrations.py +243 -0
  7. codeui/agent/mcp_server.py +94 -0
  8. codeui/analysis/__init__.py +1 -0
  9. codeui/analysis/api_drift.py +79 -0
  10. codeui/analysis/base.py +42 -0
  11. codeui/analysis/circular.py +116 -0
  12. codeui/analysis/dead_code.py +63 -0
  13. codeui/analysis/duplicates.py +69 -0
  14. codeui/analysis/runner.py +57 -0
  15. codeui/analysis/shadowing.py +49 -0
  16. codeui/analysis/undefined.py +97 -0
  17. codeui/analysis/unreachable.py +28 -0
  18. codeui/analysis/unresolved_import.py +153 -0
  19. codeui/cli/__init__.py +1 -0
  20. codeui/cli/main.py +437 -0
  21. codeui/core/__init__.py +1 -0
  22. codeui/core/cache.py +135 -0
  23. codeui/core/graph.py +435 -0
  24. codeui/core/ir.py +218 -0
  25. codeui/core/override.py +230 -0
  26. codeui/core/repo.py +85 -0
  27. codeui/core/resolver.py +281 -0
  28. codeui/errors.py +98 -0
  29. codeui/lang/__init__.py +1 -0
  30. codeui/lang/base.py +117 -0
  31. codeui/lang/generic.py +90 -0
  32. codeui/lang/go.py +167 -0
  33. codeui/lang/python.py +294 -0
  34. codeui/lang/registry.py +83 -0
  35. codeui/lang/rust.py +185 -0
  36. codeui/lang/ts.py +396 -0
  37. codeui/plugins/__init__.py +1 -0
  38. codeui/plugins/registry.py +31 -0
  39. codeui/report/__init__.py +1 -0
  40. codeui/report/json_emitter.py +24 -0
  41. codeui/report/markdown_emitter.py +33 -0
  42. codeui/report/sarif_emitter.py +56 -0
  43. codeui/server/__init__.py +1 -0
  44. codeui/server/server.py +705 -0
  45. codeui/tracer/__init__.py +1 -0
  46. codeui/tracer/tracer.py +147 -0
  47. codeui_python-0.1.0.dist-info/METADATA +191 -0
  48. codeui_python-0.1.0.dist-info/RECORD +52 -0
  49. codeui_python-0.1.0.dist-info/WHEEL +5 -0
  50. codeui_python-0.1.0.dist-info/entry_points.txt +2 -0
  51. codeui_python-0.1.0.dist-info/licenses/LICENSE +21 -0
  52. 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
@@ -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 []