code-oracle 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.
- code_oracle/__init__.py +30 -0
- code_oracle/cli.py +795 -0
- code_oracle/config.py +145 -0
- code_oracle/dataset.py +5325 -0
- code_oracle/dead_code/__init__.py +32 -0
- code_oracle/dead_code/detector.py +379 -0
- code_oracle/dead_code/entrypoints.py +333 -0
- code_oracle/dead_code/models.py +255 -0
- code_oracle/dead_code/semantics.py +416 -0
- code_oracle/decision.py +906 -0
- code_oracle/engine.py +430 -0
- code_oracle/export_onnx.py +436 -0
- code_oracle/hook.py +531 -0
- code_oracle/indexer.py +894 -0
- code_oracle/languages/__init__.py +114 -0
- code_oracle/languages/common.py +127 -0
- code_oracle/languages/go.py +395 -0
- code_oracle/languages/python.py +336 -0
- code_oracle/languages/rust.py +474 -0
- code_oracle/languages/typescript.py +775 -0
- code_oracle/linearizer.py +166 -0
- code_oracle/locator.py +301 -0
- code_oracle/models.py +237 -0
- code_oracle/perf_lint/__init__.py +38 -0
- code_oracle/perf_lint/engine.py +234 -0
- code_oracle/perf_lint/models.py +229 -0
- code_oracle/perf_lint/rules/__init__.py +31 -0
- code_oracle/perf_lint/rules/async_blocking.py +143 -0
- code_oracle/perf_lint/rules/n_plus_one.py +232 -0
- code_oracle/perf_lint/rules/nested_loops.py +137 -0
- code_oracle/perf_lint/rules/unclosed_res.py +494 -0
- code_oracle/perf_lint/visitor.py +299 -0
- code_oracle/server.py +184 -0
- code_oracle/slicer.py +225 -0
- code_oracle/symbolic.py +459 -0
- code_oracle-0.1.0.dist-info/METADATA +225 -0
- code_oracle-0.1.0.dist-info/RECORD +40 -0
- code_oracle-0.1.0.dist-info/WHEEL +4 -0
- code_oracle-0.1.0.dist-info/entry_points.txt +2 -0
- code_oracle-0.1.0.dist-info/licenses/LICENSE +190 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Multi-language root and entrypoint heuristic detector.
|
|
3
|
+
Identifies execution roots across Python, TypeScript/JavaScript, Go, and Rust:
|
|
4
|
+
- Test files and test functions
|
|
5
|
+
- CLI commands and framework decorators (@click, @app.command)
|
|
6
|
+
- HTTP routes (@app.get, @app.post, @router.get, @bp.route)
|
|
7
|
+
- Public root exports (__init__.py, index.ts, mod.rs, lib.rs, main.go)
|
|
8
|
+
- Public uppercase Go symbols in root package
|
|
9
|
+
- Dunder/magic methods and language constructors
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Dict, List, Optional, Set
|
|
15
|
+
|
|
16
|
+
from code_oracle.models import Symbol
|
|
17
|
+
|
|
18
|
+
# Regex patterns for route and framework decorators
|
|
19
|
+
ROUTE_DECORATOR_PATTERN = re.compile(
|
|
20
|
+
r"@\s*(?:(?:[\w\.]+\.)?(?:app|router|bp|api|server|web)\s*\.\s*(?:get|post|put|delete|patch|options|head|route|websocket)|(?:Get|Post|Put|Delete|Patch|Controller|Route)\b)",
|
|
21
|
+
re.IGNORECASE,
|
|
22
|
+
)
|
|
23
|
+
CLI_DECORATOR_PATTERN = re.compile(
|
|
24
|
+
r"@\s*(?:[\w\.]+\.)?(?:click|typer|app|cli|main)\s*\.\s*(?:command|group|option|argument)\b",
|
|
25
|
+
re.IGNORECASE,
|
|
26
|
+
)
|
|
27
|
+
TEST_DECORATOR_PATTERN = re.compile(
|
|
28
|
+
r"@\s*(?:pytest\s*\.\s*fixture|fixture)\b|#\[(?:tokio::test|test|actix_web::|rocket::)",
|
|
29
|
+
re.IGNORECASE,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class EntrypointDetector:
|
|
34
|
+
"""Detects framework entrypoints, test suites, and root exports."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, workspace_root: Optional[Path] = None):
|
|
37
|
+
self.workspace_root = (workspace_root or Path.cwd()).resolve()
|
|
38
|
+
self._source_cache: Dict[str, List[str]] = {}
|
|
39
|
+
|
|
40
|
+
def get_source_lines(self, rel_path: str) -> List[str]:
|
|
41
|
+
"""Fetch and cache lines of a source file."""
|
|
42
|
+
norm_path = rel_path.replace("\\", "/")
|
|
43
|
+
if norm_path in self._source_cache:
|
|
44
|
+
return self._source_cache[norm_path]
|
|
45
|
+
|
|
46
|
+
full_p = self.workspace_root / norm_path
|
|
47
|
+
if full_p.is_file():
|
|
48
|
+
try:
|
|
49
|
+
lines = full_p.read_text(encoding="utf-8", errors="replace").splitlines()
|
|
50
|
+
self._source_cache[norm_path] = lines
|
|
51
|
+
return lines
|
|
52
|
+
except Exception:
|
|
53
|
+
pass
|
|
54
|
+
self._source_cache[norm_path] = []
|
|
55
|
+
return []
|
|
56
|
+
|
|
57
|
+
def is_test_file(self, file_path: str) -> bool:
|
|
58
|
+
"""Check if file is located in a test directory or matches test naming."""
|
|
59
|
+
clean = file_path.replace("\\", "/").lower()
|
|
60
|
+
parts = clean.split("/")
|
|
61
|
+
|
|
62
|
+
# Directory checks
|
|
63
|
+
if any(p in ("tests", "test", "__tests__", "spec") for p in parts):
|
|
64
|
+
return True
|
|
65
|
+
|
|
66
|
+
# Extension/filename checks
|
|
67
|
+
file_name = parts[-1]
|
|
68
|
+
if file_name.startswith("test_") or file_name.endswith("_test.py"):
|
|
69
|
+
return True
|
|
70
|
+
if file_name.endswith("_test.go"):
|
|
71
|
+
return True
|
|
72
|
+
if (
|
|
73
|
+
file_name.endswith((".spec.ts", ".test.ts", ".spec.tsx", ".test.tsx"))
|
|
74
|
+
or file_name.endswith((".spec.js", ".test.js", ".spec.jsx", ".test.jsx"))
|
|
75
|
+
):
|
|
76
|
+
return True
|
|
77
|
+
if file_name.endswith("_test.rs"):
|
|
78
|
+
return True
|
|
79
|
+
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
def get_symbol_decorators(self, symbol: Symbol) -> List[str]:
|
|
83
|
+
"""
|
|
84
|
+
Scan lines preceding the symbol's lineno for decorators.
|
|
85
|
+
Robustly handles multi-line decorator blocks and comments.
|
|
86
|
+
"""
|
|
87
|
+
lines = self.get_source_lines(symbol.file_path)
|
|
88
|
+
if not lines or symbol.lineno < 1 or symbol.lineno > len(lines):
|
|
89
|
+
return []
|
|
90
|
+
|
|
91
|
+
decorators: List[str] = []
|
|
92
|
+
idx = symbol.lineno - 2 # 0-indexed line immediately above symbol
|
|
93
|
+
accum: List[str] = []
|
|
94
|
+
paren_depth = 0
|
|
95
|
+
bracket_depth = 0
|
|
96
|
+
|
|
97
|
+
while idx >= 0:
|
|
98
|
+
line = lines[idx].strip()
|
|
99
|
+
if not line:
|
|
100
|
+
if not accum:
|
|
101
|
+
idx -= 1
|
|
102
|
+
continue
|
|
103
|
+
else:
|
|
104
|
+
accum.append(line)
|
|
105
|
+
idx -= 1
|
|
106
|
+
continue
|
|
107
|
+
|
|
108
|
+
# Standalone comments between decorators
|
|
109
|
+
if not accum and (line.startswith("#") or line.startswith("//")):
|
|
110
|
+
idx -= 1
|
|
111
|
+
continue
|
|
112
|
+
|
|
113
|
+
open_parens = line.count("(")
|
|
114
|
+
close_parens = line.count(")")
|
|
115
|
+
open_brackets = line.count("[")
|
|
116
|
+
close_brackets = line.count("]")
|
|
117
|
+
|
|
118
|
+
paren_depth += (close_parens - open_parens)
|
|
119
|
+
bracket_depth += (close_brackets - open_brackets)
|
|
120
|
+
|
|
121
|
+
accum.append(line)
|
|
122
|
+
|
|
123
|
+
# Check if this line is the beginning of a decorator (@ or #[)
|
|
124
|
+
if line.startswith("@") or line.startswith("#["):
|
|
125
|
+
if paren_depth <= 0 and bracket_depth <= 0:
|
|
126
|
+
dec_text = "\n".join(reversed(accum)).strip()
|
|
127
|
+
decorators.append(dec_text)
|
|
128
|
+
accum = []
|
|
129
|
+
paren_depth = 0
|
|
130
|
+
bracket_depth = 0
|
|
131
|
+
idx -= 1
|
|
132
|
+
continue
|
|
133
|
+
|
|
134
|
+
# If not inside a decorator call and line is not a decorator header, decorator block ended
|
|
135
|
+
if paren_depth <= 0 and bracket_depth <= 0:
|
|
136
|
+
break
|
|
137
|
+
|
|
138
|
+
idx -= 1
|
|
139
|
+
|
|
140
|
+
return decorators
|
|
141
|
+
|
|
142
|
+
def is_test_symbol(self, symbol: Symbol) -> bool:
|
|
143
|
+
"""Check if symbol represents a test function, benchmark, or test suite."""
|
|
144
|
+
if self.is_test_file(symbol.file_path):
|
|
145
|
+
return True
|
|
146
|
+
|
|
147
|
+
name = symbol.name
|
|
148
|
+
# Python / JS / Rust test naming
|
|
149
|
+
if name.startswith("test_") or (name.startswith("Test") and symbol.kind in ("class", "function")):
|
|
150
|
+
return True
|
|
151
|
+
|
|
152
|
+
# Go test naming
|
|
153
|
+
if symbol.file_path.endswith(".go"):
|
|
154
|
+
if name.startswith(("Test", "Benchmark", "Fuzz", "Example")):
|
|
155
|
+
return True
|
|
156
|
+
|
|
157
|
+
# Pytest fixture or Rust #[test] check in decorator/calls
|
|
158
|
+
if any(c.callee.endswith(("fixture", "pytest.fixture")) for c in symbol.calls):
|
|
159
|
+
return True
|
|
160
|
+
|
|
161
|
+
# Check source lines for test decorators
|
|
162
|
+
decorators = self.get_symbol_decorators(symbol)
|
|
163
|
+
if any(TEST_DECORATOR_PATTERN.search(dec) for dec in decorators):
|
|
164
|
+
return True
|
|
165
|
+
|
|
166
|
+
return False
|
|
167
|
+
|
|
168
|
+
def is_property_method(self, symbol: Symbol) -> bool:
|
|
169
|
+
"""Check if symbol is decorated with @property, @cached_property, @setter, etc."""
|
|
170
|
+
if not symbol.is_method:
|
|
171
|
+
return False
|
|
172
|
+
decorators = self.get_symbol_decorators(symbol)
|
|
173
|
+
for dec in decorators:
|
|
174
|
+
clean = dec.lower()
|
|
175
|
+
if any(p in clean for p in ("@property", "@cached_property", ".setter", ".deleter", "@abstractmethod", "@overload")):
|
|
176
|
+
return True
|
|
177
|
+
return False
|
|
178
|
+
|
|
179
|
+
def is_cli_symbol(self, symbol: Symbol) -> bool:
|
|
180
|
+
"""Check if symbol is a CLI command or program entrypoint."""
|
|
181
|
+
name = symbol.name
|
|
182
|
+
clean_file = symbol.file_path.replace("\\", "/").lower()
|
|
183
|
+
parts = clean_file.split("/")
|
|
184
|
+
filename = parts[-1]
|
|
185
|
+
|
|
186
|
+
# Standalone main/cli functions
|
|
187
|
+
if name in ("main", "cli", "run_cli", "app"):
|
|
188
|
+
return True
|
|
189
|
+
|
|
190
|
+
# Go / Rust runtime entrypoints
|
|
191
|
+
if name == "main" and symbol.kind == "function":
|
|
192
|
+
return True
|
|
193
|
+
if symbol.file_path.endswith(".go") and name == "init":
|
|
194
|
+
return True
|
|
195
|
+
|
|
196
|
+
# Known CLI files or CLI command naming conventions
|
|
197
|
+
if filename in ("cli.py", "main.py", "__main__.py", "main.go", "main.rs", "commands.py", "cmd.py"):
|
|
198
|
+
if (
|
|
199
|
+
name in ("main", "cli", "run", "execute", "start")
|
|
200
|
+
or name.startswith(("cmd_", "do_"))
|
|
201
|
+
or name.endswith(("_command", "_cmd"))
|
|
202
|
+
):
|
|
203
|
+
return True
|
|
204
|
+
|
|
205
|
+
# General CLI command naming convention across any file
|
|
206
|
+
if name.startswith("cmd_") and symbol.kind in ("function", "async_function"):
|
|
207
|
+
return True
|
|
208
|
+
|
|
209
|
+
# Click / Typer / CLI decorators in calls
|
|
210
|
+
for c in symbol.calls:
|
|
211
|
+
if any(kw in c.callee for kw in ("click.command", "click.group", "app.command", "cli.command", "typer.command")):
|
|
212
|
+
return True
|
|
213
|
+
|
|
214
|
+
# Inspect source lines for @click or @app.command
|
|
215
|
+
decorators = self.get_symbol_decorators(symbol)
|
|
216
|
+
if any(CLI_DECORATOR_PATTERN.search(dec) for dec in decorators):
|
|
217
|
+
return True
|
|
218
|
+
|
|
219
|
+
return False
|
|
220
|
+
|
|
221
|
+
def is_route_symbol(self, symbol: Symbol) -> bool:
|
|
222
|
+
"""Check if symbol is a web/HTTP route handler (@app.get, @router.post, etc.)."""
|
|
223
|
+
# Check call references
|
|
224
|
+
for c in symbol.calls:
|
|
225
|
+
callee_lower = c.callee.lower()
|
|
226
|
+
if any(
|
|
227
|
+
route_kw in callee_lower
|
|
228
|
+
for route_kw in (
|
|
229
|
+
"app.get", "app.post", "app.put", "app.delete", "app.patch", "app.route",
|
|
230
|
+
"router.get", "router.post", "router.put", "router.delete", "router.patch",
|
|
231
|
+
"bp.route", "bp.get", "bp.post",
|
|
232
|
+
)
|
|
233
|
+
):
|
|
234
|
+
return True
|
|
235
|
+
|
|
236
|
+
# Inspect source lines for route decorators
|
|
237
|
+
decorators = self.get_symbol_decorators(symbol)
|
|
238
|
+
if any(ROUTE_DECORATOR_PATTERN.search(dec) for dec in decorators):
|
|
239
|
+
return True
|
|
240
|
+
|
|
241
|
+
return False
|
|
242
|
+
|
|
243
|
+
def is_root_export(self, symbol: Symbol) -> bool:
|
|
244
|
+
"""Check if symbol is defined in a public root export file."""
|
|
245
|
+
clean = symbol.file_path.replace("\\", "/")
|
|
246
|
+
parts = clean.split("/")
|
|
247
|
+
filename = parts[-1]
|
|
248
|
+
|
|
249
|
+
# Python root exports
|
|
250
|
+
if filename == "__init__.py":
|
|
251
|
+
return True
|
|
252
|
+
|
|
253
|
+
# TypeScript / JavaScript root exports
|
|
254
|
+
if filename in ("index.ts", "index.tsx", "index.js", "index.jsx", "main.ts", "main.tsx"):
|
|
255
|
+
return True
|
|
256
|
+
|
|
257
|
+
# Rust root exports
|
|
258
|
+
if filename in ("mod.rs", "lib.rs", "main.rs"):
|
|
259
|
+
return True
|
|
260
|
+
|
|
261
|
+
# Go main file
|
|
262
|
+
if filename == "main.go":
|
|
263
|
+
return True
|
|
264
|
+
|
|
265
|
+
return False
|
|
266
|
+
|
|
267
|
+
def is_public_go_symbol(self, symbol: Symbol) -> bool:
|
|
268
|
+
"""Check if symbol is an exported Go symbol in the root package."""
|
|
269
|
+
if not symbol.file_path.endswith(".go"):
|
|
270
|
+
return False
|
|
271
|
+
|
|
272
|
+
clean = symbol.file_path.replace("\\", "/")
|
|
273
|
+
# Root package check (in root directory or top-level file)
|
|
274
|
+
is_root_dir = "/" not in clean or clean.startswith("./") and clean.count("/") == 1
|
|
275
|
+
if is_root_dir or clean.startswith("main."):
|
|
276
|
+
if symbol.name and symbol.name[0].isupper():
|
|
277
|
+
return True
|
|
278
|
+
|
|
279
|
+
return False
|
|
280
|
+
|
|
281
|
+
def is_magic_method(self, symbol: Symbol) -> bool:
|
|
282
|
+
"""Check if symbol is a language dunder/magic method or constructor."""
|
|
283
|
+
name = symbol.name
|
|
284
|
+
# Python dunder methods
|
|
285
|
+
if name.startswith("__") and name.endswith("__"):
|
|
286
|
+
return True
|
|
287
|
+
|
|
288
|
+
# TypeScript / JavaScript constructor
|
|
289
|
+
if name == "constructor":
|
|
290
|
+
return True
|
|
291
|
+
|
|
292
|
+
return False
|
|
293
|
+
|
|
294
|
+
def is_entrypoint(self, symbol: Symbol) -> bool:
|
|
295
|
+
"""
|
|
296
|
+
Evaluate if a symbol qualifies as an execution or API root.
|
|
297
|
+
Roots are the starting seeds for graph reachability analysis.
|
|
298
|
+
"""
|
|
299
|
+
# Module-level blocks are always execution entrypoints
|
|
300
|
+
if symbol.kind == "module" or symbol.name == "<module>":
|
|
301
|
+
return True
|
|
302
|
+
|
|
303
|
+
# Test suites and functions
|
|
304
|
+
if self.is_test_symbol(symbol):
|
|
305
|
+
return True
|
|
306
|
+
|
|
307
|
+
# CLI entrypoints
|
|
308
|
+
if self.is_cli_symbol(symbol):
|
|
309
|
+
return True
|
|
310
|
+
|
|
311
|
+
# HTTP route handlers
|
|
312
|
+
if self.is_route_symbol(symbol):
|
|
313
|
+
return True
|
|
314
|
+
|
|
315
|
+
# Public root exports
|
|
316
|
+
if self.is_root_export(symbol):
|
|
317
|
+
return True
|
|
318
|
+
|
|
319
|
+
# Public Go symbols in root package
|
|
320
|
+
if self.is_public_go_symbol(symbol):
|
|
321
|
+
return True
|
|
322
|
+
|
|
323
|
+
# Dunder and magic methods
|
|
324
|
+
if self.is_magic_method(symbol):
|
|
325
|
+
return True
|
|
326
|
+
|
|
327
|
+
return False
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def is_entrypoint(symbol: Symbol, detector: Optional[EntrypointDetector] = None) -> bool:
|
|
331
|
+
"""Convenience helper to check if a symbol is an entrypoint root."""
|
|
332
|
+
det = detector or EntrypointDetector()
|
|
333
|
+
return det.is_entrypoint(symbol)
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Data models for the Graph Reachability Dead Code Engine.
|
|
3
|
+
Defines representation for dead/orphan symbols and scan reports.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from typing import Any, Dict, List, Optional, Union
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SemanticClassification(str, Enum):
|
|
12
|
+
"""Semantic classification for unreachable symbols."""
|
|
13
|
+
PUBLIC_API_SURFACE = "PUBLIC_API_SURFACE"
|
|
14
|
+
INTERNAL_ORPHAN = "INTERNAL_ORPHAN"
|
|
15
|
+
GENUINE_CRUFT = "GENUINE_CRUFT"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class SemanticDeadSymbol:
|
|
20
|
+
"""Rich semantic dead code symbol representation."""
|
|
21
|
+
id: str
|
|
22
|
+
name: str
|
|
23
|
+
qualname: str
|
|
24
|
+
file_path: str
|
|
25
|
+
kind: str
|
|
26
|
+
lineno: int
|
|
27
|
+
end_lineno: int
|
|
28
|
+
is_orphan: bool
|
|
29
|
+
is_transitive: bool
|
|
30
|
+
cluster_id: Optional[str]
|
|
31
|
+
raw_reachability_confidence: float
|
|
32
|
+
semantic_classification: SemanticClassification
|
|
33
|
+
calibrated_confidence: float
|
|
34
|
+
semantic_probabilities: Dict[str, float]
|
|
35
|
+
suppressed: bool
|
|
36
|
+
reason: str
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def lines_count(self) -> int:
|
|
40
|
+
"""Count of lines occupied by this symbol."""
|
|
41
|
+
return max(1, self.end_lineno - self.lineno + 1)
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def confidence(self) -> float:
|
|
45
|
+
"""Alias for backward compatibility with DeadSymbol."""
|
|
46
|
+
return self.calibrated_confidence
|
|
47
|
+
|
|
48
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
49
|
+
"""Serialize semantic dead symbol representation to dictionary."""
|
|
50
|
+
return {
|
|
51
|
+
"id": self.id,
|
|
52
|
+
"name": self.name,
|
|
53
|
+
"qualname": self.qualname,
|
|
54
|
+
"file_path": self.file_path,
|
|
55
|
+
"kind": self.kind,
|
|
56
|
+
"lineno": self.lineno,
|
|
57
|
+
"end_lineno": self.end_lineno,
|
|
58
|
+
"lines_count": max(1, self.end_lineno - self.lineno + 1),
|
|
59
|
+
"is_orphan": self.is_orphan,
|
|
60
|
+
"is_transitive": self.is_transitive,
|
|
61
|
+
"cluster_id": self.cluster_id,
|
|
62
|
+
"raw_reachability_confidence": round(self.raw_reachability_confidence, 4),
|
|
63
|
+
"semantic_classification": (
|
|
64
|
+
self.semantic_classification.value
|
|
65
|
+
if isinstance(self.semantic_classification, Enum)
|
|
66
|
+
else str(self.semantic_classification)
|
|
67
|
+
),
|
|
68
|
+
"calibrated_confidence": round(self.calibrated_confidence, 4),
|
|
69
|
+
"semantic_probabilities": {
|
|
70
|
+
k: round(v, 4) for k, v in self.semantic_probabilities.items()
|
|
71
|
+
},
|
|
72
|
+
"suppressed": self.suppressed,
|
|
73
|
+
"reason": self.reason,
|
|
74
|
+
"confidence": round(self.calibrated_confidence, 4),
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass
|
|
79
|
+
class DeadSymbol:
|
|
80
|
+
"""Represents an unreachable, orphan, or transitively dead symbol."""
|
|
81
|
+
id: str
|
|
82
|
+
name: str
|
|
83
|
+
qualname: str
|
|
84
|
+
file_path: str
|
|
85
|
+
kind: str
|
|
86
|
+
lineno: int
|
|
87
|
+
end_lineno: int
|
|
88
|
+
is_orphan: bool = True
|
|
89
|
+
is_transitive: bool = False
|
|
90
|
+
cluster_id: Optional[str] = None
|
|
91
|
+
confidence: float = 1.0
|
|
92
|
+
reason: str = "Unreferenced symbol with 0 incoming calls"
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def lines_count(self) -> int:
|
|
96
|
+
"""Count of lines occupied by this symbol."""
|
|
97
|
+
return max(1, self.end_lineno - self.lineno + 1)
|
|
98
|
+
|
|
99
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
100
|
+
"""Serialize dead symbol representation to dictionary."""
|
|
101
|
+
return {
|
|
102
|
+
"id": self.id,
|
|
103
|
+
"name": self.name,
|
|
104
|
+
"qualname": self.qualname,
|
|
105
|
+
"file_path": self.file_path,
|
|
106
|
+
"kind": self.kind,
|
|
107
|
+
"lineno": self.lineno,
|
|
108
|
+
"end_lineno": self.end_lineno,
|
|
109
|
+
"lines_count": self.lines_count,
|
|
110
|
+
"is_orphan": self.is_orphan,
|
|
111
|
+
"is_transitive": self.is_transitive,
|
|
112
|
+
"cluster_id": self.cluster_id,
|
|
113
|
+
"confidence": round(self.confidence, 4),
|
|
114
|
+
"reason": self.reason,
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dataclass
|
|
119
|
+
class DeadCodeReport:
|
|
120
|
+
"""Complete dead code and orphan symbol detection report."""
|
|
121
|
+
workspace_root: str
|
|
122
|
+
total_symbols_scanned: int
|
|
123
|
+
dead_symbols: List[Union[DeadSymbol, SemanticDeadSymbol]] = field(default_factory=list)
|
|
124
|
+
suppressed_symbols: List[SemanticDeadSymbol] = field(default_factory=list)
|
|
125
|
+
roots_count: int = 0
|
|
126
|
+
scanned_files_count: int = 0
|
|
127
|
+
latency_ms: float = 0.0
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def dead_symbols_count(self) -> int:
|
|
131
|
+
"""Total number of dead symbols found."""
|
|
132
|
+
return len(self.dead_symbols)
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def dead_lines_count(self) -> int:
|
|
136
|
+
"""Sum of lines across all detected dead symbols."""
|
|
137
|
+
return sum(s.lines_count for s in self.dead_symbols)
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def orphan_symbols(self) -> List[Union[DeadSymbol, SemanticDeadSymbol]]:
|
|
141
|
+
"""Direct orphan symbols (in-degree == 0)."""
|
|
142
|
+
return [s for s in self.dead_symbols if s.is_orphan]
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def transitive_symbols(self) -> List[Union[DeadSymbol, SemanticDeadSymbol]]:
|
|
146
|
+
"""Transitive dead cluster symbols."""
|
|
147
|
+
return [s for s in self.dead_symbols if s.is_transitive]
|
|
148
|
+
|
|
149
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
150
|
+
"""Convert report to dictionary."""
|
|
151
|
+
return {
|
|
152
|
+
"workspace_root": self.workspace_root,
|
|
153
|
+
"total_symbols_scanned": self.total_symbols_scanned,
|
|
154
|
+
"dead_symbols_count": self.dead_symbols_count,
|
|
155
|
+
"dead_lines_count": self.dead_lines_count,
|
|
156
|
+
"orphan_symbols_count": len(self.orphan_symbols),
|
|
157
|
+
"transitive_symbols_count": len(self.transitive_symbols),
|
|
158
|
+
"roots_count": self.roots_count,
|
|
159
|
+
"scanned_files_count": self.scanned_files_count,
|
|
160
|
+
"latency_ms": round(self.latency_ms, 2),
|
|
161
|
+
"dead_symbols": [s.to_dict() for s in self.dead_symbols],
|
|
162
|
+
"suppressed_symbols_count": len(self.suppressed_symbols),
|
|
163
|
+
"suppressed_symbols": [s.to_dict() for s in self.suppressed_symbols],
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
def format_table(self) -> str:
|
|
167
|
+
"""Format report into clean ASCII table."""
|
|
168
|
+
if not self.dead_symbols:
|
|
169
|
+
suppressed_note = f" ({len(self.suppressed_symbols)} public API symbols suppressed)" if self.suppressed_symbols else ""
|
|
170
|
+
return (
|
|
171
|
+
f"\033[92m✔ No dead code detected\033[0m{suppressed_note} across "
|
|
172
|
+
f"{self.total_symbols_scanned} symbols in {self.scanned_files_count} files "
|
|
173
|
+
f"({self.latency_ms:.2f} ms)."
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
headers = ["Symbol", "Kind", "Location", "Lines", "Classification", "Confidence"]
|
|
177
|
+
rows: List[List[str]] = []
|
|
178
|
+
for s in self.dead_symbols:
|
|
179
|
+
if hasattr(s, "semantic_classification"):
|
|
180
|
+
sem = s.semantic_classification
|
|
181
|
+
classification = sem.value if isinstance(sem, Enum) else str(sem)
|
|
182
|
+
else:
|
|
183
|
+
classification = "ORPHAN" if s.is_orphan else "TRANSITIVE"
|
|
184
|
+
loc = f"{s.file_path}:{s.lineno}"
|
|
185
|
+
rows.append([
|
|
186
|
+
s.qualname,
|
|
187
|
+
s.kind,
|
|
188
|
+
loc,
|
|
189
|
+
str(s.lines_count),
|
|
190
|
+
classification,
|
|
191
|
+
f"{s.confidence:.2f}",
|
|
192
|
+
])
|
|
193
|
+
|
|
194
|
+
col_widths = [len(h) for h in headers]
|
|
195
|
+
for row in rows:
|
|
196
|
+
for i, val in enumerate(row):
|
|
197
|
+
col_widths[i] = max(col_widths[i], len(val))
|
|
198
|
+
|
|
199
|
+
def make_separator(char: str = "-") -> str:
|
|
200
|
+
parts = [char * (w + 2) for w in col_widths]
|
|
201
|
+
return f"+{'+'.join(parts)}+"
|
|
202
|
+
|
|
203
|
+
lines = [
|
|
204
|
+
make_separator("-"),
|
|
205
|
+
"| " + " | ".join(h.ljust(col_widths[i]) for i, h in enumerate(headers)) + " |",
|
|
206
|
+
make_separator("="),
|
|
207
|
+
]
|
|
208
|
+
for row in rows:
|
|
209
|
+
color = "\033[91m" if row[4] in ("ORPHAN", "GENUINE_CRUFT") else "\033[93m"
|
|
210
|
+
color_reset = "\033[0m"
|
|
211
|
+
formatted_cells = [row[i].ljust(col_widths[i]) for i in range(len(row))]
|
|
212
|
+
lines.append(f"| {color}{' | '.join(formatted_cells)}{color_reset} |")
|
|
213
|
+
lines.append(make_separator("-"))
|
|
214
|
+
|
|
215
|
+
suppressed_str = f" ({len(self.suppressed_symbols)} public API symbols suppressed)" if self.suppressed_symbols else ""
|
|
216
|
+
summary = (
|
|
217
|
+
f"\033[91m✖ Found {self.dead_symbols_count} dead symbols\033[0m "
|
|
218
|
+
f"({self.dead_lines_count} lines){suppressed_str} across {self.scanned_files_count} files "
|
|
219
|
+
f"in {self.latency_ms:.2f} ms."
|
|
220
|
+
)
|
|
221
|
+
lines.append(summary)
|
|
222
|
+
return "\n".join(lines)
|
|
223
|
+
|
|
224
|
+
def format_text(self) -> str:
|
|
225
|
+
"""Format report into concise text lines."""
|
|
226
|
+
if not self.dead_symbols:
|
|
227
|
+
suppressed_note = f" ({len(self.suppressed_symbols)} public API symbols suppressed)" if self.suppressed_symbols else ""
|
|
228
|
+
return (
|
|
229
|
+
f"✔ No dead code detected{suppressed_note} across {self.total_symbols_scanned} symbols "
|
|
230
|
+
f"in {self.scanned_files_count} files ({self.latency_ms:.2f} ms)."
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
suppressed_header = f", {len(self.suppressed_symbols)} suppressed" if self.suppressed_symbols else ""
|
|
234
|
+
lines = [
|
|
235
|
+
f"Dead Code Report ({self.dead_symbols_count} dead symbols, {self.dead_lines_count} lines{suppressed_header}):",
|
|
236
|
+
"--------------------------------------------------------------------------------",
|
|
237
|
+
]
|
|
238
|
+
for s in self.dead_symbols:
|
|
239
|
+
if hasattr(s, "semantic_classification"):
|
|
240
|
+
sem = s.semantic_classification
|
|
241
|
+
tag = sem.value if isinstance(sem, Enum) else str(sem)
|
|
242
|
+
else:
|
|
243
|
+
tag = "ORPHAN" if s.is_orphan else "TRANSITIVE"
|
|
244
|
+
lines.append(
|
|
245
|
+
f" • {s.file_path}:{s.lineno} {s.qualname} ({s.kind}, {s.lines_count} lines) [{tag}]"
|
|
246
|
+
)
|
|
247
|
+
lines.append(f" Reason: {s.reason}")
|
|
248
|
+
if self.suppressed_symbols:
|
|
249
|
+
lines.append(f" Note: {len(self.suppressed_symbols)} public API symbols were suppressed from this report.")
|
|
250
|
+
lines.append("--------------------------------------------------------------------------------")
|
|
251
|
+
lines.append(
|
|
252
|
+
f"Scan completed in {self.latency_ms:.2f} ms (Roots: {self.roots_count}, "
|
|
253
|
+
f"Total Symbols: {self.total_symbols_scanned})."
|
|
254
|
+
)
|
|
255
|
+
return "\n".join(lines)
|