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.
Files changed (40) hide show
  1. code_oracle/__init__.py +30 -0
  2. code_oracle/cli.py +795 -0
  3. code_oracle/config.py +145 -0
  4. code_oracle/dataset.py +5325 -0
  5. code_oracle/dead_code/__init__.py +32 -0
  6. code_oracle/dead_code/detector.py +379 -0
  7. code_oracle/dead_code/entrypoints.py +333 -0
  8. code_oracle/dead_code/models.py +255 -0
  9. code_oracle/dead_code/semantics.py +416 -0
  10. code_oracle/decision.py +906 -0
  11. code_oracle/engine.py +430 -0
  12. code_oracle/export_onnx.py +436 -0
  13. code_oracle/hook.py +531 -0
  14. code_oracle/indexer.py +894 -0
  15. code_oracle/languages/__init__.py +114 -0
  16. code_oracle/languages/common.py +127 -0
  17. code_oracle/languages/go.py +395 -0
  18. code_oracle/languages/python.py +336 -0
  19. code_oracle/languages/rust.py +474 -0
  20. code_oracle/languages/typescript.py +775 -0
  21. code_oracle/linearizer.py +166 -0
  22. code_oracle/locator.py +301 -0
  23. code_oracle/models.py +237 -0
  24. code_oracle/perf_lint/__init__.py +38 -0
  25. code_oracle/perf_lint/engine.py +234 -0
  26. code_oracle/perf_lint/models.py +229 -0
  27. code_oracle/perf_lint/rules/__init__.py +31 -0
  28. code_oracle/perf_lint/rules/async_blocking.py +143 -0
  29. code_oracle/perf_lint/rules/n_plus_one.py +232 -0
  30. code_oracle/perf_lint/rules/nested_loops.py +137 -0
  31. code_oracle/perf_lint/rules/unclosed_res.py +494 -0
  32. code_oracle/perf_lint/visitor.py +299 -0
  33. code_oracle/server.py +184 -0
  34. code_oracle/slicer.py +225 -0
  35. code_oracle/symbolic.py +459 -0
  36. code_oracle-0.1.0.dist-info/METADATA +225 -0
  37. code_oracle-0.1.0.dist-info/RECORD +40 -0
  38. code_oracle-0.1.0.dist-info/WHEEL +4 -0
  39. code_oracle-0.1.0.dist-info/entry_points.txt +2 -0
  40. 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)