invar-tools 1.17.26__py3-none-any.whl → 1.17.27__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.
- invar/core/contracts.py +38 -6
- invar/core/doc_edit.py +22 -9
- invar/core/doc_parser.py +17 -11
- invar/core/entry_points.py +48 -42
- invar/core/extraction.py +25 -9
- invar/core/format_specs.py +7 -2
- invar/core/format_strategies.py +6 -4
- invar/core/formatter.py +9 -6
- invar/core/hypothesis_strategies.py +24 -5
- invar/core/lambda_helpers.py +2 -2
- invar/core/parser.py +40 -21
- invar/core/patterns/detector.py +25 -6
- invar/core/patterns/p0_exhaustive.py +5 -5
- invar/core/patterns/p0_literal.py +8 -8
- invar/core/patterns/p0_newtype.py +2 -2
- invar/core/patterns/p0_nonempty.py +12 -7
- invar/core/patterns/p0_validation.py +4 -8
- invar/core/patterns/registry.py +12 -2
- invar/core/property_gen.py +47 -23
- invar/core/shell_analysis.py +70 -66
- invar/core/strategies.py +14 -3
- invar/core/suggestions.py +12 -4
- invar/core/tautology.py +33 -10
- invar/core/template_parser.py +23 -15
- invar/core/ts_parsers.py +6 -2
- invar/core/ts_sig_parser.py +18 -10
- invar/core/utils.py +20 -9
- invar/templates/protocol/python/tools.md +3 -0
- {invar_tools-1.17.26.dist-info → invar_tools-1.17.27.dist-info}/METADATA +1 -1
- {invar_tools-1.17.26.dist-info → invar_tools-1.17.27.dist-info}/RECORD +35 -35
- {invar_tools-1.17.26.dist-info → invar_tools-1.17.27.dist-info}/WHEEL +0 -0
- {invar_tools-1.17.26.dist-info → invar_tools-1.17.27.dist-info}/entry_points.txt +0 -0
- {invar_tools-1.17.26.dist-info → invar_tools-1.17.27.dist-info}/licenses/LICENSE +0 -0
- {invar_tools-1.17.26.dist-info → invar_tools-1.17.27.dist-info}/licenses/LICENSE-GPL +0 -0
- {invar_tools-1.17.26.dist-info → invar_tools-1.17.27.dist-info}/licenses/NOTICE +0 -0
invar/core/parser.py
CHANGED
|
@@ -21,7 +21,11 @@ from invar.core.purity import (
|
|
|
21
21
|
)
|
|
22
22
|
|
|
23
23
|
|
|
24
|
-
@pre(
|
|
24
|
+
@pre(
|
|
25
|
+
lambda source, path="<string>": isinstance(source, str)
|
|
26
|
+
and len(source.strip()) > 0
|
|
27
|
+
and isinstance(path, str)
|
|
28
|
+
)
|
|
25
29
|
def parse_source(source: str, path: str = "<string>") -> FileInfo | None:
|
|
26
30
|
"""
|
|
27
31
|
Parse Python source code and extract symbols.
|
|
@@ -64,7 +68,7 @@ def parse_source(source: str, path: str = "<string>") -> FileInfo | None:
|
|
|
64
68
|
)
|
|
65
69
|
|
|
66
70
|
|
|
67
|
-
@pre(lambda tree: isinstance(tree, ast.Module) and hasattr(tree,
|
|
71
|
+
@pre(lambda tree: isinstance(tree, ast.Module) and hasattr(tree, "body"))
|
|
68
72
|
def _extract_symbols(tree: ast.Module) -> list[Symbol]:
|
|
69
73
|
"""
|
|
70
74
|
Extract function, class, and method symbols from AST.
|
|
@@ -95,10 +99,14 @@ def _extract_symbols(tree: ast.Module) -> list[Symbol]:
|
|
|
95
99
|
return symbols
|
|
96
100
|
|
|
97
101
|
|
|
98
|
-
@pre(
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
)
|
|
102
|
+
@pre(
|
|
103
|
+
lambda node: (
|
|
104
|
+
isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef)
|
|
105
|
+
and hasattr(node, "name")
|
|
106
|
+
and hasattr(node, "args")
|
|
107
|
+
and hasattr(node, "lineno")
|
|
108
|
+
)
|
|
109
|
+
)
|
|
102
110
|
@post(lambda result: result.kind == SymbolKind.FUNCTION)
|
|
103
111
|
def _parse_function(node: ast.FunctionDef | ast.AsyncFunctionDef) -> Symbol:
|
|
104
112
|
"""Parse a function definition into a Symbol."""
|
|
@@ -129,10 +137,15 @@ def _parse_function(node: ast.FunctionDef | ast.AsyncFunctionDef) -> Symbol:
|
|
|
129
137
|
)
|
|
130
138
|
|
|
131
139
|
|
|
132
|
-
@pre(
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
)
|
|
140
|
+
@pre(
|
|
141
|
+
lambda node, class_name: (
|
|
142
|
+
isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef)
|
|
143
|
+
and hasattr(node, "name")
|
|
144
|
+
and hasattr(node, "args")
|
|
145
|
+
and hasattr(node, "lineno")
|
|
146
|
+
and len(class_name) > 0
|
|
147
|
+
)
|
|
148
|
+
)
|
|
136
149
|
@post(lambda result: result.kind == SymbolKind.METHOD)
|
|
137
150
|
def _parse_method(node: ast.FunctionDef | ast.AsyncFunctionDef, class_name: str) -> Symbol:
|
|
138
151
|
"""
|
|
@@ -175,7 +188,11 @@ def _parse_method(node: ast.FunctionDef | ast.AsyncFunctionDef, class_name: str)
|
|
|
175
188
|
)
|
|
176
189
|
|
|
177
190
|
|
|
178
|
-
@pre(
|
|
191
|
+
@pre(
|
|
192
|
+
lambda node: isinstance(node, ast.ClassDef)
|
|
193
|
+
and hasattr(node, "name")
|
|
194
|
+
and hasattr(node, "lineno")
|
|
195
|
+
)
|
|
179
196
|
@post(lambda result: result.kind == SymbolKind.CLASS)
|
|
180
197
|
def _parse_class(node: ast.ClassDef) -> Symbol:
|
|
181
198
|
"""Parse a class definition into a Symbol."""
|
|
@@ -190,10 +207,11 @@ def _parse_class(node: ast.ClassDef) -> Symbol:
|
|
|
190
207
|
)
|
|
191
208
|
|
|
192
209
|
|
|
193
|
-
@pre(
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
)
|
|
210
|
+
@pre(
|
|
211
|
+
lambda node: (
|
|
212
|
+
isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef) and hasattr(node, "decorator_list")
|
|
213
|
+
)
|
|
214
|
+
)
|
|
197
215
|
@post(lambda result: all(c.kind in ("pre", "post") for c in result))
|
|
198
216
|
def _extract_contracts(node: ast.FunctionDef | ast.AsyncFunctionDef) -> list[Contract]:
|
|
199
217
|
"""Extract @pre and @post contracts from function decorators."""
|
|
@@ -233,7 +251,7 @@ def _parse_decorator_as_contract(decorator: ast.expr) -> Contract | None:
|
|
|
233
251
|
return None
|
|
234
252
|
|
|
235
253
|
|
|
236
|
-
@pre(lambda call: isinstance(call, ast.Call) and hasattr(call,
|
|
254
|
+
@pre(lambda call: isinstance(call, ast.Call) and hasattr(call, "args"))
|
|
237
255
|
def _get_contract_expression(call: ast.Call) -> str:
|
|
238
256
|
"""Extract the expression string from a contract decorator call."""
|
|
239
257
|
if call.args:
|
|
@@ -241,10 +259,11 @@ def _get_contract_expression(call: ast.Call) -> str:
|
|
|
241
259
|
return ""
|
|
242
260
|
|
|
243
261
|
|
|
244
|
-
@pre(
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
)
|
|
262
|
+
@pre(
|
|
263
|
+
lambda node: (
|
|
264
|
+
isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef) and hasattr(node, "args")
|
|
265
|
+
)
|
|
266
|
+
)
|
|
248
267
|
@post(lambda result: result.startswith("(") and ")" in result)
|
|
249
268
|
def _build_signature(node: ast.FunctionDef | ast.AsyncFunctionDef) -> str:
|
|
250
269
|
"""Build a signature string from function arguments."""
|
|
@@ -267,7 +286,7 @@ def _build_signature(node: ast.FunctionDef | ast.AsyncFunctionDef) -> str:
|
|
|
267
286
|
return sig
|
|
268
287
|
|
|
269
288
|
|
|
270
|
-
@pre(lambda tree: isinstance(tree, ast.Module) and hasattr(tree,
|
|
289
|
+
@pre(lambda tree: isinstance(tree, ast.Module) and hasattr(tree, "body"))
|
|
271
290
|
@post(lambda result: all(isinstance(s, str) and s for s in result))
|
|
272
291
|
def _extract_imports(tree: ast.Module) -> list[str]:
|
|
273
292
|
"""Extract imported module names from AST (top-level only)."""
|
invar/core/patterns/detector.py
CHANGED
|
@@ -147,11 +147,9 @@ class BaseDetector:
|
|
|
147
147
|
result = ast.unparse(annotation)
|
|
148
148
|
return result if result else "<unknown>"
|
|
149
149
|
|
|
150
|
-
@pre(lambda self, params, type_name: len(type_name) > 0)
|
|
150
|
+
@pre(lambda self, params, type_name: isinstance(params, list) and len(type_name) > 0)
|
|
151
151
|
@post(lambda result: result >= 0)
|
|
152
|
-
def count_type_occurrences(
|
|
153
|
-
self, params: list[tuple[str, str | None]], type_name: str
|
|
154
|
-
) -> int:
|
|
152
|
+
def count_type_occurrences(self, params: list[tuple[str, str | None]], type_name: str) -> int:
|
|
155
153
|
"""
|
|
156
154
|
Count how many parameters have a specific type.
|
|
157
155
|
|
|
@@ -203,12 +201,33 @@ class BaseDetector:
|
|
|
203
201
|
for case in match_node.cases:
|
|
204
202
|
pattern = case.pattern
|
|
205
203
|
if isinstance(pattern, ast.MatchValue):
|
|
206
|
-
cases.append(
|
|
204
|
+
cases.append(
|
|
205
|
+
ast.unparse(pattern.value) if hasattr(ast, "unparse") else str(pattern.value)
|
|
206
|
+
)
|
|
207
207
|
elif isinstance(pattern, ast.MatchAs) and pattern.pattern is None:
|
|
208
208
|
cases.append("_") # Wildcard
|
|
209
209
|
return cases
|
|
210
210
|
|
|
211
|
-
@pre(
|
|
211
|
+
@pre(
|
|
212
|
+
lambda self,
|
|
213
|
+
pattern_id,
|
|
214
|
+
priority,
|
|
215
|
+
file_path,
|
|
216
|
+
line,
|
|
217
|
+
message,
|
|
218
|
+
current_code,
|
|
219
|
+
suggested_pattern,
|
|
220
|
+
confidence,
|
|
221
|
+
reference_pattern: pattern_id in PatternID
|
|
222
|
+
and priority in Priority
|
|
223
|
+
and confidence in Confidence
|
|
224
|
+
and line > 0
|
|
225
|
+
and len(file_path) > 0
|
|
226
|
+
and len(message) > 0
|
|
227
|
+
and len(current_code) > 0
|
|
228
|
+
and len(suggested_pattern) > 0
|
|
229
|
+
and len(reference_pattern) > 0
|
|
230
|
+
)
|
|
212
231
|
@post(lambda result: result.reference_file == ".invar/examples/functional.py")
|
|
213
232
|
def make_suggestion(
|
|
214
233
|
self,
|
|
@@ -106,10 +106,8 @@ class ExhaustiveMatchDetector(BaseDetector):
|
|
|
106
106
|
|
|
107
107
|
return suggestions
|
|
108
108
|
|
|
109
|
-
@pre(lambda self, node, file_path: len(file_path) > 0)
|
|
110
|
-
def _check_match(
|
|
111
|
-
self, node: ast.Match, file_path: str
|
|
112
|
-
) -> PatternSuggestion | None:
|
|
109
|
+
@pre(lambda self, node, file_path: node is not None and len(file_path) > 0)
|
|
110
|
+
def _check_match(self, node: ast.Match, file_path: str) -> PatternSuggestion | None:
|
|
113
111
|
"""
|
|
114
112
|
Check if match statement could benefit from assert_never.
|
|
115
113
|
|
|
@@ -144,7 +142,9 @@ class ExhaustiveMatchDetector(BaseDetector):
|
|
|
144
142
|
elif isinstance(pattern, ast.MatchValue):
|
|
145
143
|
if isinstance(pattern.value, ast.Attribute):
|
|
146
144
|
has_enum_patterns = True
|
|
147
|
-
enum_cases.append(
|
|
145
|
+
enum_cases.append(
|
|
146
|
+
ast.unparse(pattern.value) if hasattr(ast, "unparse") else "..."
|
|
147
|
+
)
|
|
148
148
|
|
|
149
149
|
# Suggest if: has enum patterns + has wildcard + doesn't use assert_never
|
|
150
150
|
if has_enum_patterns and has_wildcard and not uses_assert_never:
|
|
@@ -101,7 +101,7 @@ class LiteralDetector(BaseDetector):
|
|
|
101
101
|
|
|
102
102
|
return suggestions
|
|
103
103
|
|
|
104
|
-
@pre(lambda self, node, file_path: len(file_path) > 0)
|
|
104
|
+
@pre(lambda self, node, file_path: node is not None and len(file_path) > 0)
|
|
105
105
|
@post(lambda result: all(isinstance(s, PatternSuggestion) for s in result))
|
|
106
106
|
def _check_function(
|
|
107
107
|
self, node: ast.FunctionDef | ast.AsyncFunctionDef, file_path: str
|
|
@@ -145,7 +145,11 @@ class LiteralDetector(BaseDetector):
|
|
|
145
145
|
|
|
146
146
|
return suggestions
|
|
147
147
|
|
|
148
|
-
@post(
|
|
148
|
+
@post(
|
|
149
|
+
lambda result: all(
|
|
150
|
+
len(name) > 0 and len(vals) > 0 and line > 0 for name, vals, line in result
|
|
151
|
+
)
|
|
152
|
+
)
|
|
149
153
|
def _find_membership_checks(
|
|
150
154
|
self, node: ast.FunctionDef | ast.AsyncFunctionDef
|
|
151
155
|
) -> list[tuple[str, list[str | int], int]]:
|
|
@@ -206,9 +210,7 @@ class LiteralDetector(BaseDetector):
|
|
|
206
210
|
self._collect_membership_checks(stmt.orelse, checks)
|
|
207
211
|
|
|
208
212
|
@post(lambda result: result is None or (len(result[0]) > 0 and len(result[1]) > 0))
|
|
209
|
-
def _extract_membership_check(
|
|
210
|
-
self, test: ast.expr
|
|
211
|
-
) -> tuple[str, list[str | int]] | None:
|
|
213
|
+
def _extract_membership_check(self, test: ast.expr) -> tuple[str, list[str | int]] | None:
|
|
212
214
|
"""
|
|
213
215
|
Extract variable and values from membership check.
|
|
214
216
|
|
|
@@ -259,9 +261,7 @@ class LiteralDetector(BaseDetector):
|
|
|
259
261
|
if isinstance(node, (ast.Tuple, ast.List, ast.Set)):
|
|
260
262
|
values = []
|
|
261
263
|
for elt in node.elts:
|
|
262
|
-
if isinstance(elt, ast.Constant) and isinstance(
|
|
263
|
-
elt.value, (str, int)
|
|
264
|
-
):
|
|
264
|
+
if isinstance(elt, ast.Constant) and isinstance(elt.value, (str, int)):
|
|
265
265
|
values.append(elt.value)
|
|
266
266
|
else:
|
|
267
267
|
return None # Non-literal value
|
|
@@ -107,7 +107,7 @@ class NewTypeDetector(BaseDetector):
|
|
|
107
107
|
|
|
108
108
|
return suggestions
|
|
109
109
|
|
|
110
|
-
@pre(lambda self, node, file_path: len(file_path) > 0)
|
|
110
|
+
@pre(lambda self, node, file_path: node is not None and len(file_path) > 0)
|
|
111
111
|
def _check_function(
|
|
112
112
|
self, node: ast.FunctionDef | ast.AsyncFunctionDef, file_path: str
|
|
113
113
|
) -> PatternSuggestion | None:
|
|
@@ -153,7 +153,7 @@ class NewTypeDetector(BaseDetector):
|
|
|
153
153
|
|
|
154
154
|
return None
|
|
155
155
|
|
|
156
|
-
@pre(lambda self, param_names, _node: len(param_names) > 0)
|
|
156
|
+
@pre(lambda self, param_names, _node: len(param_names) > 0 and _node is not None)
|
|
157
157
|
@post(lambda result: result in Confidence)
|
|
158
158
|
def _calculate_confidence(
|
|
159
159
|
self, param_names: list[str], _node: ast.FunctionDef | ast.AsyncFunctionDef
|
|
@@ -102,7 +102,7 @@ class NonEmptyDetector(BaseDetector):
|
|
|
102
102
|
|
|
103
103
|
return suggestions
|
|
104
104
|
|
|
105
|
-
@pre(lambda self, node, file_path: len(file_path) > 0)
|
|
105
|
+
@pre(lambda self, node, file_path: node is not None and len(file_path) > 0)
|
|
106
106
|
def _check_function(
|
|
107
107
|
self, node: ast.FunctionDef | ast.AsyncFunctionDef, file_path: str
|
|
108
108
|
) -> PatternSuggestion | None:
|
|
@@ -173,9 +173,7 @@ class NonEmptyDetector(BaseDetector):
|
|
|
173
173
|
return checks
|
|
174
174
|
|
|
175
175
|
@pre(lambda self, stmts, checks: stmts is not None and checks is not None)
|
|
176
|
-
def _collect_empty_checks(
|
|
177
|
-
self, stmts: list[ast.stmt], checks: list[tuple[str, int]]
|
|
178
|
-
) -> None:
|
|
176
|
+
def _collect_empty_checks(self, stmts: list[ast.stmt], checks: list[tuple[str, int]]) -> None:
|
|
179
177
|
"""
|
|
180
178
|
Recursively collect empty checks, avoiding nested functions.
|
|
181
179
|
|
|
@@ -260,7 +258,7 @@ class NonEmptyDetector(BaseDetector):
|
|
|
260
258
|
"""
|
|
261
259
|
return any(isinstance(stmt, (ast.Raise, ast.Return)) for stmt in body)
|
|
262
260
|
|
|
263
|
-
@pre(lambda self, node, var_name: len(var_name) > 0)
|
|
261
|
+
@pre(lambda self, node, var_name: node is not None and len(var_name) > 0)
|
|
264
262
|
def _get_param_type(
|
|
265
263
|
self, node: ast.FunctionDef | ast.AsyncFunctionDef, var_name: str
|
|
266
264
|
) -> str | None:
|
|
@@ -278,7 +276,11 @@ class NonEmptyDetector(BaseDetector):
|
|
|
278
276
|
return self._annotation_to_str(arg.annotation)
|
|
279
277
|
return None
|
|
280
278
|
|
|
281
|
-
@pre(
|
|
279
|
+
@pre(
|
|
280
|
+
lambda self, _node, var_name, param_type=None: _node is not None
|
|
281
|
+
and len(var_name) > 0
|
|
282
|
+
and (param_type is None or isinstance(param_type, str))
|
|
283
|
+
)
|
|
282
284
|
@post(lambda result: result in Confidence)
|
|
283
285
|
def _calculate_confidence(
|
|
284
286
|
self,
|
|
@@ -305,7 +307,10 @@ class NonEmptyDetector(BaseDetector):
|
|
|
305
307
|
|
|
306
308
|
return Confidence.LOW
|
|
307
309
|
|
|
308
|
-
@pre(
|
|
310
|
+
@pre(
|
|
311
|
+
lambda self, var_name, param_type=None: len(var_name) > 0
|
|
312
|
+
and (param_type is None or isinstance(param_type, str))
|
|
313
|
+
)
|
|
309
314
|
@post(lambda result: len(result) > 0 and "if not" in result)
|
|
310
315
|
def _format_check(self, var_name: str, param_type: str | None) -> str:
|
|
311
316
|
"""
|
|
@@ -114,7 +114,7 @@ class ValidationDetector(BaseDetector):
|
|
|
114
114
|
|
|
115
115
|
return suggestions
|
|
116
116
|
|
|
117
|
-
@pre(lambda self, node, file_path: len(file_path) > 0)
|
|
117
|
+
@pre(lambda self, node, file_path: node is not None and len(file_path) > 0)
|
|
118
118
|
def _check_function(
|
|
119
119
|
self, node: ast.FunctionDef | ast.AsyncFunctionDef, file_path: str
|
|
120
120
|
) -> PatternSuggestion | None:
|
|
@@ -156,9 +156,7 @@ class ValidationDetector(BaseDetector):
|
|
|
156
156
|
return None
|
|
157
157
|
|
|
158
158
|
@post(lambda result: result >= 0)
|
|
159
|
-
def _count_early_error_returns(
|
|
160
|
-
self, node: ast.FunctionDef | ast.AsyncFunctionDef
|
|
161
|
-
) -> int:
|
|
159
|
+
def _count_early_error_returns(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> int:
|
|
162
160
|
"""
|
|
163
161
|
Count early returns with error-like values inside if statements.
|
|
164
162
|
|
|
@@ -239,7 +237,7 @@ class ValidationDetector(BaseDetector):
|
|
|
239
237
|
|
|
240
238
|
return False
|
|
241
239
|
|
|
242
|
-
@pre(lambda self, node, early_returns: early_returns >= 0)
|
|
240
|
+
@pre(lambda self, node, early_returns: node is not None and early_returns >= 0)
|
|
243
241
|
@post(lambda result: result in Confidence)
|
|
244
242
|
def _calculate_confidence(
|
|
245
243
|
self, node: ast.FunctionDef | ast.AsyncFunctionDef, early_returns: int
|
|
@@ -269,9 +267,7 @@ class ValidationDetector(BaseDetector):
|
|
|
269
267
|
return Confidence.LOW
|
|
270
268
|
|
|
271
269
|
@post(lambda result: len(result) > 0 and "def " in result)
|
|
272
|
-
def _format_function_preview(
|
|
273
|
-
self, node: ast.FunctionDef | ast.AsyncFunctionDef
|
|
274
|
-
) -> str:
|
|
270
|
+
def _format_function_preview(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> str:
|
|
275
271
|
"""
|
|
276
272
|
Format function preview for display.
|
|
277
273
|
|
invar/core/patterns/registry.py
CHANGED
|
@@ -72,7 +72,13 @@ class PatternRegistry:
|
|
|
72
72
|
"""
|
|
73
73
|
return [d for d in self._detectors if d.priority == priority]
|
|
74
74
|
|
|
75
|
-
@pre(
|
|
75
|
+
@pre(
|
|
76
|
+
lambda self, file_path, source, min_confidence=None, priority_filter=None: len(file_path)
|
|
77
|
+
> 0
|
|
78
|
+
and isinstance(source, str)
|
|
79
|
+
and (min_confidence is None or min_confidence in Confidence)
|
|
80
|
+
and (priority_filter is None or priority_filter in Priority)
|
|
81
|
+
)
|
|
76
82
|
@post(lambda result: result is not None)
|
|
77
83
|
def detect_file(
|
|
78
84
|
self,
|
|
@@ -220,7 +226,11 @@ def get_registry() -> PatternRegistry:
|
|
|
220
226
|
return PatternRegistry()
|
|
221
227
|
|
|
222
228
|
|
|
223
|
-
@pre(
|
|
229
|
+
@pre(
|
|
230
|
+
lambda file_path, source, min_confidence=None: len(file_path) > 0
|
|
231
|
+
and isinstance(source, str)
|
|
232
|
+
and (min_confidence is None or min_confidence in Confidence)
|
|
233
|
+
)
|
|
224
234
|
@post(lambda result: result is not None)
|
|
225
235
|
def detect_patterns(
|
|
226
236
|
file_path: str,
|
invar/core/property_gen.py
CHANGED
|
@@ -194,10 +194,12 @@ def _check_decorator_contracts(dec: ast.Call) -> tuple[bool, bool]:
|
|
|
194
194
|
return has_pre, has_post
|
|
195
195
|
|
|
196
196
|
|
|
197
|
-
@pre(
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
)
|
|
197
|
+
@pre(
|
|
198
|
+
lambda node: (
|
|
199
|
+
isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
|
|
200
|
+
and hasattr(node, "decorator_list")
|
|
201
|
+
)
|
|
202
|
+
)
|
|
201
203
|
@post(lambda result: isinstance(result, tuple) and len(result) == 2)
|
|
202
204
|
def _get_function_contracts(node: ast.FunctionDef | ast.AsyncFunctionDef) -> tuple[bool, bool]:
|
|
203
205
|
"""Check function decorators for contracts, return (has_pre, has_post).
|
|
@@ -247,18 +249,22 @@ def find_contracted_functions(source: str) -> list[dict[str, Any]]:
|
|
|
247
249
|
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
248
250
|
has_pre, has_post = _get_function_contracts(node)
|
|
249
251
|
if has_pre or has_post:
|
|
250
|
-
functions.append(
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
252
|
+
functions.append(
|
|
253
|
+
{
|
|
254
|
+
"name": node.name,
|
|
255
|
+
"lineno": node.lineno,
|
|
256
|
+
"has_pre": has_pre,
|
|
257
|
+
"has_post": has_post,
|
|
258
|
+
"params": _extract_params(node),
|
|
259
|
+
"return_type": _extract_return_type(node),
|
|
260
|
+
}
|
|
261
|
+
)
|
|
258
262
|
return functions
|
|
259
263
|
|
|
260
264
|
|
|
261
|
-
@pre(
|
|
265
|
+
@pre(
|
|
266
|
+
lambda node: isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and hasattr(node, "args")
|
|
267
|
+
)
|
|
262
268
|
@post(lambda result: isinstance(result, list))
|
|
263
269
|
def _extract_params(node: ast.FunctionDef | ast.AsyncFunctionDef) -> list[dict[str, str]]:
|
|
264
270
|
"""Extract parameter names and type annotations from function node."""
|
|
@@ -280,7 +286,11 @@ def _extract_return_type(node: ast.FunctionDef | ast.AsyncFunctionDef) -> str |
|
|
|
280
286
|
return None
|
|
281
287
|
|
|
282
288
|
|
|
283
|
-
@pre(
|
|
289
|
+
@pre(
|
|
290
|
+
lambda func, strategies, max_examples=100: callable(func)
|
|
291
|
+
and isinstance(strategies, dict)
|
|
292
|
+
and max_examples > 0
|
|
293
|
+
)
|
|
284
294
|
@post(lambda result: result is None or callable(result))
|
|
285
295
|
def build_test_function(
|
|
286
296
|
func: Callable,
|
|
@@ -366,25 +376,37 @@ def _skip_result(name: str, reason: str) -> PropertyTestResult:
|
|
|
366
376
|
|
|
367
377
|
# Skip patterns for untestable error detection
|
|
368
378
|
_SKIP_PATTERNS = (
|
|
369
|
-
"Nothing",
|
|
370
|
-
"
|
|
379
|
+
"Nothing",
|
|
380
|
+
"NoSuchExample",
|
|
381
|
+
"filter_too_much",
|
|
382
|
+
"Could not resolve",
|
|
383
|
+
"validation error",
|
|
384
|
+
"missing",
|
|
385
|
+
"positional argument",
|
|
386
|
+
"Unable to satisfy",
|
|
371
387
|
"has no attribute 'check'", # invar_runtime contracts, not deal contracts
|
|
372
388
|
)
|
|
373
389
|
|
|
374
390
|
|
|
375
|
-
@pre(
|
|
391
|
+
@pre(
|
|
392
|
+
lambda err_str, func_name, max_examples: len(err_str) > 0
|
|
393
|
+
and len(func_name) > 0
|
|
394
|
+
and max_examples > 0
|
|
395
|
+
)
|
|
376
396
|
@post(lambda result: isinstance(result, PropertyTestResult))
|
|
377
|
-
def _handle_test_exception(
|
|
378
|
-
err_str: str, func_name: str, max_examples: int
|
|
379
|
-
) -> PropertyTestResult:
|
|
397
|
+
def _handle_test_exception(err_str: str, func_name: str, max_examples: int) -> PropertyTestResult:
|
|
380
398
|
"""Handle exception from property test, returning skip or failure result."""
|
|
381
399
|
# Check for invar_runtime contracts (deal.cases requires deal contracts)
|
|
382
400
|
if "has no attribute 'check'" in err_str:
|
|
383
|
-
return _skip_result(
|
|
401
|
+
return _skip_result(
|
|
402
|
+
func_name, "Skipped: uses invar_runtime (deal.cases requires deal contracts)"
|
|
403
|
+
)
|
|
384
404
|
if any(p in err_str for p in _SKIP_PATTERNS):
|
|
385
405
|
return _skip_result(func_name, "Skipped: untestable types")
|
|
386
406
|
seed = _extract_hypothesis_seed(err_str)
|
|
387
|
-
return PropertyTestResult(
|
|
407
|
+
return PropertyTestResult(
|
|
408
|
+
func_name, passed=False, examples_run=max_examples, error=err_str, seed=seed
|
|
409
|
+
)
|
|
388
410
|
|
|
389
411
|
|
|
390
412
|
@pre(lambda func, max_examples: callable(func) and max_examples > 0)
|
|
@@ -424,7 +446,9 @@ def run_property_test(func: Callable, max_examples: int = 100) -> PropertyTestRe
|
|
|
424
446
|
except deal.PostContractError as e:
|
|
425
447
|
err_str = str(e)
|
|
426
448
|
seed = _extract_hypothesis_seed(err_str)
|
|
427
|
-
return PropertyTestResult(
|
|
449
|
+
return PropertyTestResult(
|
|
450
|
+
func_name, passed=False, examples_run=max_examples, error=err_str, seed=seed
|
|
451
|
+
)
|
|
428
452
|
except ImportError:
|
|
429
453
|
pass # Fall through to custom strategy approach
|
|
430
454
|
except Exception as e:
|
invar/core/shell_analysis.py
CHANGED
|
@@ -22,69 +22,71 @@ if TYPE_CHECKING:
|
|
|
22
22
|
from invar.core.models import Symbol
|
|
23
23
|
|
|
24
24
|
# I/O indicators that mark a function as legitimately in Shell
|
|
25
|
-
IO_INDICATORS: frozenset[str] = frozenset(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
25
|
+
IO_INDICATORS: frozenset[str] = frozenset(
|
|
26
|
+
[
|
|
27
|
+
# File operations
|
|
28
|
+
".read(",
|
|
29
|
+
".write(",
|
|
30
|
+
".read_text(",
|
|
31
|
+
".write_text(",
|
|
32
|
+
".read_bytes(",
|
|
33
|
+
".write_bytes(",
|
|
34
|
+
"open(",
|
|
35
|
+
"Path(",
|
|
36
|
+
".exists()",
|
|
37
|
+
".is_file()",
|
|
38
|
+
".is_dir()",
|
|
39
|
+
".rglob(",
|
|
40
|
+
".glob(",
|
|
41
|
+
".iterdir(",
|
|
42
|
+
".mkdir(",
|
|
43
|
+
".unlink(",
|
|
44
|
+
"shutil.",
|
|
45
|
+
"tempfile.",
|
|
46
|
+
# Process operations
|
|
47
|
+
"subprocess.",
|
|
48
|
+
"os.system(",
|
|
49
|
+
"os.popen(",
|
|
50
|
+
"os.getenv(",
|
|
51
|
+
"os.environ",
|
|
52
|
+
# Terminal/System
|
|
53
|
+
"sys.stdout",
|
|
54
|
+
"sys.stderr",
|
|
55
|
+
"sys.stdin",
|
|
56
|
+
".isatty()",
|
|
57
|
+
# Module loading
|
|
58
|
+
"importlib.",
|
|
59
|
+
"exec_module(",
|
|
60
|
+
# Network operations
|
|
61
|
+
"requests.",
|
|
62
|
+
"aiohttp.",
|
|
63
|
+
"httpx.",
|
|
64
|
+
"urllib.",
|
|
65
|
+
# Console output
|
|
66
|
+
"print(",
|
|
67
|
+
"console.",
|
|
68
|
+
"Console(",
|
|
69
|
+
"typer.",
|
|
70
|
+
"click.",
|
|
71
|
+
"rich.",
|
|
72
|
+
# Result wrapping (Shell's primary job)
|
|
73
|
+
"Success(",
|
|
74
|
+
"Failure(",
|
|
75
|
+
"Result[",
|
|
76
|
+
# Database
|
|
77
|
+
"cursor.",
|
|
78
|
+
"connection.",
|
|
79
|
+
"session.",
|
|
80
|
+
# Logging
|
|
81
|
+
"logger.",
|
|
82
|
+
"logging.",
|
|
83
|
+
# Serialization (often to files)
|
|
84
|
+
"json.dump(",
|
|
85
|
+
"json.load(",
|
|
86
|
+
"toml.load(",
|
|
87
|
+
"yaml.load(",
|
|
88
|
+
]
|
|
89
|
+
)
|
|
88
90
|
|
|
89
91
|
# Marker pattern to exempt functions from complexity check
|
|
90
92
|
COMPLEXITY_MARKER_PATTERN = re.compile(r"#\s*@shell_complexity\s*:")
|
|
@@ -111,7 +113,7 @@ def has_io_operations(source: str) -> bool:
|
|
|
111
113
|
return any(indicator in source for indicator in IO_INDICATORS)
|
|
112
114
|
|
|
113
115
|
|
|
114
|
-
@pre(lambda symbol, source: symbol is not None) # Symbol must exist
|
|
116
|
+
@pre(lambda symbol, source: symbol is not None and isinstance(source, str)) # Symbol must exist
|
|
115
117
|
def has_orchestration_marker(symbol: Symbol, source: str) -> bool:
|
|
116
118
|
"""
|
|
117
119
|
Check if symbol has @shell_orchestration marker comment.
|
|
@@ -144,7 +146,7 @@ def has_orchestration_marker(symbol: Symbol, source: str) -> bool:
|
|
|
144
146
|
return bool(ORCHESTRATION_MARKER_PATTERN.search(context))
|
|
145
147
|
|
|
146
148
|
|
|
147
|
-
@pre(lambda symbol, source: symbol is not None) # Symbol must exist
|
|
149
|
+
@pre(lambda symbol, source: symbol is not None and isinstance(source, str)) # Symbol must exist
|
|
148
150
|
def has_complexity_marker(symbol: Symbol, source: str) -> bool:
|
|
149
151
|
"""
|
|
150
152
|
Check if symbol has @shell_complexity marker comment.
|
|
@@ -221,7 +223,9 @@ def count_branches(source: str) -> int:
|
|
|
221
223
|
return count
|
|
222
224
|
|
|
223
225
|
|
|
224
|
-
@pre(
|
|
226
|
+
@pre(
|
|
227
|
+
lambda symbol, file_source: symbol is not None and isinstance(file_source, str)
|
|
228
|
+
) # Symbol must exist
|
|
225
229
|
def get_symbol_source(symbol: Symbol, file_source: str) -> str:
|
|
226
230
|
"""
|
|
227
231
|
Extract the source code for a specific symbol.
|