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,32 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dead Code and Orphan Symbol Detection Engine.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from code_oracle.dead_code.detector import DeadCodeDetector, detect_dead_code
|
|
6
|
+
from code_oracle.dead_code.entrypoints import EntrypointDetector, is_entrypoint
|
|
7
|
+
from code_oracle.dead_code.models import (
|
|
8
|
+
DeadCodeReport,
|
|
9
|
+
DeadSymbol,
|
|
10
|
+
SemanticClassification,
|
|
11
|
+
SemanticDeadSymbol,
|
|
12
|
+
)
|
|
13
|
+
from code_oracle.dead_code.semantics import (
|
|
14
|
+
DeadCodeSemanticsClassifier,
|
|
15
|
+
DeadCodeSemanticsModel,
|
|
16
|
+
vectorize_symbol,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"DeadCodeDetector",
|
|
21
|
+
"detect_dead_code",
|
|
22
|
+
"DeadCodeReport",
|
|
23
|
+
"DeadSymbol",
|
|
24
|
+
"SemanticClassification",
|
|
25
|
+
"SemanticDeadSymbol",
|
|
26
|
+
"DeadCodeSemanticsClassifier",
|
|
27
|
+
"DeadCodeSemanticsModel",
|
|
28
|
+
"vectorize_symbol",
|
|
29
|
+
"EntrypointDetector",
|
|
30
|
+
"is_entrypoint",
|
|
31
|
+
]
|
|
32
|
+
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Graph Reachability Dead Code Engine.
|
|
3
|
+
Traverses the workspace symbol reference graph from entrypoint roots,
|
|
4
|
+
isolating direct orphans (in-degree == 0) and transitive dead clusters.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from collections import deque
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import time
|
|
10
|
+
from typing import Dict, List, Optional, Set, Tuple
|
|
11
|
+
|
|
12
|
+
from code_oracle.dead_code.entrypoints import EntrypointDetector
|
|
13
|
+
from code_oracle.dead_code.models import DeadCodeReport, DeadSymbol, SemanticDeadSymbol
|
|
14
|
+
from code_oracle.dead_code.semantics import DeadCodeSemanticsClassifier
|
|
15
|
+
from code_oracle.indexer import WorkspaceIndexer
|
|
16
|
+
from code_oracle.models import Symbol
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def is_symbol_exported(symbol: Symbol, detector: EntrypointDetector) -> bool:
|
|
20
|
+
"""
|
|
21
|
+
Determine if a symbol is exported / public in its host language.
|
|
22
|
+
Prioritizes AST-extracted symbol.is_exported and visibility metadata.
|
|
23
|
+
"""
|
|
24
|
+
# Local nested inner functions are never exported
|
|
25
|
+
if "." in symbol.qualname and not symbol.is_method:
|
|
26
|
+
return False
|
|
27
|
+
|
|
28
|
+
if getattr(symbol, "is_exported", False):
|
|
29
|
+
return True
|
|
30
|
+
|
|
31
|
+
if getattr(symbol, "visibility", None) == "public":
|
|
32
|
+
return True
|
|
33
|
+
|
|
34
|
+
ext = Path(symbol.file_path).suffix.lower()
|
|
35
|
+
|
|
36
|
+
if ext == ".py":
|
|
37
|
+
return not symbol.name.startswith("_")
|
|
38
|
+
|
|
39
|
+
if ext == ".go":
|
|
40
|
+
return bool(symbol.name and symbol.name[0].isupper())
|
|
41
|
+
|
|
42
|
+
# For Rust and TypeScript, inspect source code lines
|
|
43
|
+
lines = detector.get_source_lines(symbol.file_path)
|
|
44
|
+
if lines and 1 <= symbol.lineno <= len(lines):
|
|
45
|
+
line_content = lines[symbol.lineno - 1].strip()
|
|
46
|
+
if ext == ".rs":
|
|
47
|
+
return line_content.startswith("pub ") or "pub fn " in line_content or "pub struct " in line_content
|
|
48
|
+
if ext in (".ts", ".tsx", ".js", ".jsx", ".mjs"):
|
|
49
|
+
return line_content.startswith("export ") or "export default" in line_content
|
|
50
|
+
|
|
51
|
+
# Fallback to signature inspection
|
|
52
|
+
sig = symbol.signature.strip()
|
|
53
|
+
if ext == ".rs":
|
|
54
|
+
return sig.startswith("pub ")
|
|
55
|
+
if ext in (".ts", ".tsx", ".js", ".jsx", ".mjs"):
|
|
56
|
+
return sig.startswith("export ")
|
|
57
|
+
|
|
58
|
+
return True
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class DeadCodeDetector:
|
|
62
|
+
"""
|
|
63
|
+
Reachability engine that identifies unreachable and orphan symbols.
|
|
64
|
+
Operates over the WorkspaceIndexer symbol topology.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def __init__(
|
|
68
|
+
self,
|
|
69
|
+
workspace_root: Optional[Path] = None,
|
|
70
|
+
indexer: Optional[WorkspaceIndexer] = None,
|
|
71
|
+
):
|
|
72
|
+
self.workspace_root = (workspace_root or Path.cwd()).resolve()
|
|
73
|
+
self.indexer = indexer or WorkspaceIndexer(workspace_root=self.workspace_root)
|
|
74
|
+
self.entrypoint_detector = EntrypointDetector(workspace_root=self.workspace_root)
|
|
75
|
+
|
|
76
|
+
def _matches_paths(self, file_path: str, filter_paths: List[str]) -> bool:
|
|
77
|
+
"""Check if symbol file path matches any requested filter path."""
|
|
78
|
+
norm_file = file_path.replace("\\", "/")
|
|
79
|
+
if norm_file.startswith("./"):
|
|
80
|
+
norm_file = norm_file[2:]
|
|
81
|
+
|
|
82
|
+
for p in filter_paths:
|
|
83
|
+
p_str = str(p).replace("\\", "/")
|
|
84
|
+
if p_str.startswith("./"):
|
|
85
|
+
p_str = p_str[2:]
|
|
86
|
+
|
|
87
|
+
p_obj = Path(p)
|
|
88
|
+
if p_obj.is_absolute():
|
|
89
|
+
try:
|
|
90
|
+
norm_p = str(p_obj.resolve().relative_to(self.workspace_root)).replace("\\", "/")
|
|
91
|
+
except ValueError:
|
|
92
|
+
norm_p = p_str
|
|
93
|
+
else:
|
|
94
|
+
norm_p = p_str
|
|
95
|
+
|
|
96
|
+
if norm_file == norm_p:
|
|
97
|
+
return True
|
|
98
|
+
if norm_file.startswith(norm_p.rstrip("/") + "/"):
|
|
99
|
+
return True
|
|
100
|
+
|
|
101
|
+
return False
|
|
102
|
+
|
|
103
|
+
def _find_cluster_root(
|
|
104
|
+
self,
|
|
105
|
+
symbol_id: str,
|
|
106
|
+
reverse_graph: Dict[str, Set[str]],
|
|
107
|
+
dead_ids: Set[str],
|
|
108
|
+
) -> str:
|
|
109
|
+
"""
|
|
110
|
+
Trace back callers within dead candidates to locate the root orphan or cycle anchor.
|
|
111
|
+
"""
|
|
112
|
+
visited = set()
|
|
113
|
+
queue = deque([symbol_id])
|
|
114
|
+
cluster_orphans = []
|
|
115
|
+
|
|
116
|
+
while queue:
|
|
117
|
+
curr = queue.popleft()
|
|
118
|
+
if curr in visited:
|
|
119
|
+
continue
|
|
120
|
+
visited.add(curr)
|
|
121
|
+
|
|
122
|
+
dead_callers = [c for c in reverse_graph.get(curr, set()) if c in dead_ids and c != curr]
|
|
123
|
+
if not dead_callers:
|
|
124
|
+
cluster_orphans.append(curr)
|
|
125
|
+
else:
|
|
126
|
+
for c in dead_callers:
|
|
127
|
+
if c not in visited:
|
|
128
|
+
queue.append(c)
|
|
129
|
+
|
|
130
|
+
if cluster_orphans:
|
|
131
|
+
# Pick first/lowest orphan as root
|
|
132
|
+
return sorted(cluster_orphans)[0]
|
|
133
|
+
# In a closed mutual cycle, pick the lexicographically lowest symbol ID
|
|
134
|
+
return sorted(visited)[0] if visited else symbol_id
|
|
135
|
+
|
|
136
|
+
def detect(
|
|
137
|
+
self,
|
|
138
|
+
paths: Optional[List[str]] = None,
|
|
139
|
+
min_lines: int = 0,
|
|
140
|
+
include_unexported: bool = False,
|
|
141
|
+
semantic: bool = False,
|
|
142
|
+
suppress_api: bool = False,
|
|
143
|
+
neural_semantics: Optional[bool] = None,
|
|
144
|
+
suppress_public_api: Optional[bool] = None,
|
|
145
|
+
) -> DeadCodeReport:
|
|
146
|
+
"""
|
|
147
|
+
Execute full workspace reachability analysis and return dead code report.
|
|
148
|
+
"""
|
|
149
|
+
if neural_semantics is not None:
|
|
150
|
+
semantic = neural_semantics
|
|
151
|
+
if suppress_public_api is not None:
|
|
152
|
+
suppress_api = suppress_public_api
|
|
153
|
+
if suppress_api:
|
|
154
|
+
semantic = True
|
|
155
|
+
|
|
156
|
+
start_time = time.perf_counter()
|
|
157
|
+
|
|
158
|
+
# Ensure index is updated
|
|
159
|
+
self.indexer.scan_workspace()
|
|
160
|
+
|
|
161
|
+
all_symbols = list(self.indexer._definitions.values())
|
|
162
|
+
forward_graph: Dict[str, Set[str]] = {s.id: set() for s in all_symbols}
|
|
163
|
+
reverse_graph: Dict[str, Set[str]] = {s.id: set() for s in all_symbols}
|
|
164
|
+
|
|
165
|
+
roots: Set[str] = set()
|
|
166
|
+
|
|
167
|
+
# 1. Identify entrypoint roots
|
|
168
|
+
for sym in all_symbols:
|
|
169
|
+
if self.entrypoint_detector.is_entrypoint(sym):
|
|
170
|
+
roots.add(sym.id)
|
|
171
|
+
|
|
172
|
+
# 2. Expand roots from public export files (__init__.py, index.ts, mod.rs)
|
|
173
|
+
for rel_file, imports in self.indexer._file_imports.items():
|
|
174
|
+
if self.entrypoint_detector.is_root_export(
|
|
175
|
+
Symbol(name="", qualname="", file_path=rel_file, kind="module", lineno=1, end_lineno=1)
|
|
176
|
+
):
|
|
177
|
+
for imp in imports:
|
|
178
|
+
target_file = self.indexer.resolve_import_to_file(imp, rel_file)
|
|
179
|
+
if target_file:
|
|
180
|
+
if imp.name == "*":
|
|
181
|
+
for file_sym in self.indexer.get_file_symbols(target_file):
|
|
182
|
+
roots.add(file_sym.id)
|
|
183
|
+
else:
|
|
184
|
+
target_id = f"{target_file}::{imp.name}"
|
|
185
|
+
if target_id in self.indexer._definitions:
|
|
186
|
+
roots.add(target_id)
|
|
187
|
+
else:
|
|
188
|
+
for file_sym in self.indexer.get_file_symbols(target_file):
|
|
189
|
+
if file_sym.name == imp.name:
|
|
190
|
+
roots.add(file_sym.id)
|
|
191
|
+
|
|
192
|
+
# 3. Build directed reference edges using WorkspaceIndex definitions, callers, and importers
|
|
193
|
+
for sym in all_symbols:
|
|
194
|
+
# Call edges
|
|
195
|
+
for call in sym.calls:
|
|
196
|
+
callee_sym = self.indexer.resolve_callee(call, sym)
|
|
197
|
+
if callee_sym and callee_sym.id in self.indexer._definitions:
|
|
198
|
+
forward_graph[sym.id].add(callee_sym.id)
|
|
199
|
+
reverse_graph[callee_sym.id].add(sym.id)
|
|
200
|
+
|
|
201
|
+
# For polymorphic / dynamic method calls (e.g. obj.method()),
|
|
202
|
+
# link candidate methods with the same name across definitions
|
|
203
|
+
if "." in call.callee:
|
|
204
|
+
method_name = call.callee.split(".")[-1].split("::")[-1]
|
|
205
|
+
for candidate in self.indexer.get_symbols_by_name(method_name):
|
|
206
|
+
if candidate.kind in ("method", "function") and candidate.id in self.indexer._definitions:
|
|
207
|
+
forward_graph[sym.id].add(candidate.id)
|
|
208
|
+
reverse_graph[candidate.id].add(sym.id)
|
|
209
|
+
|
|
210
|
+
# Class inheritance and constructor edges
|
|
211
|
+
if sym.kind == "class":
|
|
212
|
+
init_sym = self.indexer.resolve_class_init(sym)
|
|
213
|
+
if init_sym and init_sym.id in self.indexer._definitions:
|
|
214
|
+
forward_graph[sym.id].add(init_sym.id)
|
|
215
|
+
reverse_graph[init_sym.id].add(sym.id)
|
|
216
|
+
|
|
217
|
+
for base_name in getattr(sym, "bases", []):
|
|
218
|
+
base_sym = self.indexer.get_definition(base_name)
|
|
219
|
+
if base_sym and base_sym.id in self.indexer._definitions:
|
|
220
|
+
forward_graph[sym.id].add(base_sym.id)
|
|
221
|
+
reverse_graph[base_sym.id].add(sym.id)
|
|
222
|
+
|
|
223
|
+
# Incorporate indexer._callers into graph topology
|
|
224
|
+
for target_name, callers in self.indexer._callers.items():
|
|
225
|
+
target_syms = self.indexer.get_symbols_by_name(target_name)
|
|
226
|
+
for c_ref in callers:
|
|
227
|
+
if c_ref.caller and c_ref.caller in self.indexer._definitions:
|
|
228
|
+
for t_sym in target_syms:
|
|
229
|
+
if t_sym.id in self.indexer._definitions:
|
|
230
|
+
forward_graph[c_ref.caller].add(t_sym.id)
|
|
231
|
+
reverse_graph[t_sym.id].add(c_ref.caller)
|
|
232
|
+
|
|
233
|
+
# Incorporate indexer._importers for root export propagation
|
|
234
|
+
for target_name, importers in self.indexer._importers.items():
|
|
235
|
+
target_syms = self.indexer.get_symbols_by_name(target_name)
|
|
236
|
+
for imp_ref in importers:
|
|
237
|
+
if self.entrypoint_detector.is_root_export(
|
|
238
|
+
Symbol(name="", qualname="", file_path=imp_ref.file_path, kind="module", lineno=1, end_lineno=1)
|
|
239
|
+
):
|
|
240
|
+
for t_sym in target_syms:
|
|
241
|
+
roots.add(t_sym.id)
|
|
242
|
+
|
|
243
|
+
# 4. Forward reachability traversal (BFS from roots)
|
|
244
|
+
reachable: Set[str] = set(roots)
|
|
245
|
+
queue = deque(roots)
|
|
246
|
+
|
|
247
|
+
while queue:
|
|
248
|
+
curr_id = queue.popleft()
|
|
249
|
+
for callee_id in forward_graph.get(curr_id, ()):
|
|
250
|
+
if callee_id not in reachable:
|
|
251
|
+
reachable.add(callee_id)
|
|
252
|
+
queue.append(callee_id)
|
|
253
|
+
|
|
254
|
+
# 5. Extract unreachable symbols (ignoring module blocks)
|
|
255
|
+
dead_candidates: List[Symbol] = []
|
|
256
|
+
dead_candidate_ids: Set[str] = set()
|
|
257
|
+
|
|
258
|
+
for sym in all_symbols:
|
|
259
|
+
if sym.kind == "module" or sym.name == "<module>":
|
|
260
|
+
continue
|
|
261
|
+
|
|
262
|
+
if sym.id in reachable:
|
|
263
|
+
continue
|
|
264
|
+
|
|
265
|
+
# Exemption: dunder/magic methods and properties on an ALIVE class are not dead
|
|
266
|
+
if (
|
|
267
|
+
self.entrypoint_detector.is_magic_method(sym)
|
|
268
|
+
or self.entrypoint_detector.is_property_method(sym)
|
|
269
|
+
) and sym.is_method:
|
|
270
|
+
parent_qualname = sym.qualname.rsplit(".", 1)[0] if "." in sym.qualname else ""
|
|
271
|
+
class_id = f"{sym.file_path}::{parent_qualname}"
|
|
272
|
+
if class_id in reachable:
|
|
273
|
+
continue
|
|
274
|
+
|
|
275
|
+
dead_candidates.append(sym)
|
|
276
|
+
dead_candidate_ids.add(sym.id)
|
|
277
|
+
|
|
278
|
+
# 6. Classify direct orphans and transitive dead clusters
|
|
279
|
+
dead_symbols: List[DeadSymbol] = []
|
|
280
|
+
candidate_pairs: List[Tuple[Symbol, DeadSymbol]] = []
|
|
281
|
+
|
|
282
|
+
for sym in dead_candidates:
|
|
283
|
+
# Check visibility
|
|
284
|
+
is_exported = is_symbol_exported(sym, self.entrypoint_detector)
|
|
285
|
+
if not include_unexported and not is_exported:
|
|
286
|
+
continue
|
|
287
|
+
|
|
288
|
+
# Check min lines
|
|
289
|
+
lines_count = max(1, sym.end_lineno - sym.lineno + 1)
|
|
290
|
+
if min_lines > 0 and lines_count < min_lines:
|
|
291
|
+
continue
|
|
292
|
+
|
|
293
|
+
# Check paths filter
|
|
294
|
+
if paths and not self._matches_paths(sym.file_path, paths):
|
|
295
|
+
continue
|
|
296
|
+
|
|
297
|
+
# Classify orphan vs transitive
|
|
298
|
+
live_callers = [c for c in reverse_graph.get(sym.id, set()) if c in reachable]
|
|
299
|
+
dead_callers = [c for c in reverse_graph.get(sym.id, set()) if c in dead_candidate_ids and c != sym.id]
|
|
300
|
+
|
|
301
|
+
if not dead_callers and not live_callers:
|
|
302
|
+
is_orphan = True
|
|
303
|
+
is_transitive = False
|
|
304
|
+
cluster_id = None
|
|
305
|
+
reason = "Unreferenced symbol with 0 incoming calls"
|
|
306
|
+
else:
|
|
307
|
+
is_orphan = False
|
|
308
|
+
is_transitive = True
|
|
309
|
+
cluster_id = self._find_cluster_root(sym.id, reverse_graph, dead_candidate_ids)
|
|
310
|
+
if cluster_id == sym.id:
|
|
311
|
+
reason = "Dead cycle: mutual calls with no external entrypoint"
|
|
312
|
+
else:
|
|
313
|
+
reason = f"Transitive dead symbol: only called by unreachable symbols (cluster: {cluster_id})"
|
|
314
|
+
|
|
315
|
+
d_sym = DeadSymbol(
|
|
316
|
+
id=sym.id,
|
|
317
|
+
name=sym.name,
|
|
318
|
+
qualname=sym.qualname,
|
|
319
|
+
file_path=sym.file_path,
|
|
320
|
+
kind=sym.kind,
|
|
321
|
+
lineno=sym.lineno,
|
|
322
|
+
end_lineno=sym.end_lineno,
|
|
323
|
+
is_orphan=is_orphan,
|
|
324
|
+
is_transitive=is_transitive,
|
|
325
|
+
cluster_id=cluster_id,
|
|
326
|
+
confidence=1.0,
|
|
327
|
+
reason=reason,
|
|
328
|
+
)
|
|
329
|
+
dead_symbols.append(d_sym)
|
|
330
|
+
candidate_pairs.append((sym, d_sym))
|
|
331
|
+
|
|
332
|
+
suppressed_symbols: List[SemanticDeadSymbol] = []
|
|
333
|
+
if semantic:
|
|
334
|
+
classifier = DeadCodeSemanticsClassifier()
|
|
335
|
+
active_symbols, suppressed_symbols = classifier.classify_candidates(
|
|
336
|
+
candidate_pairs,
|
|
337
|
+
suppress_public_api=suppress_api,
|
|
338
|
+
)
|
|
339
|
+
dead_symbols = active_symbols
|
|
340
|
+
|
|
341
|
+
elapsed_ms = (time.perf_counter() - start_time) * 1000.0
|
|
342
|
+
|
|
343
|
+
# Sort dead symbols by file path and line number
|
|
344
|
+
dead_symbols.sort(key=lambda s: (s.file_path, s.lineno))
|
|
345
|
+
suppressed_symbols.sort(key=lambda s: (s.file_path, s.lineno))
|
|
346
|
+
|
|
347
|
+
return DeadCodeReport(
|
|
348
|
+
workspace_root=str(self.workspace_root),
|
|
349
|
+
total_symbols_scanned=len(all_symbols),
|
|
350
|
+
dead_symbols=dead_symbols,
|
|
351
|
+
suppressed_symbols=suppressed_symbols,
|
|
352
|
+
roots_count=len(roots),
|
|
353
|
+
scanned_files_count=len(self.indexer._file_cache),
|
|
354
|
+
latency_ms=elapsed_ms,
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def detect_dead_code(
|
|
359
|
+
workspace_root: Optional[Path] = None,
|
|
360
|
+
indexer: Optional[WorkspaceIndexer] = None,
|
|
361
|
+
paths: Optional[List[str]] = None,
|
|
362
|
+
min_lines: int = 0,
|
|
363
|
+
include_unexported: bool = False,
|
|
364
|
+
semantic: bool = False,
|
|
365
|
+
suppress_api: bool = False,
|
|
366
|
+
neural_semantics: Optional[bool] = None,
|
|
367
|
+
suppress_public_api: Optional[bool] = None,
|
|
368
|
+
) -> DeadCodeReport:
|
|
369
|
+
"""Top-level convenience function to detect dead code."""
|
|
370
|
+
detector = DeadCodeDetector(workspace_root=workspace_root, indexer=indexer)
|
|
371
|
+
return detector.detect(
|
|
372
|
+
paths=paths,
|
|
373
|
+
min_lines=min_lines,
|
|
374
|
+
include_unexported=include_unexported,
|
|
375
|
+
semantic=semantic,
|
|
376
|
+
suppress_api=suppress_api,
|
|
377
|
+
neural_semantics=neural_semantics,
|
|
378
|
+
suppress_public_api=suppress_public_api,
|
|
379
|
+
)
|