codesuture 0.5.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.
- codesuture/__init__.py +1 -0
- codesuture/__main__.py +5 -0
- codesuture/_eval_fix.py +5 -0
- codesuture/audit.py +127 -0
- codesuture/cli.py +127 -0
- codesuture/code_replacer.py +82 -0
- codesuture/codesuture_fix.py +85 -0
- codesuture/debuggee.py +7 -0
- codesuture/diff_guard.py +27 -0
- codesuture/explain.py +147 -0
- codesuture/fingerprint.py +52 -0
- codesuture/guard_synthesizer.py +607 -0
- codesuture/knowledge.py +35 -0
- codesuture/middleware.py +86 -0
- codesuture/pattern_matcher.py +555 -0
- codesuture/persistence.py +330 -0
- codesuture/plugins/__init__.py +0 -0
- codesuture/plugins/autonomous.py +64 -0
- codesuture/rewind.py +43 -0
- codesuture/rollback.py +85 -0
- codesuture/sandbox.py +105 -0
- codesuture/shadow.py +20 -0
- codesuture/tracer.py +447 -0
- codesuture/watcher.py +78 -0
- codesuture-0.5.0.dist-info/METADATA +106 -0
- codesuture-0.5.0.dist-info/RECORD +30 -0
- codesuture-0.5.0.dist-info/WHEEL +5 -0
- codesuture-0.5.0.dist-info/entry_points.txt +2 -0
- codesuture-0.5.0.dist-info/licenses/LICENSE +33 -0
- codesuture-0.5.0.dist-info/top_level.txt +1 -0
codesuture/middleware.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
|
|
2
|
+
import sys
|
|
3
|
+
import traceback
|
|
4
|
+
|
|
5
|
+
class CodeSutureMiddleware:
|
|
6
|
+
|
|
7
|
+
def __init__(self, app):
|
|
8
|
+
self.app = app
|
|
9
|
+
self._retried_exc_types = set()
|
|
10
|
+
|
|
11
|
+
def __call__(self, environ, start_response):
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
return self.app(environ, start_response)
|
|
15
|
+
except Exception as exc:
|
|
16
|
+
return self._handle_exception(exc, environ, start_response)
|
|
17
|
+
|
|
18
|
+
def _handle_exception(self, exc, environ, start_response):
|
|
19
|
+
|
|
20
|
+
exc_type = type(exc)
|
|
21
|
+
|
|
22
|
+
if exc_type in self._retried_exc_types:
|
|
23
|
+
raise exc
|
|
24
|
+
|
|
25
|
+
tb = sys.exc_info()[2]
|
|
26
|
+
if tb is None:
|
|
27
|
+
raise exc
|
|
28
|
+
|
|
29
|
+
inner_tb = tb
|
|
30
|
+
while inner_tb.tb_next:
|
|
31
|
+
inner_tb = inner_tb.tb_next
|
|
32
|
+
frame = inner_tb.tb_frame
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
from codesuture.pattern_matcher import analyze_exception
|
|
36
|
+
from codesuture.guard_synthesizer import synthesize_guarded_code
|
|
37
|
+
from codesuture.code_replacer import replace_function_code, get_function_from_frame
|
|
38
|
+
|
|
39
|
+
spec = analyze_exception(frame, exc_type, exc, tb)
|
|
40
|
+
if spec is None:
|
|
41
|
+
raise exc
|
|
42
|
+
|
|
43
|
+
func = get_function_from_frame(frame)
|
|
44
|
+
if func is None:
|
|
45
|
+
raise exc
|
|
46
|
+
|
|
47
|
+
new_bc = synthesize_guarded_code(frame.f_code, spec)
|
|
48
|
+
new_code = new_bc.to_code()
|
|
49
|
+
replace_function_code(func, new_code)
|
|
50
|
+
|
|
51
|
+
guard_type = spec.strategy
|
|
52
|
+
target_name = spec.var_name
|
|
53
|
+
if spec.key_name:
|
|
54
|
+
target_name = spec.key_name if isinstance(spec.key_name, str) else spec.key_name[-1]
|
|
55
|
+
|
|
56
|
+
self._retried_exc_types.add(exc_type)
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
patched_start_called = []
|
|
60
|
+
|
|
61
|
+
def patched_start_response(status, headers, exc_info=None):
|
|
62
|
+
|
|
63
|
+
headers = list(headers) + [
|
|
64
|
+
("X-CodeSuture", f"patched=1;guard={guard_type};target={target_name}")
|
|
65
|
+
]
|
|
66
|
+
patched_start_called.append(True)
|
|
67
|
+
return start_response(status, headers, exc_info)
|
|
68
|
+
|
|
69
|
+
result = self.app(environ, patched_start_response)
|
|
70
|
+
|
|
71
|
+
if not patched_start_called:
|
|
72
|
+
start_response("200 OK", [
|
|
73
|
+
("X-CodeSuture", f"patched=1;guard={guard_type};target={target_name}")
|
|
74
|
+
])
|
|
75
|
+
|
|
76
|
+
return result
|
|
77
|
+
|
|
78
|
+
except Exception:
|
|
79
|
+
|
|
80
|
+
raise exc
|
|
81
|
+
|
|
82
|
+
except Exception as inner:
|
|
83
|
+
if inner is exc:
|
|
84
|
+
raise
|
|
85
|
+
|
|
86
|
+
raise exc
|
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
from types import FrameType
|
|
2
|
+
from typing import Optional, NamedTuple
|
|
3
|
+
import dis
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
class PatchSpec(NamedTuple):
|
|
7
|
+
strategy: str
|
|
8
|
+
var_name: str
|
|
9
|
+
default_value: object
|
|
10
|
+
key_name: Optional[str] = None
|
|
11
|
+
list_len_var: Optional[str] = None
|
|
12
|
+
is_async: bool = False
|
|
13
|
+
inside_loop: bool = False
|
|
14
|
+
target_func: Optional[object] = None
|
|
15
|
+
target_name: Optional[str] = None
|
|
16
|
+
|
|
17
|
+
def analyze_exception(frame: FrameType, exc_type, exc_value, exc_tb) -> Optional[PatchSpec]:
|
|
18
|
+
from codesuture.code_replacer import get_function_from_frame
|
|
19
|
+
func = get_function_from_frame(frame)
|
|
20
|
+
if func is not None:
|
|
21
|
+
func_name = getattr(func, '__qualname__', func.__name__)
|
|
22
|
+
exc_type_name = getattr(exc_type, '__name__', str(exc_type))
|
|
23
|
+
spec = check_learned_rules(func_name, exc_type_name, str(exc_value))
|
|
24
|
+
if spec:
|
|
25
|
+
|
|
26
|
+
if frame.f_code.co_flags & 0x100:
|
|
27
|
+
spec = spec._replace(is_async=True)
|
|
28
|
+
|
|
29
|
+
instructions = list(dis.get_instructions(frame.f_code))
|
|
30
|
+
crash_instr = _find_target_instr(instructions, frame.f_lasti)
|
|
31
|
+
if crash_instr is not None:
|
|
32
|
+
inside_loop = False
|
|
33
|
+
for instr in instructions:
|
|
34
|
+
if instr.offset >= crash_instr.offset:
|
|
35
|
+
break
|
|
36
|
+
if instr.opname == 'FOR_ITER':
|
|
37
|
+
|
|
38
|
+
if getattr(instr, 'argval', 0) > crash_instr.offset:
|
|
39
|
+
inside_loop = True
|
|
40
|
+
|
|
41
|
+
if inside_loop:
|
|
42
|
+
spec = spec._replace(inside_loop=True)
|
|
43
|
+
print(f"[CodeSuture WARNING] Crash detected inside a for-loop at offset {crash_instr.offset}.\n The patched function will restart from the beginning.\n If the loop has side effects (DB writes, API calls, counters),\n items processed before the crash will be reprocessed.\n Consider adding idempotency checks in the loop body.")
|
|
44
|
+
|
|
45
|
+
return spec
|
|
46
|
+
|
|
47
|
+
msg = str(exc_value)
|
|
48
|
+
type_name = getattr(exc_type, '__name__', str(exc_type))
|
|
49
|
+
|
|
50
|
+
spec = None
|
|
51
|
+
|
|
52
|
+
if type_name == 'AttributeError' and "'NoneType' object has no attribute" in msg:
|
|
53
|
+
spec = _null_guard_spec(frame)
|
|
54
|
+
|
|
55
|
+
elif type_name == 'ZeroDivisionError':
|
|
56
|
+
spec = _division_guard_spec(frame)
|
|
57
|
+
|
|
58
|
+
elif type_name == 'TypeError':
|
|
59
|
+
if "'NoneType' object is not subscriptable" in msg:
|
|
60
|
+
spec = _none_subscript_spec(frame)
|
|
61
|
+
elif "'NoneType' object is not callable" in msg:
|
|
62
|
+
spec = _callable_guard_spec(frame)
|
|
63
|
+
elif "can only concatenate str" in msg:
|
|
64
|
+
spec = _str_concat_spec(frame, msg)
|
|
65
|
+
|
|
66
|
+
if spec is None and type_name in ('TypeError', 'ValueError'):
|
|
67
|
+
|
|
68
|
+
coerce = _type_coercion_spec(frame, msg)
|
|
69
|
+
if coerce is not None:
|
|
70
|
+
spec = coerce
|
|
71
|
+
|
|
72
|
+
if spec is None and type_name == 'IndexError' and "list index out of range" in msg:
|
|
73
|
+
spec = _index_bound_spec(frame)
|
|
74
|
+
|
|
75
|
+
if spec is None and type_name == 'KeyError':
|
|
76
|
+
spec = _dict_get_spec(frame, msg)
|
|
77
|
+
|
|
78
|
+
if spec is None and type_name == 'FileNotFoundError':
|
|
79
|
+
spec = _file_guard_spec(frame)
|
|
80
|
+
|
|
81
|
+
if spec is not None and frame.f_code.co_flags & 0x100:
|
|
82
|
+
spec = spec._replace(is_async=True)
|
|
83
|
+
|
|
84
|
+
return spec
|
|
85
|
+
|
|
86
|
+
def check_learned_rules(func_name, exc_type_name, exc_message):
|
|
87
|
+
from codesuture.knowledge import load_learned_rules
|
|
88
|
+
rules = load_learned_rules()
|
|
89
|
+
for rule in rules:
|
|
90
|
+
if rule["func_name"] == func_name and rule["exc_type_name"] == exc_type_name:
|
|
91
|
+
return PatchSpec(
|
|
92
|
+
strategy='autonomous_rule',
|
|
93
|
+
var_name=func_name,
|
|
94
|
+
default_value=rule["new_source"]
|
|
95
|
+
)
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
def _find_target_instr(instructions, lasti):
|
|
99
|
+
target = None
|
|
100
|
+
for instr in instructions:
|
|
101
|
+
if instr.offset <= lasti:
|
|
102
|
+
target = instr
|
|
103
|
+
else:
|
|
104
|
+
break
|
|
105
|
+
return target
|
|
106
|
+
|
|
107
|
+
def _check_property_origin(frame, instructions, crash_idx):
|
|
108
|
+
load_attr_instr = None
|
|
109
|
+
load_attr_idx = -1
|
|
110
|
+
for i in range(crash_idx - 1, -1, -1):
|
|
111
|
+
if instructions[i].opname in ('LOAD_ATTR', 'LOAD_METHOD'):
|
|
112
|
+
load_attr_instr = instructions[i]
|
|
113
|
+
load_attr_idx = i
|
|
114
|
+
break
|
|
115
|
+
elif instructions[i].opname in ('LOAD_CONST', 'LOAD_FAST', 'LOAD_GLOBAL', 'LOAD_DEREF'):
|
|
116
|
+
pass
|
|
117
|
+
else:
|
|
118
|
+
break
|
|
119
|
+
if not load_attr_instr:
|
|
120
|
+
return None
|
|
121
|
+
attr_name = load_attr_instr.argval
|
|
122
|
+
obj_instr = None
|
|
123
|
+
for i in range(load_attr_idx - 1, -1, -1):
|
|
124
|
+
if instructions[i].opname in ('LOAD_FAST', 'LOAD_DEREF', 'LOAD_GLOBAL'):
|
|
125
|
+
obj_instr = instructions[i]
|
|
126
|
+
break
|
|
127
|
+
elif instructions[i].opname in ('LOAD_ATTR', 'LOAD_METHOD'):
|
|
128
|
+
pass
|
|
129
|
+
else:
|
|
130
|
+
break
|
|
131
|
+
if not obj_instr:
|
|
132
|
+
return None
|
|
133
|
+
obj = None
|
|
134
|
+
if obj_instr.opname in ('LOAD_FAST', 'LOAD_DEREF'):
|
|
135
|
+
var_name = frame.f_code.co_varnames[obj_instr.arg] if obj_instr.opname == 'LOAD_FAST' else ""
|
|
136
|
+
if obj_instr.opname == 'LOAD_DEREF':
|
|
137
|
+
names = frame.f_code.co_freevars + frame.f_code.co_cellvars
|
|
138
|
+
var_name = names[obj_instr.arg] if obj_instr.arg < len(names) else ""
|
|
139
|
+
obj = frame.f_locals.get(var_name)
|
|
140
|
+
elif obj_instr.opname == 'LOAD_GLOBAL':
|
|
141
|
+
var_name = obj_instr.argval
|
|
142
|
+
obj = frame.f_globals.get(var_name, frame.f_builtins.get(var_name))
|
|
143
|
+
if obj is None:
|
|
144
|
+
return None
|
|
145
|
+
prop = getattr(type(obj), attr_name, None)
|
|
146
|
+
if isinstance(prop, property) and prop.fget is not None:
|
|
147
|
+
return {'target_func': prop.fget, 'target_name': f"{type(obj).__name__}.{attr_name}"}
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
def _infer_default(var_name, instructions=None, crash_idx=None):
|
|
151
|
+
if instructions is not None and crash_idx is not None:
|
|
152
|
+
for i in range(crash_idx + 1, min(crash_idx + 11, len(instructions))):
|
|
153
|
+
instr = instructions[i]
|
|
154
|
+
if instr.opname in ('LOAD_ATTR', 'LOAD_METHOD'):
|
|
155
|
+
string_methods = {'upper', 'lower', 'strip', 'replace', 'split', 'join', 'format', 'startswith', 'endswith', 'capitalize', 'lstrip', 'rstrip', 'title', 'swapcase', 'center', 'ljust', 'rjust', 'zfill', 'encode'}
|
|
156
|
+
list_dict_methods = {'append', 'extend', 'pop', 'remove', 'get', 'keys', 'values', 'items', 'update'}
|
|
157
|
+
if instr.argval in string_methods:
|
|
158
|
+
return ""
|
|
159
|
+
if instr.argval in list_dict_methods:
|
|
160
|
+
return []
|
|
161
|
+
elif instr.opname in ('BINARY_OP', 'BINARY_ADD', 'BINARY_SUBTRACT', 'BINARY_MULTIPLY', 'BINARY_TRUE_DIVIDE'):
|
|
162
|
+
return 0
|
|
163
|
+
elif instr.opname in ('POP_JUMP_IF_FALSE', 'POP_JUMP_FORWARD_IF_FALSE', 'POP_JUMP_IF_TRUE', 'POP_JUMP_FORWARD_IF_TRUE'):
|
|
164
|
+
return False
|
|
165
|
+
elif instr.opname == 'IS_OP':
|
|
166
|
+
return None
|
|
167
|
+
elif instr.opname in ('FORMAT_VALUE', 'BUILD_STRING'):
|
|
168
|
+
return ""
|
|
169
|
+
elif instr.opname == 'BINARY_SUBSCR':
|
|
170
|
+
return {}
|
|
171
|
+
elif instr.opname in ('LOAD_CONST', 'LOAD_FAST', 'PRECALL', 'CALL'):
|
|
172
|
+
continue
|
|
173
|
+
elif instr.opname in ('RETURN_VALUE', 'STORE_FAST'):
|
|
174
|
+
break
|
|
175
|
+
|
|
176
|
+
name = var_name.lower()
|
|
177
|
+
if any(kw in name for kw in ('count','num','age','len','size','index','price','amount','discount')):
|
|
178
|
+
return 0
|
|
179
|
+
if any(kw in name for kw in ('name','text','string','msg','email','path','filename')):
|
|
180
|
+
return ""
|
|
181
|
+
if any(kw in name for kw in ('list','items','keys','values','array')):
|
|
182
|
+
return []
|
|
183
|
+
return ""
|
|
184
|
+
|
|
185
|
+
def _infer_attribute_default(attr_name, fallback_name, instructions=None, crash_idx=None):
|
|
186
|
+
string_methods = {
|
|
187
|
+
'capitalize', 'casefold', 'center', 'encode', 'expandtabs', 'format',
|
|
188
|
+
'format_map', 'join', 'ljust', 'lower', 'lstrip', 'removeprefix',
|
|
189
|
+
'removesuffix', 'replace', 'rjust', 'strip', 'swapcase', 'title',
|
|
190
|
+
'translate', 'upper', 'zfill',
|
|
191
|
+
}
|
|
192
|
+
if attr_name in string_methods:
|
|
193
|
+
return ""
|
|
194
|
+
return _infer_default(fallback_name, instructions, crash_idx)
|
|
195
|
+
|
|
196
|
+
_ORIGINAL_INFER_DEFAULT = _infer_default
|
|
197
|
+
|
|
198
|
+
_KNOWN_CALLABLES = {
|
|
199
|
+
'_infer_default': _ORIGINAL_INFER_DEFAULT,
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
def _callable_guard_spec(frame):
|
|
203
|
+
|
|
204
|
+
instructions = list(dis.get_instructions(frame.f_code))
|
|
205
|
+
tgt = _find_target_instr(instructions, frame.f_lasti)
|
|
206
|
+
if tgt is None:
|
|
207
|
+
return None
|
|
208
|
+
idx = instructions.index(tgt)
|
|
209
|
+
|
|
210
|
+
call_idx = None
|
|
211
|
+
for i in range(max(0, idx - 1), min(idx + 3, len(instructions))):
|
|
212
|
+
if instructions[i].opname == 'CALL':
|
|
213
|
+
call_idx = i
|
|
214
|
+
break
|
|
215
|
+
if call_idx is None:
|
|
216
|
+
call_idx = idx
|
|
217
|
+
|
|
218
|
+
var_name = None
|
|
219
|
+
for i in range(call_idx - 1, -1, -1):
|
|
220
|
+
instr = instructions[i]
|
|
221
|
+
if instr.opname == 'LOAD_GLOBAL':
|
|
222
|
+
name = instr.argval
|
|
223
|
+
|
|
224
|
+
val = frame.f_globals.get(name)
|
|
225
|
+
if val is None and name not in frame.f_builtins:
|
|
226
|
+
var_name = name
|
|
227
|
+
break
|
|
228
|
+
if instr.opname in ('LOAD_FAST', 'LOAD_DEREF'):
|
|
229
|
+
name = frame.f_code.co_varnames[instr.arg]
|
|
230
|
+
if frame.f_locals.get(name) is None:
|
|
231
|
+
var_name = name
|
|
232
|
+
break
|
|
233
|
+
if var_name is None:
|
|
234
|
+
return None
|
|
235
|
+
|
|
236
|
+
replacement = _KNOWN_CALLABLES.get(var_name, lambda *a, **kw: 0)
|
|
237
|
+
return PatchSpec('callable_guard', var_name, default_value=replacement)
|
|
238
|
+
|
|
239
|
+
def _null_guard_spec(frame):
|
|
240
|
+
instructions = list(dis.get_instructions(frame.f_code))
|
|
241
|
+
tgt = _find_target_instr(instructions, frame.f_lasti)
|
|
242
|
+
if tgt is None:
|
|
243
|
+
return None
|
|
244
|
+
|
|
245
|
+
if tgt.opname not in ('LOAD_ATTR', 'STORE_ATTR', 'DELETE_ATTR', 'LOAD_METHOD'):
|
|
246
|
+
return None
|
|
247
|
+
|
|
248
|
+
idx = instructions.index(tgt)
|
|
249
|
+
|
|
250
|
+
chain_instrs = []
|
|
251
|
+
if tgt.opname in ('LOAD_ATTR', 'LOAD_METHOD'):
|
|
252
|
+
chain_instrs.append(tgt)
|
|
253
|
+
curr = idx - 1
|
|
254
|
+
while curr >= 0:
|
|
255
|
+
instr = instructions[curr]
|
|
256
|
+
if instr.opname in ('LOAD_ATTR', 'LOAD_METHOD', 'LOAD_FAST', 'LOAD_GLOBAL', 'LOAD_DEREF'):
|
|
257
|
+
chain_instrs.insert(0, instr)
|
|
258
|
+
if instr.opname in ('LOAD_FAST', 'LOAD_GLOBAL', 'LOAD_DEREF'):
|
|
259
|
+
break
|
|
260
|
+
curr -= 1
|
|
261
|
+
else:
|
|
262
|
+
break
|
|
263
|
+
|
|
264
|
+
if not chain_instrs:
|
|
265
|
+
|
|
266
|
+
for i in range(idx, -1, -1):
|
|
267
|
+
if instructions[i].opname in ('LOAD_FAST', 'LOAD_DEREF'):
|
|
268
|
+
var_name = frame.f_code.co_varnames[instructions[i].arg]
|
|
269
|
+
attr_name = tgt.argval if tgt.opname in ('LOAD_ATTR', 'LOAD_METHOD') else None
|
|
270
|
+
return PatchSpec('null_guard', var_name, _infer_attribute_default(attr_name, var_name, instructions, idx))
|
|
271
|
+
return None
|
|
272
|
+
|
|
273
|
+
obj = None
|
|
274
|
+
root_instr = chain_instrs[0]
|
|
275
|
+
if root_instr.opname == 'LOAD_FAST':
|
|
276
|
+
root_name = frame.f_code.co_varnames[root_instr.arg]
|
|
277
|
+
obj = frame.f_locals.get(root_name)
|
|
278
|
+
parent_local = root_name
|
|
279
|
+
elif root_instr.opname == 'LOAD_GLOBAL':
|
|
280
|
+
root_name = root_instr.argval
|
|
281
|
+
obj = frame.f_globals.get(root_name, frame.f_builtins.get(root_name))
|
|
282
|
+
parent_local = root_name
|
|
283
|
+
elif root_instr.opname == 'LOAD_DEREF':
|
|
284
|
+
names = frame.f_code.co_freevars + frame.f_code.co_cellvars
|
|
285
|
+
root_name = names[root_instr.arg] if root_instr.arg < len(names) else f"unknown_{root_instr.arg}"
|
|
286
|
+
obj = frame.f_locals.get(root_name)
|
|
287
|
+
parent_local = root_name
|
|
288
|
+
else:
|
|
289
|
+
return None
|
|
290
|
+
|
|
291
|
+
if obj is None:
|
|
292
|
+
if len(chain_instrs) > 1:
|
|
293
|
+
attr_chain = [chain_instrs[j].argval for j in range(1, len(chain_instrs))]
|
|
294
|
+
attr_name = chain_instrs[-1].argval
|
|
295
|
+
default = _infer_attribute_default(attr_name, parent_local, instructions, idx)
|
|
296
|
+
return PatchSpec('null_guard', parent_local, default, key_name=tuple(attr_chain))
|
|
297
|
+
else:
|
|
298
|
+
attr_name = tgt.argval if tgt.opname in ('LOAD_ATTR', 'LOAD_METHOD') else None
|
|
299
|
+
return PatchSpec('null_guard', parent_local, _infer_attribute_default(attr_name, parent_local, instructions, idx), key_name=(attr_name,) if attr_name else None)
|
|
300
|
+
|
|
301
|
+
for i in range(1, len(chain_instrs)):
|
|
302
|
+
instr = chain_instrs[i]
|
|
303
|
+
attr = instr.argval
|
|
304
|
+
try:
|
|
305
|
+
next_obj = getattr(obj, attr)
|
|
306
|
+
except AttributeError:
|
|
307
|
+
break
|
|
308
|
+
|
|
309
|
+
if next_obj is None:
|
|
310
|
+
|
|
311
|
+
prop = getattr(type(obj), attr, None)
|
|
312
|
+
if isinstance(prop, property) and prop.fget is not None:
|
|
313
|
+
attr_chain = [chain_instrs[j].argval for j in range(1, i + 1)]
|
|
314
|
+
attr_name = chain_instrs[i+1].argval if i + 1 < len(chain_instrs) else (tgt.argval if tgt.opname in ('LOAD_ATTR', 'LOAD_METHOD') else None)
|
|
315
|
+
default = _infer_attribute_default(attr_name, attr, instructions, idx)
|
|
316
|
+
return PatchSpec('return_guard', parent_local, default, key_name=tuple(attr_chain), target_func=prop.fget, target_name=f"{type(obj).__name__}.{attr}")
|
|
317
|
+
|
|
318
|
+
attr_chain = [chain_instrs[j].argval for j in range(1, i + 1)]
|
|
319
|
+
attr_name = chain_instrs[i+1].argval if i + 1 < len(chain_instrs) else (tgt.argval if tgt.opname in ('LOAD_ATTR', 'LOAD_METHOD') else None)
|
|
320
|
+
default = _infer_attribute_default(attr_name, attr, instructions, idx)
|
|
321
|
+
return PatchSpec('null_guard', parent_local, default, key_name=tuple(attr_chain))
|
|
322
|
+
|
|
323
|
+
obj = next_obj
|
|
324
|
+
|
|
325
|
+
attr_name = tgt.argval if tgt.opname in ('LOAD_ATTR', 'LOAD_METHOD') else None
|
|
326
|
+
return PatchSpec('null_guard', parent_local, _infer_attribute_default(attr_name, parent_local, instructions, idx))
|
|
327
|
+
|
|
328
|
+
def _division_guard_spec(frame):
|
|
329
|
+
instructions = list(dis.get_instructions(frame.f_code))
|
|
330
|
+
tgt = _find_target_instr(instructions, frame.f_lasti)
|
|
331
|
+
if tgt is None:
|
|
332
|
+
return None
|
|
333
|
+
idx = instructions.index(tgt)
|
|
334
|
+
if tgt.opname == 'BINARY_OP':
|
|
335
|
+
if idx > 0 and instructions[idx-1].opname in ('LOAD_FAST', 'LOAD_DEREF'):
|
|
336
|
+
var_name = frame.f_code.co_varnames[instructions[idx-1].arg]
|
|
337
|
+
return PatchSpec(strategy='division_guard', var_name=var_name, default_value=1)
|
|
338
|
+
if tgt.opname in ('BINARY_TRUE_DIVIDE', 'BINARY_FLOOR_DIVIDE', 'BINARY_MODULO'):
|
|
339
|
+
if idx > 0 and instructions[idx-1].opname in ('LOAD_FAST', 'LOAD_DEREF'):
|
|
340
|
+
var_name = frame.f_code.co_varnames[instructions[idx-1].arg]
|
|
341
|
+
return PatchSpec(strategy='division_guard', var_name=var_name, default_value=1)
|
|
342
|
+
return None
|
|
343
|
+
|
|
344
|
+
def _none_subscript_spec(frame):
|
|
345
|
+
instructions = list(dis.get_instructions(frame.f_code))
|
|
346
|
+
tgt = _find_target_instr(instructions, frame.f_lasti)
|
|
347
|
+
if tgt is None or tgt.opname not in ('BINARY_SUBSCR', 'STORE_SUBSCR'):
|
|
348
|
+
return None
|
|
349
|
+
idx = instructions.index(tgt)
|
|
350
|
+
|
|
351
|
+
chain_spec = _try_chain_subscript(frame, instructions, idx)
|
|
352
|
+
if chain_spec is not None:
|
|
353
|
+
return chain_spec
|
|
354
|
+
|
|
355
|
+
container_var = None
|
|
356
|
+
key_const = None
|
|
357
|
+
key_var = None
|
|
358
|
+
|
|
359
|
+
for i in range(idx-1, -1, -1):
|
|
360
|
+
instr = instructions[i]
|
|
361
|
+
if instr.opname in ('LOAD_FAST', 'LOAD_DEREF'):
|
|
362
|
+
if container_var is None:
|
|
363
|
+
container_var = frame.f_code.co_varnames[instr.arg]
|
|
364
|
+
elif key_var is None and key_const is None:
|
|
365
|
+
key_var = frame.f_code.co_varnames[instr.arg]
|
|
366
|
+
break
|
|
367
|
+
else:
|
|
368
|
+
break
|
|
369
|
+
elif instr.opname == 'LOAD_CONST':
|
|
370
|
+
if key_const is None and key_var is None:
|
|
371
|
+
key_const = frame.f_code.co_consts[instr.arg]
|
|
372
|
+
|
|
373
|
+
if container_var is None:
|
|
374
|
+
return None
|
|
375
|
+
final_key = key_const if key_const is not None else key_var
|
|
376
|
+
if final_key is None:
|
|
377
|
+
return None
|
|
378
|
+
|
|
379
|
+
spec = PatchSpec('subscript_guard', var_name=container_var,
|
|
380
|
+
default_value=_infer_default(final_key if isinstance(final_key, str) else '', instructions, idx),
|
|
381
|
+
key_name=final_key)
|
|
382
|
+
|
|
383
|
+
prop_origin = _check_property_origin(frame, instructions, idx)
|
|
384
|
+
if prop_origin:
|
|
385
|
+
|
|
386
|
+
return PatchSpec('return_guard', container_var, {}, key_name=spec.key_name, target_func=prop_origin['target_func'], target_name=prop_origin['target_name'])
|
|
387
|
+
|
|
388
|
+
return spec
|
|
389
|
+
|
|
390
|
+
def _try_chain_subscript(frame, instructions, failing_idx):
|
|
391
|
+
|
|
392
|
+
keys_back = []
|
|
393
|
+
|
|
394
|
+
key_idx = failing_idx - 1
|
|
395
|
+
if key_idx < 0:
|
|
396
|
+
return None
|
|
397
|
+
key_instr = instructions[key_idx]
|
|
398
|
+
if key_instr.opname == 'LOAD_CONST':
|
|
399
|
+
keys_back.append(frame.f_code.co_consts[key_instr.arg])
|
|
400
|
+
else:
|
|
401
|
+
|
|
402
|
+
return None
|
|
403
|
+
|
|
404
|
+
pos = key_idx - 1
|
|
405
|
+
root_var = None
|
|
406
|
+
while pos >= 0:
|
|
407
|
+
instr = instructions[pos]
|
|
408
|
+
if instr.opname == 'BINARY_SUBSCR':
|
|
409
|
+
pos -= 1
|
|
410
|
+
if pos < 0:
|
|
411
|
+
return None
|
|
412
|
+
k_instr = instructions[pos]
|
|
413
|
+
if k_instr.opname == 'LOAD_CONST':
|
|
414
|
+
keys_back.append(frame.f_code.co_consts[k_instr.arg])
|
|
415
|
+
else:
|
|
416
|
+
|
|
417
|
+
return None
|
|
418
|
+
pos -= 1
|
|
419
|
+
elif instr.opname in ('LOAD_FAST', 'LOAD_DEREF'):
|
|
420
|
+
root_var = frame.f_code.co_varnames[instr.arg]
|
|
421
|
+
break
|
|
422
|
+
else:
|
|
423
|
+
return None
|
|
424
|
+
|
|
425
|
+
if root_var is None:
|
|
426
|
+
return None
|
|
427
|
+
|
|
428
|
+
keys_back.reverse()
|
|
429
|
+
|
|
430
|
+
keys_fwd = []
|
|
431
|
+
fwd = failing_idx + 1
|
|
432
|
+
while fwd + 1 < len(instructions):
|
|
433
|
+
if (instructions[fwd].opname == 'LOAD_CONST' and
|
|
434
|
+
instructions[fwd + 1].opname == 'BINARY_SUBSCR'):
|
|
435
|
+
keys_fwd.append(frame.f_code.co_consts[instructions[fwd].arg])
|
|
436
|
+
fwd += 2
|
|
437
|
+
else:
|
|
438
|
+
break
|
|
439
|
+
|
|
440
|
+
all_keys = keys_back + keys_fwd
|
|
441
|
+
|
|
442
|
+
if len(all_keys) < 2:
|
|
443
|
+
return None
|
|
444
|
+
|
|
445
|
+
last_instr_idx = failing_idx + len(keys_fwd) * 2
|
|
446
|
+
final_key = all_keys[-1]
|
|
447
|
+
default = _infer_default(final_key if isinstance(final_key, str) else '', instructions, last_instr_idx)
|
|
448
|
+
|
|
449
|
+
return PatchSpec('chain_subscript_guard', var_name=root_var,
|
|
450
|
+
default_value=default, key_name=tuple(all_keys))
|
|
451
|
+
|
|
452
|
+
def _index_bound_spec(frame):
|
|
453
|
+
instructions = list(dis.get_instructions(frame.f_code))
|
|
454
|
+
tgt = _find_target_instr(instructions, frame.f_lasti)
|
|
455
|
+
if tgt is None or tgt.opname != 'BINARY_SUBSCR':
|
|
456
|
+
return None
|
|
457
|
+
idx_instr = instructions.index(tgt)
|
|
458
|
+
loads = []
|
|
459
|
+
for i in range(idx_instr-1, -1, -1):
|
|
460
|
+
if instructions[i].opname in ('LOAD_FAST', 'LOAD_DEREF'):
|
|
461
|
+
loads.append(frame.f_code.co_varnames[instructions[i].arg])
|
|
462
|
+
if len(loads) == 2:
|
|
463
|
+
break
|
|
464
|
+
if len(loads) < 2:
|
|
465
|
+
return None
|
|
466
|
+
list_var, idx_var = loads[1], loads[0]
|
|
467
|
+
return PatchSpec('index_guard', idx_var, 0, list_len_var=list_var)
|
|
468
|
+
|
|
469
|
+
def _dict_get_spec(frame, msg):
|
|
470
|
+
match = re.search(r"KeyError\: '(\w+)'", msg)
|
|
471
|
+
if not match:
|
|
472
|
+
match = re.search(r"'(\w+)'", msg)
|
|
473
|
+
if not match:
|
|
474
|
+
return None
|
|
475
|
+
key = match.group(1)
|
|
476
|
+
instructions = list(dis.get_instructions(frame.f_code))
|
|
477
|
+
tgt = _find_target_instr(instructions, frame.f_lasti)
|
|
478
|
+
if tgt is None or tgt.opname != 'BINARY_SUBSCR':
|
|
479
|
+
return None
|
|
480
|
+
idx = instructions.index(tgt)
|
|
481
|
+
for i in range(idx-1, -1, -1):
|
|
482
|
+
if instructions[i].opname in ('LOAD_FAST', 'LOAD_DEREF'):
|
|
483
|
+
dict_var = frame.f_code.co_varnames[instructions[i].arg]
|
|
484
|
+
return PatchSpec('key_guard', dict_var, _infer_default(key, instructions, idx), key_name=key)
|
|
485
|
+
return None
|
|
486
|
+
|
|
487
|
+
def _str_concat_spec(frame, msg):
|
|
488
|
+
instructions = list(dis.get_instructions(frame.f_code))
|
|
489
|
+
tgt = _find_target_instr(instructions, frame.f_lasti)
|
|
490
|
+
if tgt and (tgt.opname == 'BINARY_ADD' or (tgt.opname == 'BINARY_OP' and tgt.arg == 0)):
|
|
491
|
+
idx = instructions.index(tgt)
|
|
492
|
+
if idx > 0 and instructions[idx-1].opname in ('LOAD_FAST', 'LOAD_DEREF'):
|
|
493
|
+
var = frame.f_code.co_varnames[instructions[idx-1].arg]
|
|
494
|
+
return PatchSpec('str_coerce_guard', var, "")
|
|
495
|
+
return None
|
|
496
|
+
|
|
497
|
+
def _file_guard_spec(frame):
|
|
498
|
+
instructions = list(dis.get_instructions(frame.f_code))
|
|
499
|
+
tgt = _find_target_instr(instructions, frame.f_lasti)
|
|
500
|
+
if tgt is None:
|
|
501
|
+
return None
|
|
502
|
+
idx = instructions.index(tgt)
|
|
503
|
+
for i in range(idx-1, -1, -1):
|
|
504
|
+
if instructions[i].opname in ('LOAD_FAST', 'LOAD_DEREF'):
|
|
505
|
+
var = frame.f_code.co_varnames[instructions[i].arg]
|
|
506
|
+
return PatchSpec('file_guard', var, "")
|
|
507
|
+
return None
|
|
508
|
+
|
|
509
|
+
def _type_coercion_spec(frame, msg):
|
|
510
|
+
|
|
511
|
+
instructions = list(dis.get_instructions(frame.f_code))
|
|
512
|
+
tgt = _find_target_instr(instructions, frame.f_lasti)
|
|
513
|
+
if tgt is None:
|
|
514
|
+
return None
|
|
515
|
+
idx = instructions.index(tgt)
|
|
516
|
+
|
|
517
|
+
call_idx = None
|
|
518
|
+
for i in range(max(0, idx - 1), min(idx + 3, len(instructions))):
|
|
519
|
+
if instructions[i].opname == 'CALL':
|
|
520
|
+
call_idx = i
|
|
521
|
+
break
|
|
522
|
+
if call_idx is None:
|
|
523
|
+
call_idx = idx
|
|
524
|
+
|
|
525
|
+
var_name = None
|
|
526
|
+
for i in range(call_idx - 1, -1, -1):
|
|
527
|
+
instr = instructions[i]
|
|
528
|
+
if instr.opname in ('LOAD_FAST', 'LOAD_DEREF'):
|
|
529
|
+
var_name = frame.f_code.co_varnames[instr.arg]
|
|
530
|
+
break
|
|
531
|
+
if instr.opname == 'LOAD_GLOBAL':
|
|
532
|
+
continue
|
|
533
|
+
if instr.opname in ('PRECALL', 'PUSH_NULL'):
|
|
534
|
+
continue
|
|
535
|
+
break
|
|
536
|
+
|
|
537
|
+
if var_name is None:
|
|
538
|
+
return None
|
|
539
|
+
|
|
540
|
+
default = 0
|
|
541
|
+
for i in range(call_idx - 1, -1, -1):
|
|
542
|
+
instr = instructions[i]
|
|
543
|
+
if instr.opname == 'LOAD_GLOBAL':
|
|
544
|
+
name = instr.argval
|
|
545
|
+
if isinstance(name, tuple):
|
|
546
|
+
name = name[1] if len(name) > 1 else name[0]
|
|
547
|
+
if name == 'int':
|
|
548
|
+
default = 0
|
|
549
|
+
elif name == 'float':
|
|
550
|
+
default = 0.0
|
|
551
|
+
elif name == 'str':
|
|
552
|
+
default = ""
|
|
553
|
+
break
|
|
554
|
+
|
|
555
|
+
return PatchSpec('type_coercion_guard', var_name, default)
|