crosshair-tool 0.0.99__cp312-cp312-macosx_10_13_x86_64.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.
- _crosshair_tracers.cpython-312-darwin.so +0 -0
- crosshair/__init__.py +42 -0
- crosshair/__main__.py +8 -0
- crosshair/_mark_stacks.h +790 -0
- crosshair/_preliminaries_test.py +18 -0
- crosshair/_tracers.h +94 -0
- crosshair/_tracers_pycompat.h +522 -0
- crosshair/_tracers_test.py +138 -0
- crosshair/abcstring.py +245 -0
- crosshair/auditwall.py +190 -0
- crosshair/auditwall_test.py +77 -0
- crosshair/codeconfig.py +113 -0
- crosshair/codeconfig_test.py +117 -0
- crosshair/condition_parser.py +1237 -0
- crosshair/condition_parser_test.py +497 -0
- crosshair/conftest.py +30 -0
- crosshair/copyext.py +155 -0
- crosshair/copyext_test.py +84 -0
- crosshair/core.py +1763 -0
- crosshair/core_and_libs.py +149 -0
- crosshair/core_regestered_types_test.py +82 -0
- crosshair/core_test.py +1316 -0
- crosshair/diff_behavior.py +314 -0
- crosshair/diff_behavior_test.py +261 -0
- crosshair/dynamic_typing.py +346 -0
- crosshair/dynamic_typing_test.py +210 -0
- crosshair/enforce.py +282 -0
- crosshair/enforce_test.py +182 -0
- crosshair/examples/PEP316/__init__.py +1 -0
- crosshair/examples/PEP316/bugs_detected/__init__.py +0 -0
- crosshair/examples/PEP316/bugs_detected/getattr_magic.py +16 -0
- crosshair/examples/PEP316/bugs_detected/hash_consistent_with_equals.py +31 -0
- crosshair/examples/PEP316/bugs_detected/shopping_cart.py +24 -0
- crosshair/examples/PEP316/bugs_detected/showcase.py +39 -0
- crosshair/examples/PEP316/correct_code/__init__.py +0 -0
- crosshair/examples/PEP316/correct_code/arith.py +60 -0
- crosshair/examples/PEP316/correct_code/chess.py +77 -0
- crosshair/examples/PEP316/correct_code/nesting_inference.py +17 -0
- crosshair/examples/PEP316/correct_code/numpy_examples.py +132 -0
- crosshair/examples/PEP316/correct_code/rolling_average.py +35 -0
- crosshair/examples/PEP316/correct_code/showcase.py +104 -0
- crosshair/examples/__init__.py +0 -0
- crosshair/examples/check_examples_test.py +146 -0
- crosshair/examples/deal/__init__.py +1 -0
- crosshair/examples/icontract/__init__.py +1 -0
- crosshair/examples/icontract/bugs_detected/__init__.py +0 -0
- crosshair/examples/icontract/bugs_detected/showcase.py +41 -0
- crosshair/examples/icontract/bugs_detected/wrong_sign.py +8 -0
- crosshair/examples/icontract/correct_code/__init__.py +0 -0
- crosshair/examples/icontract/correct_code/arith.py +51 -0
- crosshair/examples/icontract/correct_code/showcase.py +94 -0
- crosshair/fnutil.py +391 -0
- crosshair/fnutil_test.py +75 -0
- crosshair/fuzz_core_test.py +516 -0
- crosshair/libimpl/__init__.py +0 -0
- crosshair/libimpl/arraylib.py +161 -0
- crosshair/libimpl/binascii_ch_test.py +30 -0
- crosshair/libimpl/binascii_test.py +67 -0
- crosshair/libimpl/binasciilib.py +150 -0
- crosshair/libimpl/bisectlib_test.py +23 -0
- crosshair/libimpl/builtinslib.py +5228 -0
- crosshair/libimpl/builtinslib_ch_test.py +1191 -0
- crosshair/libimpl/builtinslib_test.py +3735 -0
- crosshair/libimpl/codecslib.py +86 -0
- crosshair/libimpl/codecslib_test.py +86 -0
- crosshair/libimpl/collectionslib.py +264 -0
- crosshair/libimpl/collectionslib_ch_test.py +252 -0
- crosshair/libimpl/collectionslib_test.py +332 -0
- crosshair/libimpl/copylib.py +23 -0
- crosshair/libimpl/copylib_test.py +18 -0
- crosshair/libimpl/datetimelib.py +2559 -0
- crosshair/libimpl/datetimelib_ch_test.py +354 -0
- crosshair/libimpl/datetimelib_test.py +112 -0
- crosshair/libimpl/decimallib.py +5257 -0
- crosshair/libimpl/decimallib_ch_test.py +78 -0
- crosshair/libimpl/decimallib_test.py +76 -0
- crosshair/libimpl/encodings/__init__.py +23 -0
- crosshair/libimpl/encodings/_encutil.py +187 -0
- crosshair/libimpl/encodings/ascii.py +44 -0
- crosshair/libimpl/encodings/latin_1.py +40 -0
- crosshair/libimpl/encodings/utf_8.py +93 -0
- crosshair/libimpl/encodings_ch_test.py +83 -0
- crosshair/libimpl/fractionlib.py +16 -0
- crosshair/libimpl/fractionlib_test.py +80 -0
- crosshair/libimpl/functoolslib.py +34 -0
- crosshair/libimpl/functoolslib_test.py +56 -0
- crosshair/libimpl/hashliblib.py +30 -0
- crosshair/libimpl/hashliblib_test.py +18 -0
- crosshair/libimpl/heapqlib.py +47 -0
- crosshair/libimpl/heapqlib_test.py +21 -0
- crosshair/libimpl/importliblib.py +18 -0
- crosshair/libimpl/importliblib_test.py +38 -0
- crosshair/libimpl/iolib.py +216 -0
- crosshair/libimpl/iolib_ch_test.py +128 -0
- crosshair/libimpl/iolib_test.py +19 -0
- crosshair/libimpl/ipaddresslib.py +8 -0
- crosshair/libimpl/itertoolslib.py +44 -0
- crosshair/libimpl/itertoolslib_test.py +44 -0
- crosshair/libimpl/jsonlib.py +984 -0
- crosshair/libimpl/jsonlib_ch_test.py +42 -0
- crosshair/libimpl/jsonlib_test.py +51 -0
- crosshair/libimpl/mathlib.py +179 -0
- crosshair/libimpl/mathlib_ch_test.py +44 -0
- crosshair/libimpl/mathlib_test.py +67 -0
- crosshair/libimpl/oslib.py +7 -0
- crosshair/libimpl/pathliblib_test.py +10 -0
- crosshair/libimpl/randomlib.py +178 -0
- crosshair/libimpl/randomlib_test.py +120 -0
- crosshair/libimpl/relib.py +846 -0
- crosshair/libimpl/relib_ch_test.py +169 -0
- crosshair/libimpl/relib_test.py +493 -0
- crosshair/libimpl/timelib.py +72 -0
- crosshair/libimpl/timelib_test.py +82 -0
- crosshair/libimpl/typeslib.py +15 -0
- crosshair/libimpl/typeslib_test.py +36 -0
- crosshair/libimpl/unicodedatalib.py +75 -0
- crosshair/libimpl/unicodedatalib_test.py +42 -0
- crosshair/libimpl/urlliblib.py +23 -0
- crosshair/libimpl/urlliblib_test.py +19 -0
- crosshair/libimpl/weakreflib.py +13 -0
- crosshair/libimpl/weakreflib_test.py +69 -0
- crosshair/libimpl/zliblib.py +15 -0
- crosshair/libimpl/zliblib_test.py +13 -0
- crosshair/lsp_server.py +261 -0
- crosshair/lsp_server_test.py +30 -0
- crosshair/main.py +973 -0
- crosshair/main_test.py +543 -0
- crosshair/objectproxy.py +376 -0
- crosshair/objectproxy_test.py +41 -0
- crosshair/opcode_intercept.py +601 -0
- crosshair/opcode_intercept_test.py +304 -0
- crosshair/options.py +218 -0
- crosshair/options_test.py +10 -0
- crosshair/patch_equivalence_test.py +75 -0
- crosshair/path_cover.py +209 -0
- crosshair/path_cover_test.py +138 -0
- crosshair/path_search.py +161 -0
- crosshair/path_search_test.py +52 -0
- crosshair/pathing_oracle.py +271 -0
- crosshair/pathing_oracle_test.py +21 -0
- crosshair/pure_importer.py +27 -0
- crosshair/pure_importer_test.py +16 -0
- crosshair/py.typed +0 -0
- crosshair/register_contract.py +273 -0
- crosshair/register_contract_test.py +190 -0
- crosshair/simplestructs.py +1165 -0
- crosshair/simplestructs_test.py +283 -0
- crosshair/smtlib.py +24 -0
- crosshair/smtlib_test.py +14 -0
- crosshair/statespace.py +1199 -0
- crosshair/statespace_test.py +108 -0
- crosshair/stubs_parser.py +352 -0
- crosshair/stubs_parser_test.py +43 -0
- crosshair/test_util.py +329 -0
- crosshair/test_util_test.py +26 -0
- crosshair/tools/__init__.py +0 -0
- crosshair/tools/check_help_in_doc.py +264 -0
- crosshair/tools/check_init_and_setup_coincide.py +119 -0
- crosshair/tools/generate_demo_table.py +127 -0
- crosshair/tracers.py +544 -0
- crosshair/tracers_test.py +154 -0
- crosshair/type_repo.py +151 -0
- crosshair/unicode_categories.py +589 -0
- crosshair/unicode_categories_test.py +27 -0
- crosshair/util.py +741 -0
- crosshair/util_test.py +173 -0
- crosshair/watcher.py +307 -0
- crosshair/watcher_test.py +107 -0
- crosshair/z3util.py +76 -0
- crosshair/z3util_test.py +11 -0
- crosshair_tool-0.0.99.dist-info/METADATA +144 -0
- crosshair_tool-0.0.99.dist-info/RECORD +176 -0
- crosshair_tool-0.0.99.dist-info/WHEEL +6 -0
- crosshair_tool-0.0.99.dist-info/entry_points.txt +3 -0
- crosshair_tool-0.0.99.dist-info/licenses/LICENSE +93 -0
- crosshair_tool-0.0.99.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,1237 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import collections
|
|
3
|
+
import contextlib
|
|
4
|
+
import enum
|
|
5
|
+
import inspect
|
|
6
|
+
import re
|
|
7
|
+
import sys
|
|
8
|
+
import textwrap
|
|
9
|
+
import traceback
|
|
10
|
+
import types
|
|
11
|
+
from dataclasses import dataclass, replace
|
|
12
|
+
from functools import partial, wraps
|
|
13
|
+
from inspect import BoundArguments, Signature
|
|
14
|
+
from itertools import chain
|
|
15
|
+
from typing import (
|
|
16
|
+
Any,
|
|
17
|
+
Callable,
|
|
18
|
+
ContextManager,
|
|
19
|
+
Dict,
|
|
20
|
+
FrozenSet,
|
|
21
|
+
Iterable,
|
|
22
|
+
Iterator,
|
|
23
|
+
List,
|
|
24
|
+
Mapping,
|
|
25
|
+
MutableMapping,
|
|
26
|
+
Optional,
|
|
27
|
+
Sequence,
|
|
28
|
+
Set,
|
|
29
|
+
Tuple,
|
|
30
|
+
Type,
|
|
31
|
+
cast,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
import icontract # type: ignore
|
|
36
|
+
except ModuleNotFoundError:
|
|
37
|
+
icontract = None # type: ignore
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
import deal # type: ignore
|
|
41
|
+
except ModuleNotFoundError:
|
|
42
|
+
deal = None # type: ignore
|
|
43
|
+
|
|
44
|
+
from crosshair.auditwall import opened_auditwall
|
|
45
|
+
from crosshair.fnutil import FunctionInfo, fn_globals, set_first_arg_type
|
|
46
|
+
from crosshair.options import AnalysisKind
|
|
47
|
+
from crosshair.register_contract import get_contract
|
|
48
|
+
from crosshair.tracers import NoTracing
|
|
49
|
+
from crosshair.util import (
|
|
50
|
+
CrossHairInternal,
|
|
51
|
+
DynamicScopeVar,
|
|
52
|
+
EvalFriendlyReprContext,
|
|
53
|
+
IdKeyedDict,
|
|
54
|
+
debug,
|
|
55
|
+
eval_friendly_repr,
|
|
56
|
+
format_boundargs,
|
|
57
|
+
frame_summary_for_fn,
|
|
58
|
+
is_pure_python,
|
|
59
|
+
sourcelines,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class ConditionExprType(enum.Enum):
|
|
64
|
+
INVARIANT = "invariant"
|
|
65
|
+
PRECONDITION = "precondition"
|
|
66
|
+
POSTCONDITION = "postcondition"
|
|
67
|
+
|
|
68
|
+
def __str__(self):
|
|
69
|
+
return self.value
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# For convience
|
|
73
|
+
INVARIANT = ConditionExprType.INVARIANT
|
|
74
|
+
PRECONDITION = ConditionExprType.PRECONDITION
|
|
75
|
+
POSTCONDITION = ConditionExprType.POSTCONDITION
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class NoEnforce:
|
|
79
|
+
"""
|
|
80
|
+
Signal to suppress contract enforcement.
|
|
81
|
+
|
|
82
|
+
This function wrapper does nothing on its own. But the enforcement tracer
|
|
83
|
+
looks for it and will skip conditions on `fn` when this wrapper is detected.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
def __init__(self, fn):
|
|
87
|
+
self.fn = fn
|
|
88
|
+
|
|
89
|
+
def __call__(self, *a, **kw) -> object:
|
|
90
|
+
return self.fn(*a, **kw)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def strip_comment_line(line: str) -> str:
|
|
94
|
+
line = line.strip()
|
|
95
|
+
if line.startswith("'''") or line.startswith('"""'):
|
|
96
|
+
line = line[3:]
|
|
97
|
+
if line.endswith("'''") or line.endswith('"""'):
|
|
98
|
+
line = line[:-3]
|
|
99
|
+
return line.strip()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def get_doc_lines(thing: object) -> Iterable[Tuple[int, str]]:
|
|
103
|
+
_filename, line_num, lines = sourcelines(thing) # type:ignore
|
|
104
|
+
if not lines:
|
|
105
|
+
return
|
|
106
|
+
try:
|
|
107
|
+
module = ast.parse(textwrap.dedent("".join(lines)))
|
|
108
|
+
except SyntaxError:
|
|
109
|
+
debug(f"Unable to parse {thing} into an AST; will not detect PEP316 contracts.")
|
|
110
|
+
return
|
|
111
|
+
fndef = module.body[0]
|
|
112
|
+
if not isinstance(fndef, (ast.ClassDef, ast.FunctionDef)):
|
|
113
|
+
return
|
|
114
|
+
firstnode = fndef.body[0]
|
|
115
|
+
if not isinstance(firstnode, ast.Expr):
|
|
116
|
+
return
|
|
117
|
+
strnode = firstnode.value
|
|
118
|
+
if not (isinstance(strnode, ast.Constant) and isinstance(strnode.value, str)):
|
|
119
|
+
return
|
|
120
|
+
end_lineno = getattr(strnode, "end_lineno", None)
|
|
121
|
+
if end_lineno is not None:
|
|
122
|
+
candidates = enumerate(lines[strnode.lineno - 1 : end_lineno])
|
|
123
|
+
line_num += strnode.lineno - 1
|
|
124
|
+
else:
|
|
125
|
+
candidates = enumerate(lines[: strnode.lineno + 1])
|
|
126
|
+
OPEN_RE = re.compile("^\\s*r?('''|\"\"\")")
|
|
127
|
+
CLOSE_RE = re.compile("('''|\"\"\")\\s*(#.*)?$")
|
|
128
|
+
started = False
|
|
129
|
+
for idx, line in candidates:
|
|
130
|
+
if not started:
|
|
131
|
+
(line, replaced) = OPEN_RE.subn("", line)
|
|
132
|
+
if replaced:
|
|
133
|
+
started = True
|
|
134
|
+
if started:
|
|
135
|
+
(line, replaced) = CLOSE_RE.subn("", line)
|
|
136
|
+
yield (line_num + idx, line)
|
|
137
|
+
if replaced:
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class ImpliesTransformer(ast.NodeTransformer):
|
|
142
|
+
"""
|
|
143
|
+
Transform AST to rewrite implies operation.
|
|
144
|
+
|
|
145
|
+
Pre- and post-conditions commonly want an implies(X, Y) operation.
|
|
146
|
+
But it's important to only evaluate Y when X is true; so we rewrite
|
|
147
|
+
this function into "Y if X else True"
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
def visit_Call(self, node):
|
|
151
|
+
self.generic_visit(node)
|
|
152
|
+
if isinstance(node.func, ast.Name) and node.func.id == "implies":
|
|
153
|
+
if len(node.args) != 2:
|
|
154
|
+
raise SyntaxError("implies() must have exactly two arguments")
|
|
155
|
+
condition, implication = node.args
|
|
156
|
+
pos = {"lineno": node.lineno, "col_offset": node.col_offset}
|
|
157
|
+
return ast.IfExp(condition, implication, ast.Constant(True, **pos), **pos)
|
|
158
|
+
return node
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def compile_expr(src: str) -> types.CodeType:
|
|
162
|
+
parsed = ast.parse(src, "<string>", "eval")
|
|
163
|
+
parsed = ImpliesTransformer().visit(parsed)
|
|
164
|
+
return compile(parsed, "<string>", "eval")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def default_counterexample(
|
|
168
|
+
fn_name: str,
|
|
169
|
+
bound_args: BoundArguments,
|
|
170
|
+
return_val: object,
|
|
171
|
+
repr_overrides: IdKeyedDict,
|
|
172
|
+
) -> Tuple[str, str]:
|
|
173
|
+
from crosshair.tracers import ResumedTracing
|
|
174
|
+
|
|
175
|
+
with ResumedTracing(), EvalFriendlyReprContext(repr_overrides) as ctx:
|
|
176
|
+
args_string = format_boundargs(bound_args)
|
|
177
|
+
call_desc = f"{fn_name}({ctx.cleanup(args_string)})"
|
|
178
|
+
return (call_desc, eval_friendly_repr(return_val))
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@dataclass()
|
|
182
|
+
class ConditionSyntaxMessage:
|
|
183
|
+
filename: str
|
|
184
|
+
line_num: int
|
|
185
|
+
message: str
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@dataclass
|
|
189
|
+
class ConditionExpr:
|
|
190
|
+
condition_type: ConditionExprType
|
|
191
|
+
evaluate: Optional[Callable[[Mapping[str, object]], bool]]
|
|
192
|
+
filename: str
|
|
193
|
+
line: int
|
|
194
|
+
expr_source: str
|
|
195
|
+
compile_err: Optional[ConditionSyntaxMessage] = None
|
|
196
|
+
|
|
197
|
+
def __repr__(self):
|
|
198
|
+
return (
|
|
199
|
+
f"ConditionExpr(filename={self.filename!r}, "
|
|
200
|
+
f"line={self.line!r}, "
|
|
201
|
+
f"expr_source={self.expr_source!r}, "
|
|
202
|
+
f"compile_err={self.compile_err!r})"
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@dataclass(frozen=True)
|
|
207
|
+
class Conditions:
|
|
208
|
+
"""Describe the contract of a function."""
|
|
209
|
+
|
|
210
|
+
fn: Callable
|
|
211
|
+
"""
|
|
212
|
+
The body of the function to analyze.
|
|
213
|
+
Ideally, this is just the body of the function and does not include checking
|
|
214
|
+
pre- or post-conditions. (though this is not always possible)
|
|
215
|
+
"""
|
|
216
|
+
|
|
217
|
+
src_fn: Callable
|
|
218
|
+
"""
|
|
219
|
+
The body of the function to use for error reporting. Usually the same as
|
|
220
|
+
`fn`, but sometimes the original is wrapped in shell for exception handling
|
|
221
|
+
or other reasons.
|
|
222
|
+
"""
|
|
223
|
+
|
|
224
|
+
pre: Sequence[ConditionExpr]
|
|
225
|
+
""" The preconditions of the function. """
|
|
226
|
+
|
|
227
|
+
post: Sequence[ConditionExpr]
|
|
228
|
+
""" The postconditions of the function. """
|
|
229
|
+
|
|
230
|
+
raises: FrozenSet[Type[BaseException]]
|
|
231
|
+
"""
|
|
232
|
+
A set of expection types that are expected.
|
|
233
|
+
Subtypes of expected exceptions are also considered to be expected.
|
|
234
|
+
CrossHair will attempt to report when this function raises an
|
|
235
|
+
unexpected exception.
|
|
236
|
+
"""
|
|
237
|
+
|
|
238
|
+
sig: inspect.Signature
|
|
239
|
+
"""
|
|
240
|
+
The signature of the funtion. Argument and return type
|
|
241
|
+
annotations should be resolved to real python types when possible.
|
|
242
|
+
"""
|
|
243
|
+
|
|
244
|
+
# TODO: can mutation checking be implemented as just another kind of postcondition?
|
|
245
|
+
mutable_args: Optional[FrozenSet[str]]
|
|
246
|
+
"""
|
|
247
|
+
A set of arguments that are deeply immutable.
|
|
248
|
+
When None, no assertion about mutability is provided.
|
|
249
|
+
OTOH, an empty set asserts that the function does not mutate any argument.
|
|
250
|
+
"""
|
|
251
|
+
|
|
252
|
+
fn_syntax_messages: Sequence[ConditionSyntaxMessage]
|
|
253
|
+
"""
|
|
254
|
+
A list of errors resulting from the parsing of the contract.
|
|
255
|
+
In general, conditions should not be checked when such messages exist.
|
|
256
|
+
"""
|
|
257
|
+
|
|
258
|
+
counterexample_description_maker: Optional[
|
|
259
|
+
Callable[[BoundArguments, object, IdKeyedDict], Tuple[str, str]]
|
|
260
|
+
] = None
|
|
261
|
+
"""
|
|
262
|
+
An optional callback that formats a counterexample invocation as text.
|
|
263
|
+
It takes the example arguments and the returned value.
|
|
264
|
+
It returns string representations of the invocation and return value.
|
|
265
|
+
"""
|
|
266
|
+
|
|
267
|
+
def has_any(self) -> bool:
|
|
268
|
+
return bool(self.pre or self.post or self.fn_syntax_messages)
|
|
269
|
+
|
|
270
|
+
def syntax_messages(self) -> Iterator[ConditionSyntaxMessage]:
|
|
271
|
+
for cond in chain(self.pre, self.post):
|
|
272
|
+
if cond.compile_err is not None:
|
|
273
|
+
yield cond.compile_err
|
|
274
|
+
yield from self.fn_syntax_messages
|
|
275
|
+
|
|
276
|
+
def format_counterexample(
|
|
277
|
+
self, args: BoundArguments, return_val: object, repr_overrides: IdKeyedDict
|
|
278
|
+
) -> Tuple[str, str]:
|
|
279
|
+
if self.counterexample_description_maker is not None:
|
|
280
|
+
return self.counterexample_description_maker(
|
|
281
|
+
args, return_val, repr_overrides
|
|
282
|
+
)
|
|
283
|
+
return default_counterexample(
|
|
284
|
+
self.src_fn.__name__, args, return_val, repr_overrides
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
@dataclass(frozen=True)
|
|
289
|
+
class ClassConditions:
|
|
290
|
+
inv: List[ConditionExpr]
|
|
291
|
+
"""
|
|
292
|
+
Invariants declared explicitly on the class.
|
|
293
|
+
Does not include invariants of superclasses.
|
|
294
|
+
"""
|
|
295
|
+
|
|
296
|
+
methods: Mapping[str, Conditions]
|
|
297
|
+
"""
|
|
298
|
+
Maps member names to the conditions for that member.
|
|
299
|
+
|
|
300
|
+
Conditions reflect not only what's directly declared to the method, but also:
|
|
301
|
+
* Conditions from superclass implementations of the same method.
|
|
302
|
+
* Conditions inferred from class invariants.
|
|
303
|
+
* Conditions inferred from superclass invariants.
|
|
304
|
+
"""
|
|
305
|
+
|
|
306
|
+
def has_any(self) -> bool:
|
|
307
|
+
return bool(self.inv) or any(c.has_any() for c in self.methods.values())
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def merge_fn_conditions(
|
|
311
|
+
sub_conditions: Conditions, super_conditions: Conditions
|
|
312
|
+
) -> Conditions:
|
|
313
|
+
|
|
314
|
+
# TODO: resolve the warning below:
|
|
315
|
+
# (1) the type of self always changes
|
|
316
|
+
# (2) paramter renames (or *a, **kws) could result in varied bindings
|
|
317
|
+
if sub_conditions.sig is not None and sub_conditions.sig != super_conditions.sig:
|
|
318
|
+
debug(
|
|
319
|
+
"WARNING: inconsistent signatures", sub_conditions.sig, super_conditions.sig
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
pre = sub_conditions.pre if sub_conditions.pre else super_conditions.pre
|
|
323
|
+
post = list(chain(super_conditions.post, sub_conditions.post))
|
|
324
|
+
raises = sub_conditions.raises | super_conditions.raises
|
|
325
|
+
mutable_args = (
|
|
326
|
+
sub_conditions.mutable_args
|
|
327
|
+
if sub_conditions.mutable_args is not None
|
|
328
|
+
else super_conditions.mutable_args
|
|
329
|
+
)
|
|
330
|
+
fn = sub_conditions.fn
|
|
331
|
+
return Conditions(
|
|
332
|
+
fn,
|
|
333
|
+
fn,
|
|
334
|
+
pre,
|
|
335
|
+
post,
|
|
336
|
+
raises,
|
|
337
|
+
sub_conditions.sig,
|
|
338
|
+
mutable_args,
|
|
339
|
+
sub_conditions.fn_syntax_messages,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def merge_method_conditions(
|
|
344
|
+
class_conditions: List[ClassConditions],
|
|
345
|
+
) -> Dict[str, Conditions]:
|
|
346
|
+
methods: Dict[str, Conditions] = {}
|
|
347
|
+
# reverse because mro searches left side first
|
|
348
|
+
for class_condition in reversed(class_conditions):
|
|
349
|
+
methods.update(class_condition.methods)
|
|
350
|
+
return methods
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
_HEADER_LINE = re.compile(
|
|
354
|
+
r"""^(\s*)\:? # whitespace with optional leading colon
|
|
355
|
+
((?:post)|(?:pre)|(?:raises)|(?:inv)) # noncapturing keywords
|
|
356
|
+
(?:\[([\w\s\,\.]*)\])? # optional params in square brackets
|
|
357
|
+
\:\:?\s* # single or double colons
|
|
358
|
+
(.*?) # The (non-greedy) content
|
|
359
|
+
\s*$""",
|
|
360
|
+
re.VERBOSE,
|
|
361
|
+
)
|
|
362
|
+
_SECTION_LINE = re.compile(r"^(\s*)(.*?)\s*$")
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
@dataclass(init=False)
|
|
366
|
+
class SectionParse:
|
|
367
|
+
syntax_messages: List[ConditionSyntaxMessage]
|
|
368
|
+
sections: Dict[str, List[Tuple[int, str]]]
|
|
369
|
+
mutable_expr: Optional[str] = None
|
|
370
|
+
|
|
371
|
+
def __init__(self):
|
|
372
|
+
self.sections = collections.defaultdict(list)
|
|
373
|
+
self.syntax_messages = []
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def has_expr(line: str) -> bool:
|
|
377
|
+
line = line.strip()
|
|
378
|
+
return bool(line) and not line.startswith("#")
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def parse_sections(
|
|
382
|
+
lines: List[Tuple[int, str]], sections: Tuple[str, ...], filename: str
|
|
383
|
+
) -> SectionParse:
|
|
384
|
+
parse = SectionParse()
|
|
385
|
+
cur_section: Optional[Tuple[str, int]] = None
|
|
386
|
+
for line_num, line in lines:
|
|
387
|
+
if line.strip() == "":
|
|
388
|
+
continue
|
|
389
|
+
if cur_section:
|
|
390
|
+
section, indent = cur_section
|
|
391
|
+
match = _SECTION_LINE.match(line)
|
|
392
|
+
if match:
|
|
393
|
+
this_indent = len(match.groups()[0])
|
|
394
|
+
if this_indent > indent:
|
|
395
|
+
if has_expr(match.groups()[1]):
|
|
396
|
+
parse.sections[section].append((line_num, match.groups()[1]))
|
|
397
|
+
# Still in the current section; continue:
|
|
398
|
+
continue
|
|
399
|
+
cur_section = None
|
|
400
|
+
match = _HEADER_LINE.match(line)
|
|
401
|
+
if match:
|
|
402
|
+
indentstr, section, bracketed, inline_expr = match.groups()
|
|
403
|
+
if section not in sections:
|
|
404
|
+
continue
|
|
405
|
+
if bracketed is not None:
|
|
406
|
+
if section != "post":
|
|
407
|
+
parse.syntax_messages.append(
|
|
408
|
+
ConditionSyntaxMessage(
|
|
409
|
+
filename,
|
|
410
|
+
line_num,
|
|
411
|
+
f"brackets not allowed in {section} section",
|
|
412
|
+
)
|
|
413
|
+
)
|
|
414
|
+
continue
|
|
415
|
+
if parse.mutable_expr is not None:
|
|
416
|
+
parse.syntax_messages.append(
|
|
417
|
+
ConditionSyntaxMessage(
|
|
418
|
+
filename, line_num, f"duplicate post section"
|
|
419
|
+
)
|
|
420
|
+
)
|
|
421
|
+
continue
|
|
422
|
+
else:
|
|
423
|
+
parse.mutable_expr = bracketed
|
|
424
|
+
if has_expr(inline_expr):
|
|
425
|
+
parse.sections[section].append((line_num, inline_expr))
|
|
426
|
+
continue
|
|
427
|
+
else:
|
|
428
|
+
cur_section = (section, len(indentstr))
|
|
429
|
+
return parse
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
class ConditionParser:
|
|
433
|
+
def get_fn_conditions(self, fn: FunctionInfo) -> Optional[Conditions]:
|
|
434
|
+
"""
|
|
435
|
+
Return conditions declared (directly) on a function.
|
|
436
|
+
|
|
437
|
+
Does not include conditions inferred from invariants or superclasses.
|
|
438
|
+
Return None if it is impossible for this method to have conditions, even if
|
|
439
|
+
gained via subclass invariants. (i.e. `fn` is not a function or has no
|
|
440
|
+
signature)
|
|
441
|
+
"""
|
|
442
|
+
raise NotImplementedError
|
|
443
|
+
|
|
444
|
+
def get_class_conditions(self, cls: type) -> ClassConditions:
|
|
445
|
+
raise NotImplementedError
|
|
446
|
+
|
|
447
|
+
def class_can_have_conditions(self, cls: type) -> bool:
|
|
448
|
+
raise NotImplementedError
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
class ConcreteConditionParser(ConditionParser):
|
|
452
|
+
def __init__(self, toplevel_parser: Optional[ConditionParser] = None):
|
|
453
|
+
if toplevel_parser is None:
|
|
454
|
+
toplevel_parser = self
|
|
455
|
+
self._toplevel_parser = toplevel_parser
|
|
456
|
+
|
|
457
|
+
def get_toplevel_parser(self):
|
|
458
|
+
return self._toplevel_parser
|
|
459
|
+
|
|
460
|
+
def get_class_invariants(self, cls: type) -> List[ConditionExpr]:
|
|
461
|
+
"""
|
|
462
|
+
Return invariants declared explicitly on the given class.
|
|
463
|
+
|
|
464
|
+
Does not include invarants of superclasses.
|
|
465
|
+
"""
|
|
466
|
+
raise NotImplementedError
|
|
467
|
+
|
|
468
|
+
def class_can_have_conditions(self, cls: type) -> bool:
|
|
469
|
+
# We can't get conditions/line numbers for classes written in C.
|
|
470
|
+
return is_pure_python(cls)
|
|
471
|
+
|
|
472
|
+
def get_class_conditions(self, cls: type) -> ClassConditions:
|
|
473
|
+
if not self.class_can_have_conditions(cls):
|
|
474
|
+
return ClassConditions([], {})
|
|
475
|
+
|
|
476
|
+
toplevel_parser = self.get_toplevel_parser()
|
|
477
|
+
methods = {}
|
|
478
|
+
super_methods = merge_method_conditions(
|
|
479
|
+
[toplevel_parser.get_class_conditions(base) for base in cls.__bases__]
|
|
480
|
+
)
|
|
481
|
+
inv = self.get_class_invariants(cls)
|
|
482
|
+
# TODO: consider the case where superclass defines methods w/o contracts and
|
|
483
|
+
# then subclass adds an invariant.
|
|
484
|
+
method_names = set(cls.__dict__.keys()) | super_methods.keys()
|
|
485
|
+
for method_name in method_names:
|
|
486
|
+
method = cls.__dict__.get(method_name, None)
|
|
487
|
+
super_method_conditions = super_methods.get(method_name)
|
|
488
|
+
if super_method_conditions is not None:
|
|
489
|
+
# Re-type the super's `self` argument to be this class:
|
|
490
|
+
revised_sig = set_first_arg_type(super_method_conditions.sig, cls)
|
|
491
|
+
super_method_conditions = replace(
|
|
492
|
+
super_method_conditions, sig=revised_sig
|
|
493
|
+
)
|
|
494
|
+
if method is None:
|
|
495
|
+
if super_method_conditions is None:
|
|
496
|
+
continue
|
|
497
|
+
else:
|
|
498
|
+
conditions: Conditions = super_method_conditions
|
|
499
|
+
else:
|
|
500
|
+
parsed_conditions = toplevel_parser.get_fn_conditions(
|
|
501
|
+
FunctionInfo.from_class(cls, method_name)
|
|
502
|
+
)
|
|
503
|
+
if parsed_conditions is None:
|
|
504
|
+
# debug(f'Skipping "{method_name}": Unable to determine the function signature.')
|
|
505
|
+
continue
|
|
506
|
+
if super_method_conditions is None:
|
|
507
|
+
conditions = parsed_conditions
|
|
508
|
+
else:
|
|
509
|
+
conditions = merge_fn_conditions(
|
|
510
|
+
parsed_conditions, super_method_conditions
|
|
511
|
+
)
|
|
512
|
+
# Selectively add conditions inferred from invariants:
|
|
513
|
+
final_pre = list(conditions.pre)
|
|
514
|
+
final_post = list(conditions.post)
|
|
515
|
+
if method_name in (
|
|
516
|
+
"__new__", # a staticmethod, but not isinstance(staticmethod)
|
|
517
|
+
"__repr__", # is itself required for reporting problems with invariants.
|
|
518
|
+
# [set/del]attr can do anything; we can't resonably enforce invariants:
|
|
519
|
+
"__setattr__",
|
|
520
|
+
"__delattr__",
|
|
521
|
+
"__replace__", # Will raise an exception with most arbitrary **kwargs.
|
|
522
|
+
"__annotate__", # a staticmethod, but not isinstance(staticmethod)
|
|
523
|
+
"__annotate_func__",
|
|
524
|
+
):
|
|
525
|
+
pass
|
|
526
|
+
elif method_name == "__del__":
|
|
527
|
+
final_pre.extend(inv)
|
|
528
|
+
elif method_name == "__init__":
|
|
529
|
+
final_post.extend(inv)
|
|
530
|
+
elif method_name.startswith("__") and method_name.endswith("__"):
|
|
531
|
+
final_pre.extend(inv)
|
|
532
|
+
final_post.extend(inv)
|
|
533
|
+
elif method_name.startswith("_"):
|
|
534
|
+
pass
|
|
535
|
+
else:
|
|
536
|
+
final_pre.extend(inv)
|
|
537
|
+
final_post.extend(inv)
|
|
538
|
+
conditions = replace(conditions, pre=final_pre, post=final_post)
|
|
539
|
+
if conditions.has_any():
|
|
540
|
+
methods[method_name] = conditions
|
|
541
|
+
|
|
542
|
+
if inv and "__init__" not in methods:
|
|
543
|
+
# We assume that the default methods on `object` won't break invariants.
|
|
544
|
+
# Except `__init__`! That's what this conditional is for.
|
|
545
|
+
|
|
546
|
+
# Note that we don't check contracts on __init__ directly (but we do check
|
|
547
|
+
# them in while checking other contracts). Therefore, we're a little loose
|
|
548
|
+
# with the paramters (like signature) because many of them don't really
|
|
549
|
+
# matter.
|
|
550
|
+
initfn = getattr(cls, "__init__")
|
|
551
|
+
init_sig = inspect.signature(initfn)
|
|
552
|
+
methods["__init__"] = Conditions(
|
|
553
|
+
initfn, initfn, [], inv[:], frozenset(), init_sig, None, [], None
|
|
554
|
+
)
|
|
555
|
+
return ClassConditions(inv, methods)
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
class CompositeConditionParser(ConditionParser):
|
|
559
|
+
def __init__(self):
|
|
560
|
+
self.parsers = []
|
|
561
|
+
self.class_cache: Dict[type, ClassConditions] = {}
|
|
562
|
+
|
|
563
|
+
def get_toplevel_parser(self) -> ConditionParser:
|
|
564
|
+
return self
|
|
565
|
+
|
|
566
|
+
def get_fn_conditions(self, fn: FunctionInfo) -> Optional[Conditions]:
|
|
567
|
+
ret = None
|
|
568
|
+
for parser in self.parsers:
|
|
569
|
+
conditions = parser.get_fn_conditions(fn)
|
|
570
|
+
if conditions is not None:
|
|
571
|
+
ret = conditions
|
|
572
|
+
if conditions.has_any():
|
|
573
|
+
break
|
|
574
|
+
return ret
|
|
575
|
+
|
|
576
|
+
def get_class_conditions(self, cls: type) -> ClassConditions:
|
|
577
|
+
cached_ret = self.class_cache.get(cls)
|
|
578
|
+
if cached_ret is not None:
|
|
579
|
+
return cached_ret
|
|
580
|
+
ret = ClassConditions([], {})
|
|
581
|
+
# We skip the "typing" module because class condition computation fails for some
|
|
582
|
+
# typing classes:
|
|
583
|
+
if cls.__module__ != "typing":
|
|
584
|
+
for parser in self.parsers:
|
|
585
|
+
conditions = parser.get_class_conditions(cls)
|
|
586
|
+
if conditions.has_any():
|
|
587
|
+
ret = conditions
|
|
588
|
+
break
|
|
589
|
+
self.class_cache[cls] = ret
|
|
590
|
+
return ret
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def condition_from_source_text(
|
|
594
|
+
condition_type: ConditionExprType,
|
|
595
|
+
filename: str,
|
|
596
|
+
line: int,
|
|
597
|
+
expr_source: str,
|
|
598
|
+
namespace: Dict[str, object],
|
|
599
|
+
) -> ConditionExpr:
|
|
600
|
+
evaluate, compile_err = None, None
|
|
601
|
+
try:
|
|
602
|
+
compiled = compile_expr(expr_source)
|
|
603
|
+
|
|
604
|
+
def evaluatefn(bindings: Mapping[str, object]) -> bool:
|
|
605
|
+
# TODO: eval() is oddly expensive when tracing is on.
|
|
606
|
+
# Consider eval()ing this as an entire function.
|
|
607
|
+
return eval(compiled, {**namespace, **bindings})
|
|
608
|
+
|
|
609
|
+
evaluate = evaluatefn
|
|
610
|
+
except Exception:
|
|
611
|
+
e = sys.exc_info()[1]
|
|
612
|
+
compile_err = ConditionSyntaxMessage(filename, line, str(e))
|
|
613
|
+
return ConditionExpr(
|
|
614
|
+
condition_type=condition_type,
|
|
615
|
+
filename=filename,
|
|
616
|
+
line=line,
|
|
617
|
+
expr_source=expr_source,
|
|
618
|
+
evaluate=evaluate,
|
|
619
|
+
compile_err=compile_err,
|
|
620
|
+
)
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
_RAISE_SPHINX_RE = re.compile(
|
|
624
|
+
r"""
|
|
625
|
+
(?: ^ \s* \: raises \s+ ( [\w\.]+ ) \: ) |
|
|
626
|
+
(?: ^ \s* \:? raises \s* \: ( [^\r\n#]+ ) )
|
|
627
|
+
""",
|
|
628
|
+
re.MULTILINE | re.VERBOSE,
|
|
629
|
+
)
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def parse_sphinx_raises(fn: Callable) -> Set[Type[BaseException]]:
|
|
633
|
+
raises: Set[Type[BaseException]] = set()
|
|
634
|
+
doc = getattr(fn, "__doc__", None)
|
|
635
|
+
if doc is None:
|
|
636
|
+
return raises
|
|
637
|
+
for group1, group2 in _RAISE_SPHINX_RE.findall(doc):
|
|
638
|
+
if group1:
|
|
639
|
+
excnamelist = [group1]
|
|
640
|
+
else:
|
|
641
|
+
excnamelist = group2.split(",")
|
|
642
|
+
for excname in excnamelist:
|
|
643
|
+
try:
|
|
644
|
+
exc_type = eval(excname, fn_globals(fn))
|
|
645
|
+
except Exception as e:
|
|
646
|
+
continue
|
|
647
|
+
if not isinstance(exc_type, type):
|
|
648
|
+
continue
|
|
649
|
+
if not issubclass(exc_type, BaseException):
|
|
650
|
+
continue
|
|
651
|
+
raises.add(exc_type)
|
|
652
|
+
return raises
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
class Pep316Parser(ConcreteConditionParser):
|
|
656
|
+
def get_fn_conditions(self, ctxfn: FunctionInfo) -> Optional[Conditions]:
|
|
657
|
+
fn_and_sig = ctxfn.get_callable()
|
|
658
|
+
if fn_and_sig is None:
|
|
659
|
+
return None
|
|
660
|
+
(fn, sig) = fn_and_sig
|
|
661
|
+
filename, first_fn_lineno, _lines = sourcelines(fn)
|
|
662
|
+
if isinstance(fn, types.BuiltinFunctionType):
|
|
663
|
+
return Conditions(fn, fn, [], [], frozenset(), sig, frozenset(), [])
|
|
664
|
+
lines = list(get_doc_lines(fn))
|
|
665
|
+
parse = parse_sections(lines, ("pre", "post"), filename)
|
|
666
|
+
pre: List[ConditionExpr] = []
|
|
667
|
+
post_conditions: List[ConditionExpr] = []
|
|
668
|
+
mutable_args: Optional[FrozenSet[str]] = None
|
|
669
|
+
if parse.mutable_expr is not None:
|
|
670
|
+
mutable_args = frozenset(
|
|
671
|
+
expr.strip().split(".")[0]
|
|
672
|
+
for expr in parse.mutable_expr.split(",")
|
|
673
|
+
if expr != ""
|
|
674
|
+
)
|
|
675
|
+
for line_num, expr in parse.sections["pre"]:
|
|
676
|
+
pre.append(
|
|
677
|
+
condition_from_source_text(
|
|
678
|
+
PRECONDITION,
|
|
679
|
+
filename,
|
|
680
|
+
line_num,
|
|
681
|
+
expr,
|
|
682
|
+
fn_globals(fn),
|
|
683
|
+
)
|
|
684
|
+
)
|
|
685
|
+
for line_num, expr in parse.sections["post"]:
|
|
686
|
+
post_conditions.append(
|
|
687
|
+
condition_from_source_text(
|
|
688
|
+
POSTCONDITION,
|
|
689
|
+
filename,
|
|
690
|
+
line_num,
|
|
691
|
+
expr,
|
|
692
|
+
fn_globals(fn),
|
|
693
|
+
)
|
|
694
|
+
)
|
|
695
|
+
if pre and not post_conditions:
|
|
696
|
+
post_conditions.append(
|
|
697
|
+
ConditionExpr(
|
|
698
|
+
POSTCONDITION, lambda vars: True, filename, first_fn_lineno, ""
|
|
699
|
+
)
|
|
700
|
+
)
|
|
701
|
+
return Conditions(
|
|
702
|
+
fn,
|
|
703
|
+
fn,
|
|
704
|
+
pre,
|
|
705
|
+
post_conditions,
|
|
706
|
+
frozenset(parse_sphinx_raises(fn)),
|
|
707
|
+
sig,
|
|
708
|
+
mutable_args,
|
|
709
|
+
parse.syntax_messages,
|
|
710
|
+
)
|
|
711
|
+
|
|
712
|
+
def get_class_invariants(self, cls: type) -> List[ConditionExpr]:
|
|
713
|
+
try:
|
|
714
|
+
filename = inspect.getsourcefile(cls)
|
|
715
|
+
except TypeError: # raises TypeError for builtins
|
|
716
|
+
filename = None
|
|
717
|
+
if filename is None:
|
|
718
|
+
return []
|
|
719
|
+
namespace = sys.modules[cls.__module__].__dict__
|
|
720
|
+
|
|
721
|
+
parse = parse_sections(list(get_doc_lines(cls)), ("inv",), filename)
|
|
722
|
+
inv = []
|
|
723
|
+
for line_num, line in parse.sections["inv"]:
|
|
724
|
+
inv.append(
|
|
725
|
+
condition_from_source_text(
|
|
726
|
+
INVARIANT, filename, line_num, line, namespace
|
|
727
|
+
)
|
|
728
|
+
)
|
|
729
|
+
return inv
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
class IcontractParser(ConcreteConditionParser):
|
|
733
|
+
def __init__(self, toplevel_parser: Optional[ConditionParser] = None):
|
|
734
|
+
super().__init__(toplevel_parser)
|
|
735
|
+
|
|
736
|
+
def contract_text(self, contract) -> str:
|
|
737
|
+
ls = icontract._represent.inspect_lambda_condition(condition=contract.condition)
|
|
738
|
+
return ls.text if ls else ""
|
|
739
|
+
|
|
740
|
+
def get_fn_conditions(self, ctxfn: FunctionInfo) -> Optional[Conditions]:
|
|
741
|
+
if icontract is None:
|
|
742
|
+
return None
|
|
743
|
+
fn_and_sig = ctxfn.get_callable()
|
|
744
|
+
if fn_and_sig is None:
|
|
745
|
+
return None
|
|
746
|
+
(fn, sig) = fn_and_sig
|
|
747
|
+
|
|
748
|
+
checker = icontract._checkers.find_checker(func=fn) # type: ignore
|
|
749
|
+
contractless_fn = fn # type: ignore
|
|
750
|
+
while (
|
|
751
|
+
hasattr(contractless_fn, "__is_invariant_check__")
|
|
752
|
+
or hasattr(contractless_fn, "__preconditions__")
|
|
753
|
+
or hasattr(contractless_fn, "__postconditions__")
|
|
754
|
+
):
|
|
755
|
+
contractless_fn = contractless_fn.__wrapped__ # type: ignore
|
|
756
|
+
if checker is None:
|
|
757
|
+
return Conditions(
|
|
758
|
+
contractless_fn, contractless_fn, [], [], frozenset(), sig, None, []
|
|
759
|
+
)
|
|
760
|
+
|
|
761
|
+
pre: List[ConditionExpr] = []
|
|
762
|
+
post: List[ConditionExpr] = []
|
|
763
|
+
|
|
764
|
+
def eval_contract(contract, kwargs: Mapping) -> bool:
|
|
765
|
+
condition_kwargs = icontract._checkers.select_condition_kwargs(
|
|
766
|
+
contract=contract, resolved_kwargs=kwargs
|
|
767
|
+
)
|
|
768
|
+
return contract.condition(**condition_kwargs)
|
|
769
|
+
|
|
770
|
+
disjunction = checker.__preconditions__ # type: ignore
|
|
771
|
+
if len(disjunction) == 0:
|
|
772
|
+
pass
|
|
773
|
+
elif len(disjunction) == 1:
|
|
774
|
+
for contract in disjunction[0]:
|
|
775
|
+
evalfn = partial(eval_contract, contract)
|
|
776
|
+
filename, line_num, _lines = sourcelines(contract.condition)
|
|
777
|
+
pre.append(
|
|
778
|
+
ConditionExpr(
|
|
779
|
+
PRECONDITION,
|
|
780
|
+
evalfn,
|
|
781
|
+
filename,
|
|
782
|
+
line_num,
|
|
783
|
+
self.contract_text(contract),
|
|
784
|
+
)
|
|
785
|
+
)
|
|
786
|
+
else:
|
|
787
|
+
|
|
788
|
+
def eval_disjunction(disjunction, kwargs: Mapping) -> bool:
|
|
789
|
+
for conjunction in disjunction:
|
|
790
|
+
ok = True
|
|
791
|
+
for contract in conjunction:
|
|
792
|
+
if not eval_contract(contract, kwargs):
|
|
793
|
+
ok = False
|
|
794
|
+
break
|
|
795
|
+
if ok:
|
|
796
|
+
return True
|
|
797
|
+
return False
|
|
798
|
+
|
|
799
|
+
evalfn = partial(eval_disjunction, disjunction)
|
|
800
|
+
filename, line_num, _lines = sourcelines(contractless_fn)
|
|
801
|
+
source = (
|
|
802
|
+
"("
|
|
803
|
+
+ ") or (".join(
|
|
804
|
+
[
|
|
805
|
+
" and ".join([self.contract_text(c) for c in conj])
|
|
806
|
+
for conj in disjunction
|
|
807
|
+
]
|
|
808
|
+
)
|
|
809
|
+
+ ")"
|
|
810
|
+
)
|
|
811
|
+
pre.append(ConditionExpr(PRECONDITION, evalfn, filename, line_num, source))
|
|
812
|
+
|
|
813
|
+
snapshots = checker.__postcondition_snapshots__ # type: ignore
|
|
814
|
+
|
|
815
|
+
def take_snapshots(**kwargs):
|
|
816
|
+
old_as_mapping: MutableMapping[str, Any] = {}
|
|
817
|
+
for snap in snapshots:
|
|
818
|
+
snap_kwargs = icontract._checkers.select_capture_kwargs(
|
|
819
|
+
a_snapshot=snap, resolved_kwargs=kwargs
|
|
820
|
+
)
|
|
821
|
+
old_as_mapping[snap.name] = snap.capture(**snap_kwargs)
|
|
822
|
+
return icontract._checkers.Old(mapping=old_as_mapping)
|
|
823
|
+
|
|
824
|
+
def post_eval(contract, orig_kwargs: Mapping) -> bool:
|
|
825
|
+
kwargs = dict(orig_kwargs)
|
|
826
|
+
_old = kwargs.pop("__old__")
|
|
827
|
+
kwargs["OLD"] = take_snapshots(**_old.__dict__)
|
|
828
|
+
kwargs["result"] = kwargs.pop("__return__")
|
|
829
|
+
del kwargs["_"]
|
|
830
|
+
condition_kwargs = icontract._checkers.select_condition_kwargs(
|
|
831
|
+
contract=contract, resolved_kwargs=kwargs
|
|
832
|
+
)
|
|
833
|
+
return contract.condition(**condition_kwargs)
|
|
834
|
+
|
|
835
|
+
for postcondition in checker.__postconditions__: # type: ignore
|
|
836
|
+
evalfn = partial(post_eval, postcondition)
|
|
837
|
+
filename, line_num, _lines = sourcelines(postcondition.condition)
|
|
838
|
+
post.append(
|
|
839
|
+
ConditionExpr(
|
|
840
|
+
POSTCONDITION,
|
|
841
|
+
evalfn,
|
|
842
|
+
filename,
|
|
843
|
+
line_num,
|
|
844
|
+
self.contract_text(postcondition),
|
|
845
|
+
)
|
|
846
|
+
)
|
|
847
|
+
if pre and not post:
|
|
848
|
+
filename, line_num, _lines = sourcelines(contractless_fn)
|
|
849
|
+
post.append(
|
|
850
|
+
ConditionExpr(POSTCONDITION, lambda vars: True, filename, line_num, "")
|
|
851
|
+
)
|
|
852
|
+
return Conditions(
|
|
853
|
+
contractless_fn,
|
|
854
|
+
contractless_fn,
|
|
855
|
+
pre,
|
|
856
|
+
post,
|
|
857
|
+
raises=frozenset(parse_sphinx_raises(fn)),
|
|
858
|
+
sig=sig,
|
|
859
|
+
mutable_args=None,
|
|
860
|
+
fn_syntax_messages=[],
|
|
861
|
+
)
|
|
862
|
+
|
|
863
|
+
def get_class_invariants(self, cls: type) -> List[ConditionExpr]:
|
|
864
|
+
invariants = getattr(cls, "__invariants__", ()) # type: ignore
|
|
865
|
+
ret = []
|
|
866
|
+
|
|
867
|
+
def inv_eval(contract, kwargs):
|
|
868
|
+
return contract.condition(self=kwargs["self"])
|
|
869
|
+
|
|
870
|
+
for contract in invariants:
|
|
871
|
+
filename, line_num, _lines = sourcelines(contract.condition)
|
|
872
|
+
ret.append(
|
|
873
|
+
ConditionExpr(
|
|
874
|
+
INVARIANT,
|
|
875
|
+
partial(inv_eval, contract),
|
|
876
|
+
filename,
|
|
877
|
+
line_num,
|
|
878
|
+
self.contract_text(contract),
|
|
879
|
+
)
|
|
880
|
+
)
|
|
881
|
+
return ret
|
|
882
|
+
|
|
883
|
+
|
|
884
|
+
_DEALL_MARKERS_TO_SKIP = frozenset(
|
|
885
|
+
[
|
|
886
|
+
# NOTE: These are (re-)enumerated in kinds_of_contracts.rst
|
|
887
|
+
# TODO: Make this list customizable?
|
|
888
|
+
"write",
|
|
889
|
+
"network",
|
|
890
|
+
"stdin",
|
|
891
|
+
"syscall",
|
|
892
|
+
]
|
|
893
|
+
)
|
|
894
|
+
|
|
895
|
+
|
|
896
|
+
class DealParser(ConcreteConditionParser):
|
|
897
|
+
def _contract_validates(
|
|
898
|
+
self,
|
|
899
|
+
contract: "deal.introspection.ValidatedContract",
|
|
900
|
+
args: Sequence,
|
|
901
|
+
kwargs: Mapping[str, object],
|
|
902
|
+
) -> bool:
|
|
903
|
+
try:
|
|
904
|
+
contract.validate(*args, **kwargs)
|
|
905
|
+
return True
|
|
906
|
+
except contract.exception_type:
|
|
907
|
+
return False
|
|
908
|
+
|
|
909
|
+
def _extract_a_and_kw(
|
|
910
|
+
self, bindings: Mapping[str, object], sig: Signature
|
|
911
|
+
) -> Tuple[List[object], Dict[str, object]]:
|
|
912
|
+
positional_args = []
|
|
913
|
+
keyword_args = {}
|
|
914
|
+
for param in sig.parameters.values():
|
|
915
|
+
if param.kind == inspect.Parameter.KEYWORD_ONLY:
|
|
916
|
+
keyword_args[param.name] = bindings[param.name]
|
|
917
|
+
positional_args.append(bindings[param.name])
|
|
918
|
+
return (positional_args, keyword_args)
|
|
919
|
+
|
|
920
|
+
def _make_pre_expr(
|
|
921
|
+
self, contract: "deal.introspection.Pre", sig: Signature
|
|
922
|
+
) -> Callable[[Mapping[str, object]], bool]:
|
|
923
|
+
def evaluatefn(bindings: Mapping[str, object]) -> bool:
|
|
924
|
+
args, kwargs = self._extract_a_and_kw(bindings, sig)
|
|
925
|
+
return self._contract_validates(contract, args, kwargs)
|
|
926
|
+
|
|
927
|
+
return evaluatefn
|
|
928
|
+
|
|
929
|
+
def _make_post_expr(
|
|
930
|
+
self, contract: "deal.introspection.Post", sig: Signature
|
|
931
|
+
) -> Callable[[Mapping[str, object]], bool]:
|
|
932
|
+
return lambda b: self._contract_validates(contract, (b["__return__"],), {})
|
|
933
|
+
|
|
934
|
+
def _make_ensure_expr(
|
|
935
|
+
self, contract: "deal.introspection.Ensure", sig: Signature
|
|
936
|
+
) -> Callable[[Mapping[str, object]], bool]:
|
|
937
|
+
def evaluatefn(bindings: Mapping[str, object]) -> bool:
|
|
938
|
+
args, kwargs = self._extract_a_and_kw(bindings, sig)
|
|
939
|
+
kwargs["result"] = bindings["__return__"]
|
|
940
|
+
return self._contract_validates(contract, args, kwargs)
|
|
941
|
+
|
|
942
|
+
return evaluatefn
|
|
943
|
+
|
|
944
|
+
def get_fn_conditions(self, ctxfn: FunctionInfo) -> Optional[Conditions]:
|
|
945
|
+
if deal is None:
|
|
946
|
+
return None
|
|
947
|
+
fn_and_sig = ctxfn.get_callable()
|
|
948
|
+
if fn_and_sig is None:
|
|
949
|
+
return None
|
|
950
|
+
(fn, sig) = fn_and_sig
|
|
951
|
+
|
|
952
|
+
contracts = list(deal.introspection.get_contracts(fn))
|
|
953
|
+
if not contracts:
|
|
954
|
+
return None
|
|
955
|
+
deal.introspection.init_all(fn)
|
|
956
|
+
|
|
957
|
+
pre: List[ConditionExpr] = []
|
|
958
|
+
post: List[ConditionExpr] = []
|
|
959
|
+
exceptions: List[Type[Exception]] = []
|
|
960
|
+
for contract in contracts:
|
|
961
|
+
if isinstance(contract, deal.introspection.Raises):
|
|
962
|
+
exceptions.extend(contract.exceptions)
|
|
963
|
+
continue
|
|
964
|
+
if isinstance(contract, deal.introspection.Has):
|
|
965
|
+
for marker in contract.markers:
|
|
966
|
+
if marker in _DEALL_MARKERS_TO_SKIP:
|
|
967
|
+
debug(
|
|
968
|
+
f"Skipping analysis of {fn.__name__} because it is marked with '{marker}'"
|
|
969
|
+
)
|
|
970
|
+
return None
|
|
971
|
+
if not isinstance(contract, deal.introspection.ValidatedContract):
|
|
972
|
+
continue
|
|
973
|
+
fname, lineno, _lines = sourcelines(fn)
|
|
974
|
+
exprsrc = contract.source
|
|
975
|
+
if isinstance(contract, deal.introspection.Pre):
|
|
976
|
+
expr = self._make_pre_expr(contract, sig)
|
|
977
|
+
pre.append(ConditionExpr(PRECONDITION, expr, fname, lineno, exprsrc))
|
|
978
|
+
elif isinstance(contract, deal.introspection.Post):
|
|
979
|
+
expr = self._make_post_expr(contract, sig)
|
|
980
|
+
post.append(ConditionExpr(POSTCONDITION, expr, fname, lineno, exprsrc))
|
|
981
|
+
elif isinstance(contract, deal.introspection.Ensure):
|
|
982
|
+
expr = self._make_ensure_expr(contract, sig)
|
|
983
|
+
post.append(ConditionExpr(POSTCONDITION, expr, fname, lineno, exprsrc))
|
|
984
|
+
|
|
985
|
+
if pre and not post:
|
|
986
|
+
filename, line_num, _lines = sourcelines(fn)
|
|
987
|
+
post.append(
|
|
988
|
+
ConditionExpr(POSTCONDITION, lambda vars: True, filename, line_num, "")
|
|
989
|
+
)
|
|
990
|
+
raw_fn = deal.introspection.unwrap(fn)
|
|
991
|
+
return Conditions(
|
|
992
|
+
fn=raw_fn,
|
|
993
|
+
src_fn=raw_fn,
|
|
994
|
+
pre=pre,
|
|
995
|
+
post=post,
|
|
996
|
+
raises=frozenset(exceptions),
|
|
997
|
+
sig=sig,
|
|
998
|
+
mutable_args=None,
|
|
999
|
+
fn_syntax_messages=[],
|
|
1000
|
+
)
|
|
1001
|
+
|
|
1002
|
+
def get_class_invariants(self, cls: type) -> List[ConditionExpr]:
|
|
1003
|
+
return []
|
|
1004
|
+
|
|
1005
|
+
|
|
1006
|
+
class AssertsParser(ConcreteConditionParser):
|
|
1007
|
+
def __init__(self, toplevel_parser: Optional[ConditionParser] = None):
|
|
1008
|
+
super().__init__(toplevel_parser)
|
|
1009
|
+
|
|
1010
|
+
@staticmethod
|
|
1011
|
+
def is_string_literal(node: ast.AST) -> bool:
|
|
1012
|
+
if sys.version_info >= (3, 8):
|
|
1013
|
+
return (
|
|
1014
|
+
isinstance(node, ast.Expr)
|
|
1015
|
+
and isinstance(node.value, ast.Constant)
|
|
1016
|
+
and isinstance(node.value.value, str)
|
|
1017
|
+
)
|
|
1018
|
+
else:
|
|
1019
|
+
return isinstance(node, ast.Expr) and isinstance(node.value, ast.Str)
|
|
1020
|
+
|
|
1021
|
+
@staticmethod
|
|
1022
|
+
def get_first_body_line(fn: Callable) -> Optional[int]:
|
|
1023
|
+
"""
|
|
1024
|
+
Retrieve the first line of the body of the function ``fn``.
|
|
1025
|
+
|
|
1026
|
+
:return:
|
|
1027
|
+
the line number of the first non-assert statement in the given function.
|
|
1028
|
+
|
|
1029
|
+
:return:
|
|
1030
|
+
None if the function does not start with at least one assert statement.
|
|
1031
|
+
"""
|
|
1032
|
+
_filename, first_fn_lineno, lines = sourcelines(fn)
|
|
1033
|
+
if not lines:
|
|
1034
|
+
return None
|
|
1035
|
+
ast_module = ast.parse(textwrap.dedent("".join(lines)))
|
|
1036
|
+
ast_fn = ast_module.body[0]
|
|
1037
|
+
if not isinstance(ast_fn, ast.FunctionDef):
|
|
1038
|
+
return None
|
|
1039
|
+
found_any_preconditions = False
|
|
1040
|
+
for statement in ast_fn.body:
|
|
1041
|
+
if isinstance(statement, ast.Assert):
|
|
1042
|
+
found_any_preconditions = True
|
|
1043
|
+
continue
|
|
1044
|
+
elif AssertsParser.is_string_literal(statement):
|
|
1045
|
+
# A docstring, keep looking:
|
|
1046
|
+
continue
|
|
1047
|
+
break
|
|
1048
|
+
if found_any_preconditions:
|
|
1049
|
+
return first_fn_lineno + (statement.lineno - 1)
|
|
1050
|
+
else:
|
|
1051
|
+
return None
|
|
1052
|
+
|
|
1053
|
+
def get_fn_conditions(self, ctxfn: FunctionInfo) -> Optional[Conditions]:
|
|
1054
|
+
fn_and_sig = ctxfn.get_callable()
|
|
1055
|
+
if fn_and_sig is None:
|
|
1056
|
+
return None
|
|
1057
|
+
(fn, sig) = fn_and_sig
|
|
1058
|
+
# TODO replace this guard with package-level configuration?
|
|
1059
|
+
if (
|
|
1060
|
+
getattr(fn, "__module__", False)
|
|
1061
|
+
and fn.__module__.startswith("crosshair.")
|
|
1062
|
+
and not fn.__module__.endswith("_test")
|
|
1063
|
+
):
|
|
1064
|
+
return None
|
|
1065
|
+
try:
|
|
1066
|
+
first_body_line = AssertsParser.get_first_body_line(fn)
|
|
1067
|
+
except OSError:
|
|
1068
|
+
return None
|
|
1069
|
+
if first_body_line is None:
|
|
1070
|
+
return None
|
|
1071
|
+
|
|
1072
|
+
filename, first_line, _lines = sourcelines(fn)
|
|
1073
|
+
|
|
1074
|
+
@wraps(fn)
|
|
1075
|
+
def wrappedfn(*a, **kw):
|
|
1076
|
+
try:
|
|
1077
|
+
return NoEnforce(fn)(*a, **kw)
|
|
1078
|
+
except AssertionError as e:
|
|
1079
|
+
# TODO: check that this isn't failing at an early line in a different
|
|
1080
|
+
# file?
|
|
1081
|
+
_, lineno = frame_summary_for_fn(
|
|
1082
|
+
fn, traceback.extract_tb(e.__traceback__)
|
|
1083
|
+
)
|
|
1084
|
+
if lineno >= first_body_line:
|
|
1085
|
+
raise
|
|
1086
|
+
|
|
1087
|
+
post = [
|
|
1088
|
+
ConditionExpr(
|
|
1089
|
+
POSTCONDITION,
|
|
1090
|
+
lambda _: True,
|
|
1091
|
+
filename,
|
|
1092
|
+
first_line,
|
|
1093
|
+
"",
|
|
1094
|
+
)
|
|
1095
|
+
]
|
|
1096
|
+
return Conditions(
|
|
1097
|
+
wrappedfn,
|
|
1098
|
+
fn,
|
|
1099
|
+
[], # (pre)
|
|
1100
|
+
post,
|
|
1101
|
+
raises=frozenset(parse_sphinx_raises(fn)),
|
|
1102
|
+
sig=sig,
|
|
1103
|
+
mutable_args=None,
|
|
1104
|
+
fn_syntax_messages=[],
|
|
1105
|
+
)
|
|
1106
|
+
|
|
1107
|
+
def get_class_invariants(self, cls: type) -> List[ConditionExpr]:
|
|
1108
|
+
return []
|
|
1109
|
+
|
|
1110
|
+
|
|
1111
|
+
class RegisteredContractsParser(ConcreteConditionParser):
|
|
1112
|
+
"""Parser for manually registered contracts."""
|
|
1113
|
+
|
|
1114
|
+
def __init__(self, toplevel_parser: Optional[ConditionParser] = None):
|
|
1115
|
+
super().__init__(toplevel_parser)
|
|
1116
|
+
|
|
1117
|
+
def get_fn_conditions(self, ctxfn: FunctionInfo) -> Optional[Conditions]:
|
|
1118
|
+
fn_and_sig = ctxfn.get_callable()
|
|
1119
|
+
if fn_and_sig is not None:
|
|
1120
|
+
(fn, sig) = fn_and_sig
|
|
1121
|
+
sigs = [sig]
|
|
1122
|
+
contract = get_contract(fn)
|
|
1123
|
+
if not contract:
|
|
1124
|
+
return None
|
|
1125
|
+
else:
|
|
1126
|
+
# ctxfn.get_callable() returns None if no signature was found
|
|
1127
|
+
desc = ctxfn.descriptor
|
|
1128
|
+
if isinstance(desc, Callable): # type: ignore
|
|
1129
|
+
fn = cast(Callable, desc)
|
|
1130
|
+
contract = get_contract(fn)
|
|
1131
|
+
# Ensure we have at least one signature
|
|
1132
|
+
if not contract or not contract.sigs:
|
|
1133
|
+
return None
|
|
1134
|
+
sigs = contract.sigs
|
|
1135
|
+
else:
|
|
1136
|
+
return None
|
|
1137
|
+
|
|
1138
|
+
# Signatures registered in contracts have higher precedence
|
|
1139
|
+
if contract.sigs:
|
|
1140
|
+
sigs = contract.sigs
|
|
1141
|
+
pre: List[ConditionExpr] = []
|
|
1142
|
+
post: List[ConditionExpr] = []
|
|
1143
|
+
|
|
1144
|
+
filename, line_num, _lines = sourcelines(fn)
|
|
1145
|
+
|
|
1146
|
+
if contract.pre:
|
|
1147
|
+
pre_cond = contract.pre
|
|
1148
|
+
|
|
1149
|
+
def evaluatefn(kwargs: Mapping):
|
|
1150
|
+
kwargs = dict(kwargs)
|
|
1151
|
+
pre_args = inspect.signature(pre_cond).parameters.keys()
|
|
1152
|
+
new_kwargs = {arg: kwargs[arg] for arg in pre_args}
|
|
1153
|
+
return pre_cond(**new_kwargs)
|
|
1154
|
+
|
|
1155
|
+
pre.append(
|
|
1156
|
+
ConditionExpr(
|
|
1157
|
+
PRECONDITION,
|
|
1158
|
+
evaluatefn,
|
|
1159
|
+
filename,
|
|
1160
|
+
line_num,
|
|
1161
|
+
inspect.getsource(pre_cond),
|
|
1162
|
+
)
|
|
1163
|
+
)
|
|
1164
|
+
if contract.post:
|
|
1165
|
+
post_cond = contract.post
|
|
1166
|
+
|
|
1167
|
+
def post_eval(orig_kwargs: Mapping) -> bool:
|
|
1168
|
+
kwargs = dict(orig_kwargs)
|
|
1169
|
+
post_args = inspect.signature(post_cond).parameters.keys()
|
|
1170
|
+
new_kwargs = {arg: kwargs[arg] for arg in post_args}
|
|
1171
|
+
return post_cond(**new_kwargs)
|
|
1172
|
+
|
|
1173
|
+
post.append(
|
|
1174
|
+
ConditionExpr(
|
|
1175
|
+
POSTCONDITION,
|
|
1176
|
+
post_eval,
|
|
1177
|
+
filename,
|
|
1178
|
+
line_num,
|
|
1179
|
+
inspect.getsource(post_cond),
|
|
1180
|
+
)
|
|
1181
|
+
)
|
|
1182
|
+
else:
|
|
1183
|
+
# Ensure at least one postcondition to allow short-circuiting the body.
|
|
1184
|
+
post.append(
|
|
1185
|
+
ConditionExpr(POSTCONDITION, lambda vars: True, filename, line_num, "")
|
|
1186
|
+
)
|
|
1187
|
+
return Conditions(
|
|
1188
|
+
fn,
|
|
1189
|
+
fn,
|
|
1190
|
+
pre,
|
|
1191
|
+
post,
|
|
1192
|
+
raises=frozenset(parse_sphinx_raises(fn)),
|
|
1193
|
+
sig=sigs[0], # TODO: in the future, should return all sigs.
|
|
1194
|
+
mutable_args=None,
|
|
1195
|
+
fn_syntax_messages=[],
|
|
1196
|
+
)
|
|
1197
|
+
|
|
1198
|
+
def class_can_have_conditions(self, cls: type) -> bool:
|
|
1199
|
+
# We might have registered contracts for classes written in C, so we don't want
|
|
1200
|
+
# to skip evaluating conditions on the class methods.
|
|
1201
|
+
return True
|
|
1202
|
+
|
|
1203
|
+
def get_class_invariants(self, cls: type) -> List[ConditionExpr]:
|
|
1204
|
+
# TODO: Should we add a way of registering class invariants?
|
|
1205
|
+
return []
|
|
1206
|
+
|
|
1207
|
+
|
|
1208
|
+
_PARSER_MAP = {
|
|
1209
|
+
AnalysisKind.asserts: AssertsParser,
|
|
1210
|
+
AnalysisKind.PEP316: Pep316Parser,
|
|
1211
|
+
AnalysisKind.icontract: IcontractParser,
|
|
1212
|
+
AnalysisKind.deal: DealParser,
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
|
|
1216
|
+
# Condition parsers may be needed at various places in the stack.
|
|
1217
|
+
# We configure them through the use of a magic threadlocal value:
|
|
1218
|
+
_CALLTREE_PARSER = DynamicScopeVar(ConditionParser, "calltree parser")
|
|
1219
|
+
|
|
1220
|
+
|
|
1221
|
+
def condition_parser(
|
|
1222
|
+
analysis_kinds: Sequence[AnalysisKind],
|
|
1223
|
+
) -> ContextManager[ConditionParser]:
|
|
1224
|
+
current = _CALLTREE_PARSER.get_if_in_scope()
|
|
1225
|
+
if current is not None:
|
|
1226
|
+
return contextlib.nullcontext(current)
|
|
1227
|
+
debug("Using parsers: ", analysis_kinds)
|
|
1228
|
+
condition_parser = CompositeConditionParser()
|
|
1229
|
+
condition_parser.parsers.extend(
|
|
1230
|
+
_PARSER_MAP[k](condition_parser) for k in analysis_kinds
|
|
1231
|
+
)
|
|
1232
|
+
condition_parser.parsers.append(RegisteredContractsParser(condition_parser))
|
|
1233
|
+
return _CALLTREE_PARSER.open(condition_parser)
|
|
1234
|
+
|
|
1235
|
+
|
|
1236
|
+
def get_current_parser() -> ConditionParser:
|
|
1237
|
+
return _CALLTREE_PARSER.get()
|