blasttrace 1.0.2__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.
Potentially problematic release.
This version of blasttrace might be problematic. Click here for more details.
- blasttrace/__init__.py +7 -0
- blasttrace/classifier.py +415 -0
- blasttrace/license.py +293 -0
- blasttrace/parsers/__init__.py +4 -0
- blasttrace/parsers/api_route_parser.py +318 -0
- blasttrace/parsers/contract_linker.py +490 -0
- blasttrace/parsers/python_ast_parser.py +340 -0
- blasttrace/parsers/sql_schema_parser.py +376 -0
- blasttrace/parsers/test_mapper.py +182 -0
- blasttrace/parsers/ts_react_parser.py +299 -0
- blasttrace/remediation.py +220 -0
- blasttrace/server.py +824 -0
- blasttrace/visualizer.py +165 -0
- blasttrace-1.0.2.dist-info/METADATA +125 -0
- blasttrace-1.0.2.dist-info/RECORD +18 -0
- blasttrace-1.0.2.dist-info/WHEEL +5 -0
- blasttrace-1.0.2.dist-info/entry_points.txt +3 -0
- blasttrace-1.0.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BlastTrace Python AST Parser
|
|
3
|
+
Extracts function/method signatures, parameter defaults, return types, class fields,
|
|
4
|
+
ORM/SQL references, and cross-module call edges for the Deterministic Dependency Reachability Engine.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import ast
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
from typing import Dict, Any, List, Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _unparse_safe(node: Optional[ast.AST]) -> str:
|
|
14
|
+
if node is None:
|
|
15
|
+
return ""
|
|
16
|
+
try:
|
|
17
|
+
return ast.unparse(node)
|
|
18
|
+
except Exception:
|
|
19
|
+
return ""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _extract_function_signature(fn_node: ast.AST) -> Dict[str, Any]:
|
|
23
|
+
args_obj = getattr(fn_node, "args", None)
|
|
24
|
+
params: List[Dict[str, Any]] = []
|
|
25
|
+
if args_obj:
|
|
26
|
+
pos_args = list(getattr(args_obj, "posonlyargs", [])) + list(args_obj.args)
|
|
27
|
+
defaults = list(args_obj.defaults)
|
|
28
|
+
num_no_default = len(pos_args) - len(defaults)
|
|
29
|
+
|
|
30
|
+
for idx, arg in enumerate(pos_args):
|
|
31
|
+
if arg.arg in ("self", "cls"):
|
|
32
|
+
continue
|
|
33
|
+
has_def = idx >= num_no_default
|
|
34
|
+
def_node = defaults[idx - num_no_default] if has_def else None
|
|
35
|
+
params.append({
|
|
36
|
+
"name": arg.arg,
|
|
37
|
+
"annotation": _unparse_safe(arg.annotation),
|
|
38
|
+
"required": not has_def,
|
|
39
|
+
"default": _unparse_safe(def_node) if has_def else None,
|
|
40
|
+
"kind": "positional_or_keyword",
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
if args_obj.vararg:
|
|
44
|
+
params.append({
|
|
45
|
+
"name": f"*{args_obj.vararg.arg}",
|
|
46
|
+
"annotation": _unparse_safe(args_obj.vararg.annotation),
|
|
47
|
+
"required": False,
|
|
48
|
+
"default": None,
|
|
49
|
+
"kind": "vararg",
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
kw_defaults = list(args_obj.kw_defaults)
|
|
53
|
+
for idx, kwarg in enumerate(args_obj.kwonlyargs):
|
|
54
|
+
def_node = kw_defaults[idx] if idx < len(kw_defaults) else None
|
|
55
|
+
has_def = def_node is not None
|
|
56
|
+
params.append({
|
|
57
|
+
"name": kwarg.arg,
|
|
58
|
+
"annotation": _unparse_safe(kwarg.annotation),
|
|
59
|
+
"required": not has_def,
|
|
60
|
+
"default": _unparse_safe(def_node) if has_def else None,
|
|
61
|
+
"kind": "keyword_only",
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
if args_obj.kwarg:
|
|
65
|
+
params.append({
|
|
66
|
+
"name": f"**{args_obj.kwarg.arg}",
|
|
67
|
+
"annotation": _unparse_safe(args_obj.kwarg.annotation),
|
|
68
|
+
"required": False,
|
|
69
|
+
"default": None,
|
|
70
|
+
"kind": "kwarg",
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
return_type = _unparse_safe(getattr(fn_node, "returns", None))
|
|
74
|
+
decorators = [_unparse_safe(d) for d in getattr(fn_node, "decorator_list", [])]
|
|
75
|
+
|
|
76
|
+
sig_parts = []
|
|
77
|
+
for p in params:
|
|
78
|
+
part = p["name"]
|
|
79
|
+
if p["annotation"]:
|
|
80
|
+
part += f": {p['annotation']}"
|
|
81
|
+
if not p["required"] and p["default"] is not None:
|
|
82
|
+
part += f" = {p['default']}"
|
|
83
|
+
sig_parts.append(part)
|
|
84
|
+
sig_str = f"def {getattr(fn_node, 'name', 'unknown')}({', '.join(sig_parts)})"
|
|
85
|
+
if return_type:
|
|
86
|
+
sig_str += f" -> {return_type}"
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
"name": getattr(fn_node, "name", ""),
|
|
90
|
+
"is_async": isinstance(fn_node, ast.AsyncFunctionDef),
|
|
91
|
+
"params": params,
|
|
92
|
+
"return_type": return_type,
|
|
93
|
+
"decorators": decorators,
|
|
94
|
+
"signature_string": sig_str,
|
|
95
|
+
"lineno": getattr(fn_node, "lineno", 1),
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _inspect_function_body(fn_node: ast.AST) -> Dict[str, Any]:
|
|
100
|
+
calls: List[str] = []
|
|
101
|
+
attributes: List[str] = []
|
|
102
|
+
returned_keys: List[str] = []
|
|
103
|
+
string_literals: List[str] = []
|
|
104
|
+
|
|
105
|
+
for sub in ast.walk(fn_node):
|
|
106
|
+
if isinstance(sub, ast.Call):
|
|
107
|
+
if isinstance(sub.func, ast.Name):
|
|
108
|
+
calls.append(sub.func.id)
|
|
109
|
+
elif isinstance(sub.func, ast.Attribute):
|
|
110
|
+
owner = _unparse_safe(sub.func.value)
|
|
111
|
+
calls.append(f"{owner}.{sub.func.attr}" if owner else sub.func.attr)
|
|
112
|
+
calls.append(sub.func.attr)
|
|
113
|
+
elif isinstance(sub, ast.Attribute):
|
|
114
|
+
owner = _unparse_safe(sub.value)
|
|
115
|
+
attributes.append(f"{owner}.{sub.attr}" if owner else sub.attr)
|
|
116
|
+
attributes.append(sub.attr)
|
|
117
|
+
elif isinstance(sub, ast.Dict):
|
|
118
|
+
for k in sub.keys:
|
|
119
|
+
if isinstance(k, ast.Constant) and isinstance(k.value, str):
|
|
120
|
+
returned_keys.append(k.value)
|
|
121
|
+
elif isinstance(sub, ast.Constant) and isinstance(sub.value, str):
|
|
122
|
+
if len(sub.value) > 2:
|
|
123
|
+
string_literals.append(sub.value)
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
"calls": list(dict.fromkeys(calls)),
|
|
127
|
+
"attributes": list(dict.fromkeys(attributes)),
|
|
128
|
+
"dict_keys": list(dict.fromkeys(returned_keys)),
|
|
129
|
+
"string_literals": string_literals,
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def parse_python_content(content: str, rel_file: str) -> Dict[str, Any]:
|
|
134
|
+
functions: Dict[str, Dict[str, Any]] = {}
|
|
135
|
+
classes: Dict[str, Dict[str, Any]] = {}
|
|
136
|
+
imports: List[Dict[str, str]] = []
|
|
137
|
+
nodes: List[Dict[str, Any]] = []
|
|
138
|
+
edges: List[Dict[str, str]] = []
|
|
139
|
+
|
|
140
|
+
module_node_id = f"py:{rel_file}"
|
|
141
|
+
nodes.append({
|
|
142
|
+
"id": module_node_id,
|
|
143
|
+
"type": "python_module",
|
|
144
|
+
"name": rel_file,
|
|
145
|
+
"file": rel_file,
|
|
146
|
+
"layer": "backend",
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
tree = ast.parse(content)
|
|
151
|
+
except SyntaxError as exc:
|
|
152
|
+
return {
|
|
153
|
+
"error": f"SyntaxError in {rel_file}: {exc}",
|
|
154
|
+
"functions": {},
|
|
155
|
+
"classes": {},
|
|
156
|
+
"imports": [],
|
|
157
|
+
"nodes": nodes,
|
|
158
|
+
"edges": edges,
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
for node in tree.body:
|
|
162
|
+
if isinstance(node, ast.Import):
|
|
163
|
+
for alias in node.names:
|
|
164
|
+
imports.append({
|
|
165
|
+
"module": alias.name,
|
|
166
|
+
"symbol": alias.asname or alias.name,
|
|
167
|
+
"raw": f"import {alias.name}",
|
|
168
|
+
})
|
|
169
|
+
elif isinstance(node, ast.ImportFrom):
|
|
170
|
+
mod = node.module or ""
|
|
171
|
+
for alias in node.names:
|
|
172
|
+
imports.append({
|
|
173
|
+
"module": mod,
|
|
174
|
+
"symbol": alias.name,
|
|
175
|
+
"alias": alias.asname or alias.name,
|
|
176
|
+
"raw": f"from {mod} import {alias.name}",
|
|
177
|
+
})
|
|
178
|
+
elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
179
|
+
sig = _extract_function_signature(node)
|
|
180
|
+
body_info = _inspect_function_body(node)
|
|
181
|
+
sig.update(body_info)
|
|
182
|
+
sig["file"] = rel_file
|
|
183
|
+
fn_id = f"py:{rel_file}:{node.name}"
|
|
184
|
+
functions[node.name] = sig
|
|
185
|
+
nodes.append({
|
|
186
|
+
"id": fn_id,
|
|
187
|
+
"type": "python_function",
|
|
188
|
+
"name": node.name,
|
|
189
|
+
"qualified_name": f"{rel_file}:{node.name}",
|
|
190
|
+
"signature": sig["signature_string"],
|
|
191
|
+
"params": sig["params"],
|
|
192
|
+
"return_type": sig["return_type"],
|
|
193
|
+
"file": rel_file,
|
|
194
|
+
"lineno": sig["lineno"],
|
|
195
|
+
"layer": "backend",
|
|
196
|
+
})
|
|
197
|
+
edges.append({
|
|
198
|
+
"source": module_node_id,
|
|
199
|
+
"target": fn_id,
|
|
200
|
+
"relation": "defines_function",
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
elif isinstance(node, ast.ClassDef):
|
|
204
|
+
bases = [_unparse_safe(b) for b in node.bases]
|
|
205
|
+
cls_fields: Dict[str, Dict[str, Any]] = {}
|
|
206
|
+
cls_methods: Dict[str, Dict[str, Any]] = {}
|
|
207
|
+
tablename = None
|
|
208
|
+
|
|
209
|
+
for item in node.body:
|
|
210
|
+
if isinstance(item, ast.Assign):
|
|
211
|
+
for target in item.targets:
|
|
212
|
+
if isinstance(target, ast.Name):
|
|
213
|
+
val_str = _unparse_safe(item.value)
|
|
214
|
+
if target.id == "__tablename__":
|
|
215
|
+
tablename = val_str.strip("\"'")
|
|
216
|
+
elif not target.id.startswith("__"):
|
|
217
|
+
cls_fields[target.id] = {
|
|
218
|
+
"name": target.id,
|
|
219
|
+
"annotation": "",
|
|
220
|
+
"default": val_str,
|
|
221
|
+
"lineno": item.lineno,
|
|
222
|
+
}
|
|
223
|
+
elif isinstance(item, ast.AnnAssign) and isinstance(item.target, ast.Name):
|
|
224
|
+
fname = item.target.id
|
|
225
|
+
cls_fields[fname] = {
|
|
226
|
+
"name": fname,
|
|
227
|
+
"annotation": _unparse_safe(item.annotation),
|
|
228
|
+
"default": _unparse_safe(item.value) if item.value else None,
|
|
229
|
+
"lineno": item.lineno,
|
|
230
|
+
}
|
|
231
|
+
elif isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
232
|
+
msig = _extract_function_body_and_sig(item, rel_file, node.name)
|
|
233
|
+
cls_methods[item.name] = msig
|
|
234
|
+
functions[f"{node.name}.{item.name}"] = msig
|
|
235
|
+
|
|
236
|
+
cls_id = f"py:{rel_file}:{node.name}"
|
|
237
|
+
classes[node.name] = {
|
|
238
|
+
"name": node.name,
|
|
239
|
+
"bases": bases,
|
|
240
|
+
"tablename": tablename,
|
|
241
|
+
"fields": cls_fields,
|
|
242
|
+
"methods": cls_methods,
|
|
243
|
+
"file": rel_file,
|
|
244
|
+
"lineno": node.lineno,
|
|
245
|
+
}
|
|
246
|
+
nodes.append({
|
|
247
|
+
"id": cls_id,
|
|
248
|
+
"type": "python_class",
|
|
249
|
+
"name": node.name,
|
|
250
|
+
"bases": bases,
|
|
251
|
+
"fields": list(cls_fields.keys()),
|
|
252
|
+
"file": rel_file,
|
|
253
|
+
"lineno": node.lineno,
|
|
254
|
+
"layer": "backend",
|
|
255
|
+
})
|
|
256
|
+
edges.append({
|
|
257
|
+
"source": module_node_id,
|
|
258
|
+
"target": cls_id,
|
|
259
|
+
"relation": "defines_class",
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
for fname, fmeta in cls_fields.items():
|
|
263
|
+
field_id = f"py:{rel_file}:{node.name}.{fname}"
|
|
264
|
+
nodes.append({
|
|
265
|
+
"id": field_id,
|
|
266
|
+
"type": "class_field",
|
|
267
|
+
"name": f"{node.name}.{fname}",
|
|
268
|
+
"class_name": node.name,
|
|
269
|
+
"field": fname,
|
|
270
|
+
"annotation": fmeta["annotation"],
|
|
271
|
+
"file": rel_file,
|
|
272
|
+
"layer": "backend",
|
|
273
|
+
})
|
|
274
|
+
edges.append({
|
|
275
|
+
"source": cls_id,
|
|
276
|
+
"target": field_id,
|
|
277
|
+
"relation": "has_field",
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
for mname, mmeta in cls_methods.items():
|
|
281
|
+
method_id = f"py:{rel_file}:{node.name}.{mname}"
|
|
282
|
+
nodes.append({
|
|
283
|
+
"id": method_id,
|
|
284
|
+
"type": "python_method",
|
|
285
|
+
"name": f"{node.name}.{mname}",
|
|
286
|
+
"signature": mmeta["signature_string"],
|
|
287
|
+
"params": mmeta["params"],
|
|
288
|
+
"return_type": mmeta["return_type"],
|
|
289
|
+
"file": rel_file,
|
|
290
|
+
"lineno": mmeta["lineno"],
|
|
291
|
+
"layer": "backend",
|
|
292
|
+
})
|
|
293
|
+
edges.append({
|
|
294
|
+
"source": cls_id,
|
|
295
|
+
"target": method_id,
|
|
296
|
+
"relation": "has_method",
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
# Link intra-module function calls (callee -> caller impact direction)
|
|
300
|
+
for fn_name, fn_meta in functions.items():
|
|
301
|
+
caller_id = f"py:{rel_file}:{fn_name}"
|
|
302
|
+
for called in fn_meta.get("calls", []):
|
|
303
|
+
if called in functions and called != fn_name:
|
|
304
|
+
callee_id = f"py:{rel_file}:{called}"
|
|
305
|
+
edges.append({
|
|
306
|
+
"source": callee_id,
|
|
307
|
+
"target": caller_id,
|
|
308
|
+
"relation": "called_by",
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
return {
|
|
312
|
+
"file": rel_file,
|
|
313
|
+
"functions": functions,
|
|
314
|
+
"classes": classes,
|
|
315
|
+
"imports": imports,
|
|
316
|
+
"nodes": nodes,
|
|
317
|
+
"edges": edges,
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _extract_function_body_and_sig(fn_node: ast.AST, rel_file: str, cls_name: str) -> Dict[str, Any]:
|
|
322
|
+
sig = _extract_function_signature(fn_node)
|
|
323
|
+
body_info = _inspect_function_body(fn_node)
|
|
324
|
+
sig.update(body_info)
|
|
325
|
+
sig["file"] = rel_file
|
|
326
|
+
sig["class_name"] = cls_name
|
|
327
|
+
return sig
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def parse_python_file(file_path: str, repo_root: str = "") -> Dict[str, Any]:
|
|
331
|
+
if not os.path.exists(file_path):
|
|
332
|
+
return {"error": f"Python file not found: {file_path}", "functions": {}, "classes": {}, "imports": [], "nodes": [], "edges": []}
|
|
333
|
+
rel_file = os.path.relpath(file_path, repo_root).replace("\\", "/") if repo_root else os.path.basename(file_path)
|
|
334
|
+
try:
|
|
335
|
+
with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
|
|
336
|
+
content = f.read()
|
|
337
|
+
except Exception as exc:
|
|
338
|
+
return {"error": str(exc), "functions": {}, "classes": {}, "imports": [], "nodes": [], "edges": []}
|
|
339
|
+
return parse_python_content(content, rel_file)
|
|
340
|
+
|