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,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
+ )