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,336 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Python AST extractor using Python's native ast module.
|
|
3
|
+
Preserves 100% fidelity and backward compatibility for Python verification.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import ast
|
|
7
|
+
from typing import List, Optional, Set
|
|
8
|
+
|
|
9
|
+
from code_oracle.models import CallReference, ImportReference, Parameter, Symbol
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _extract_calls_from_node(node: ast.AST, caller_id: Optional[str] = None) -> List[CallReference]:
|
|
13
|
+
"""Extract all function/method calls inside a Python AST node."""
|
|
14
|
+
calls: List[CallReference] = []
|
|
15
|
+
for child in ast.walk(node):
|
|
16
|
+
if isinstance(child, ast.Call):
|
|
17
|
+
try:
|
|
18
|
+
callee_name = ast.unparse(child.func)
|
|
19
|
+
except Exception:
|
|
20
|
+
callee_name = "<unknown>"
|
|
21
|
+
kwargs = [kw.arg for kw in child.keywords if kw.arg is not None]
|
|
22
|
+
has_vararg = any(isinstance(a, ast.Starred) for a in child.args)
|
|
23
|
+
has_kwarg = any(kw.arg is None for kw in child.keywords)
|
|
24
|
+
calls.append(
|
|
25
|
+
CallReference(
|
|
26
|
+
callee=callee_name,
|
|
27
|
+
args_count=len(child.args),
|
|
28
|
+
kwargs=kwargs,
|
|
29
|
+
lineno=getattr(child, "lineno", 0),
|
|
30
|
+
caller=caller_id,
|
|
31
|
+
has_vararg=has_vararg,
|
|
32
|
+
has_kwarg=has_kwarg,
|
|
33
|
+
)
|
|
34
|
+
)
|
|
35
|
+
return calls
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def extract_python_imports(source: str, file_path: str = "") -> List[ImportReference]:
|
|
39
|
+
"""Extract all import statements from Python source."""
|
|
40
|
+
if not source.strip():
|
|
41
|
+
return []
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
tree = ast.parse(source)
|
|
45
|
+
except SyntaxError:
|
|
46
|
+
return []
|
|
47
|
+
|
|
48
|
+
imports: List[ImportReference] = []
|
|
49
|
+
for node in ast.walk(tree):
|
|
50
|
+
if isinstance(node, ast.Import):
|
|
51
|
+
for alias in node.names:
|
|
52
|
+
imports.append(
|
|
53
|
+
ImportReference(
|
|
54
|
+
module=None,
|
|
55
|
+
name=alias.name,
|
|
56
|
+
asname=alias.asname,
|
|
57
|
+
lineno=getattr(node, "lineno", 0),
|
|
58
|
+
file_path=file_path,
|
|
59
|
+
level=0,
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
elif isinstance(node, ast.ImportFrom):
|
|
63
|
+
for alias in node.names:
|
|
64
|
+
imports.append(
|
|
65
|
+
ImportReference(
|
|
66
|
+
module=node.module,
|
|
67
|
+
name=alias.name,
|
|
68
|
+
asname=alias.asname,
|
|
69
|
+
lineno=getattr(node, "lineno", 0),
|
|
70
|
+
file_path=file_path,
|
|
71
|
+
level=node.level,
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
return imports
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def extract_python_symbols(source: str, file_path: str = "") -> List[Symbol]:
|
|
78
|
+
"""Parse Python source into AST and extract symbol entities with detailed metadata."""
|
|
79
|
+
if not source.strip():
|
|
80
|
+
return []
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
tree = ast.parse(source)
|
|
84
|
+
except SyntaxError:
|
|
85
|
+
return []
|
|
86
|
+
|
|
87
|
+
symbols: List[Symbol] = []
|
|
88
|
+
|
|
89
|
+
# Check for module-level __all__
|
|
90
|
+
all_names: Optional[Set[str]] = None
|
|
91
|
+
|
|
92
|
+
def _extract_all_elts(val_node: ast.AST) -> Set[str]:
|
|
93
|
+
if isinstance(val_node, (ast.List, ast.Tuple, ast.Set)):
|
|
94
|
+
return {
|
|
95
|
+
elt.value for elt in val_node.elts
|
|
96
|
+
if isinstance(elt, ast.Constant) and isinstance(elt.value, str)
|
|
97
|
+
}
|
|
98
|
+
return set()
|
|
99
|
+
|
|
100
|
+
for stmt in tree.body:
|
|
101
|
+
if isinstance(stmt, ast.Assign):
|
|
102
|
+
for target in stmt.targets:
|
|
103
|
+
if isinstance(target, ast.Name) and target.id == "__all__":
|
|
104
|
+
elts = _extract_all_elts(stmt.value)
|
|
105
|
+
all_names = elts if all_names is None else (all_names | elts)
|
|
106
|
+
elif isinstance(stmt, ast.AnnAssign):
|
|
107
|
+
if isinstance(stmt.target, ast.Name) and stmt.target.id == "__all__" and stmt.value:
|
|
108
|
+
elts = _extract_all_elts(stmt.value)
|
|
109
|
+
all_names = elts if all_names is None else (all_names | elts)
|
|
110
|
+
elif isinstance(stmt, ast.AugAssign):
|
|
111
|
+
if isinstance(stmt.target, ast.Name) and stmt.target.id == "__all__":
|
|
112
|
+
elts = _extract_all_elts(stmt.value)
|
|
113
|
+
all_names = elts if all_names is None else (all_names | elts)
|
|
114
|
+
|
|
115
|
+
def process_body(nodes: List[ast.stmt], parent_qualname: Optional[str] = None, is_parent_class: bool = False):
|
|
116
|
+
for node in nodes:
|
|
117
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
118
|
+
qualname = f"{parent_qualname}.{node.name}" if parent_qualname else node.name
|
|
119
|
+
is_static = any(
|
|
120
|
+
(isinstance(d, ast.Name) and d.id == "staticmethod")
|
|
121
|
+
or (isinstance(d, ast.Attribute) and d.attr == "staticmethod")
|
|
122
|
+
for d in node.decorator_list
|
|
123
|
+
)
|
|
124
|
+
is_method = is_parent_class and not is_static
|
|
125
|
+
kind = "method" if is_parent_class else ("async_function" if isinstance(node, ast.AsyncFunctionDef) else "function")
|
|
126
|
+
|
|
127
|
+
# Parameters extraction
|
|
128
|
+
args_node = node.args
|
|
129
|
+
posonly_names = {a.arg for a in args_node.posonlyargs}
|
|
130
|
+
pos_args_nodes = args_node.posonlyargs + args_node.args
|
|
131
|
+
num_defaults = len(args_node.defaults)
|
|
132
|
+
defaults_offset = len(pos_args_nodes) - num_defaults
|
|
133
|
+
|
|
134
|
+
params: List[Parameter] = []
|
|
135
|
+
for i, arg in enumerate(pos_args_nodes):
|
|
136
|
+
default_str = None
|
|
137
|
+
has_default = False
|
|
138
|
+
if i >= defaults_offset:
|
|
139
|
+
default_node = args_node.defaults[i - defaults_offset]
|
|
140
|
+
default_str = ast.unparse(default_node)
|
|
141
|
+
has_default = True
|
|
142
|
+
annotation_str = ast.unparse(arg.annotation) if arg.annotation else None
|
|
143
|
+
params.append(
|
|
144
|
+
Parameter(
|
|
145
|
+
name=arg.arg,
|
|
146
|
+
annotation=annotation_str,
|
|
147
|
+
default=default_str,
|
|
148
|
+
has_default=has_default,
|
|
149
|
+
is_posonly=(arg.arg in posonly_names),
|
|
150
|
+
)
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Keyword-only parameters
|
|
154
|
+
for i, kwarg in enumerate(args_node.kwonlyargs):
|
|
155
|
+
kw_default = args_node.kw_defaults[i]
|
|
156
|
+
has_kw_default = kw_default is not None
|
|
157
|
+
kw_default_str = ast.unparse(kw_default) if has_kw_default else None
|
|
158
|
+
annotation_str = ast.unparse(kwarg.annotation) if kwarg.annotation else None
|
|
159
|
+
params.append(
|
|
160
|
+
Parameter(
|
|
161
|
+
name=kwarg.arg,
|
|
162
|
+
annotation=annotation_str,
|
|
163
|
+
default=kw_default_str,
|
|
164
|
+
has_default=has_kw_default,
|
|
165
|
+
is_kwonly=True,
|
|
166
|
+
)
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# Varargs & kwargs
|
|
170
|
+
if args_node.vararg:
|
|
171
|
+
params.append(Parameter(name=args_node.vararg.arg, is_vararg=True))
|
|
172
|
+
if args_node.kwarg:
|
|
173
|
+
params.append(Parameter(name=args_node.kwarg.arg, is_kwarg=True))
|
|
174
|
+
|
|
175
|
+
min_args = len(pos_args_nodes) - num_defaults
|
|
176
|
+
max_args = None if args_node.vararg is not None else len(pos_args_nodes)
|
|
177
|
+
|
|
178
|
+
accepted_kwargs = None if args_node.kwarg is not None else {
|
|
179
|
+
p.name for p in params if not p.is_vararg and not p.is_kwarg and not p.is_posonly
|
|
180
|
+
}
|
|
181
|
+
required_kwargs = {
|
|
182
|
+
kwarg.arg for i, kwarg in enumerate(args_node.kwonlyargs)
|
|
183
|
+
if args_node.kw_defaults[i] is None
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
# Return annotation
|
|
187
|
+
return_type = ast.unparse(node.returns) if node.returns else None
|
|
188
|
+
|
|
189
|
+
# Signature representation
|
|
190
|
+
sig_params = []
|
|
191
|
+
for p in params:
|
|
192
|
+
p_str = p.name
|
|
193
|
+
if p.is_vararg:
|
|
194
|
+
p_str = f"*{p.name}"
|
|
195
|
+
elif p.is_kwarg:
|
|
196
|
+
p_str = f"**{p.name}"
|
|
197
|
+
if p.annotation:
|
|
198
|
+
p_str += f": {p.annotation}"
|
|
199
|
+
if p.has_default and p.default is not None:
|
|
200
|
+
p_str += f" = {p.default}"
|
|
201
|
+
sig_params.append(p_str)
|
|
202
|
+
ret_suffix = f" -> {return_type}" if return_type else ""
|
|
203
|
+
prefix = "async def" if isinstance(node, ast.AsyncFunctionDef) else "def"
|
|
204
|
+
signature = f"{prefix} {node.name}({', '.join(sig_params)}){ret_suffix}"
|
|
205
|
+
|
|
206
|
+
sym_id = f"{file_path}::{qualname}"
|
|
207
|
+
calls = _extract_calls_from_node(node, caller_id=sym_id)
|
|
208
|
+
|
|
209
|
+
# Docstring and export visibility
|
|
210
|
+
docstring = ast.get_docstring(node)
|
|
211
|
+
if parent_qualname and not is_parent_class:
|
|
212
|
+
is_exported = False
|
|
213
|
+
visibility = "private"
|
|
214
|
+
elif is_parent_class:
|
|
215
|
+
if node.name.startswith("__") and not node.name.endswith("__"):
|
|
216
|
+
visibility = "private"
|
|
217
|
+
is_exported = False
|
|
218
|
+
elif node.name.startswith("_"):
|
|
219
|
+
visibility = "internal"
|
|
220
|
+
is_exported = False
|
|
221
|
+
else:
|
|
222
|
+
visibility = "public"
|
|
223
|
+
is_exported = True
|
|
224
|
+
else:
|
|
225
|
+
if all_names is not None:
|
|
226
|
+
is_exported = node.name in all_names
|
|
227
|
+
visibility = "public" if is_exported else ("private" if node.name.startswith("__") else "internal")
|
|
228
|
+
else:
|
|
229
|
+
if node.name.startswith("__") and not node.name.endswith("__"):
|
|
230
|
+
visibility = "private"
|
|
231
|
+
is_exported = False
|
|
232
|
+
elif node.name.startswith("_"):
|
|
233
|
+
visibility = "internal"
|
|
234
|
+
is_exported = False
|
|
235
|
+
else:
|
|
236
|
+
visibility = "public"
|
|
237
|
+
is_exported = True
|
|
238
|
+
|
|
239
|
+
symbol = Symbol(
|
|
240
|
+
name=node.name,
|
|
241
|
+
qualname=qualname,
|
|
242
|
+
file_path=file_path,
|
|
243
|
+
kind=kind,
|
|
244
|
+
lineno=node.lineno,
|
|
245
|
+
end_lineno=node.end_lineno or node.lineno,
|
|
246
|
+
signature=signature,
|
|
247
|
+
params=params,
|
|
248
|
+
min_args=min_args,
|
|
249
|
+
max_args=max_args,
|
|
250
|
+
accepted_kwargs=accepted_kwargs,
|
|
251
|
+
required_kwargs=required_kwargs,
|
|
252
|
+
return_type=return_type,
|
|
253
|
+
calls=calls,
|
|
254
|
+
is_method=is_method,
|
|
255
|
+
is_static=is_static,
|
|
256
|
+
docstring=docstring,
|
|
257
|
+
is_exported=is_exported,
|
|
258
|
+
visibility=visibility,
|
|
259
|
+
)
|
|
260
|
+
symbols.append(symbol)
|
|
261
|
+
process_body(node.body, parent_qualname=qualname, is_parent_class=False)
|
|
262
|
+
|
|
263
|
+
elif isinstance(node, ast.ClassDef):
|
|
264
|
+
qualname = f"{parent_qualname}.{node.name}" if parent_qualname else node.name
|
|
265
|
+
bases_list = [ast.unparse(b) for b in node.bases]
|
|
266
|
+
bases_str = ", ".join(bases_list)
|
|
267
|
+
signature = f"class {node.name}({bases_str})" if bases_str else f"class {node.name}"
|
|
268
|
+
sym_id = f"{file_path}::{qualname}"
|
|
269
|
+
calls = _extract_calls_from_node(node, caller_id=sym_id)
|
|
270
|
+
|
|
271
|
+
docstring = ast.get_docstring(node)
|
|
272
|
+
if all_names is not None:
|
|
273
|
+
is_exported = node.name in all_names
|
|
274
|
+
visibility = "public" if is_exported else ("private" if node.name.startswith("__") else "internal")
|
|
275
|
+
else:
|
|
276
|
+
if node.name.startswith("__") and not node.name.endswith("__"):
|
|
277
|
+
visibility = "private"
|
|
278
|
+
is_exported = False
|
|
279
|
+
elif node.name.startswith("_"):
|
|
280
|
+
visibility = "internal"
|
|
281
|
+
is_exported = False
|
|
282
|
+
else:
|
|
283
|
+
visibility = "public"
|
|
284
|
+
is_exported = True
|
|
285
|
+
|
|
286
|
+
symbol = Symbol(
|
|
287
|
+
name=node.name,
|
|
288
|
+
qualname=qualname,
|
|
289
|
+
file_path=file_path,
|
|
290
|
+
kind="class",
|
|
291
|
+
lineno=node.lineno,
|
|
292
|
+
end_lineno=node.end_lineno or node.lineno,
|
|
293
|
+
signature=signature,
|
|
294
|
+
calls=calls,
|
|
295
|
+
bases=bases_list,
|
|
296
|
+
docstring=docstring,
|
|
297
|
+
is_exported=is_exported,
|
|
298
|
+
visibility=visibility,
|
|
299
|
+
)
|
|
300
|
+
symbols.append(symbol)
|
|
301
|
+
process_body(node.body, parent_qualname=qualname, is_parent_class=True)
|
|
302
|
+
|
|
303
|
+
process_body(tree.body)
|
|
304
|
+
|
|
305
|
+
module_calls = [
|
|
306
|
+
c for c in _extract_calls_from_node(tree, caller_id=f"{file_path}::<module>")
|
|
307
|
+
if not any(s.lineno <= c.lineno <= s.end_lineno for s in symbols)
|
|
308
|
+
]
|
|
309
|
+
if module_calls:
|
|
310
|
+
line_count = len(source.splitlines()) or 1
|
|
311
|
+
module_doc = ast.get_docstring(tree)
|
|
312
|
+
module_sym = Symbol(
|
|
313
|
+
name="<module>",
|
|
314
|
+
qualname="<module>",
|
|
315
|
+
file_path=file_path,
|
|
316
|
+
kind="module",
|
|
317
|
+
lineno=1,
|
|
318
|
+
end_lineno=line_count,
|
|
319
|
+
signature=f"# module {file_path}",
|
|
320
|
+
calls=module_calls,
|
|
321
|
+
docstring=module_doc,
|
|
322
|
+
is_exported=True,
|
|
323
|
+
visibility="public",
|
|
324
|
+
)
|
|
325
|
+
symbols.append(module_sym)
|
|
326
|
+
|
|
327
|
+
return symbols
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def validate_python_syntax(source: str, file_path: str = "") -> Optional[str]:
|
|
331
|
+
"""Validate Python source syntax, returning an error message if invalid."""
|
|
332
|
+
try:
|
|
333
|
+
ast.parse(source)
|
|
334
|
+
return None
|
|
335
|
+
except SyntaxError as e:
|
|
336
|
+
return f"SyntaxError at line {e.lineno}:{e.offset}: {e.msg}"
|