python-code-validator 0.1.2__py3-none-any.whl → 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- code_validator/__init__.py +43 -11
- code_validator/__main__.py +12 -3
- code_validator/cli.py +48 -24
- code_validator/components/ast_utils.py +6 -2
- code_validator/components/definitions.py +6 -5
- code_validator/components/factories.py +35 -16
- code_validator/components/scope_handler.py +5 -3
- code_validator/config.py +60 -7
- code_validator/core.py +161 -38
- code_validator/output.py +160 -14
- code_validator/rules_library/basic_rules.py +23 -16
- code_validator/rules_library/constraint_logic.py +301 -257
- code_validator/rules_library/selector_nodes.py +66 -5
- {python_code_validator-0.1.2.dist-info → python_code_validator-0.2.1.dist-info}/METADATA +13 -30
- python_code_validator-0.2.1.dist-info/RECORD +22 -0
- python_code_validator-0.1.2.dist-info/RECORD +0 -22
- {python_code_validator-0.1.2.dist-info → python_code_validator-0.2.1.dist-info}/WHEEL +0 -0
- {python_code_validator-0.1.2.dist-info → python_code_validator-0.2.1.dist-info}/entry_points.txt +0 -0
- {python_code_validator-0.1.2.dist-info → python_code_validator-0.2.1.dist-info}/licenses/LICENSE +0 -0
- {python_code_validator-0.1.2.dist-info → python_code_validator-0.2.1.dist-info}/top_level.txt +0 -0
@@ -1,257 +1,301 @@
|
|
1
|
-
"""Contains concrete implementations of all Constraint components.
|
2
|
-
|
3
|
-
Each class in this module implements the `Constraint` protocol and encapsulates
|
4
|
-
the logic for a specific condition that can be checked against a list of
|
5
|
-
AST nodes
|
6
|
-
""
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
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
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
return
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
if
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
return
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
def
|
200
|
-
"""
|
201
|
-
if
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
def __init__(self, **kwargs: Any):
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
"""
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
return False
|
246
|
-
return True
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
1
|
+
"""Contains concrete implementations of all Constraint components.
|
2
|
+
|
3
|
+
Each class in this module implements the `Constraint` protocol and encapsulates
|
4
|
+
the logic for a specific condition that can be checked against a list of
|
5
|
+
AST nodes. These classes are instantiated by the `ConstraintFactory` based on
|
6
|
+
the "constraint" block in a JSON rule.
|
7
|
+
|
8
|
+
The module also includes helper functions for processing AST nodes, which are
|
9
|
+
used internally by the constraint classes.
|
10
|
+
"""
|
11
|
+
|
12
|
+
import ast
|
13
|
+
from typing import Any
|
14
|
+
|
15
|
+
from .. import LogLevel
|
16
|
+
from ..components.ast_utils import get_full_name
|
17
|
+
from ..components.definitions import Constraint
|
18
|
+
from ..output import log_initialization
|
19
|
+
|
20
|
+
|
21
|
+
class IsRequiredConstraint(Constraint):
|
22
|
+
"""Checks that at least one node was found by the selector.
|
23
|
+
|
24
|
+
This constraint is used to enforce the presence of a required language
|
25
|
+
construct. It can also check for an exact number of occurrences.
|
26
|
+
|
27
|
+
JSON Params:
|
28
|
+
count (int, optional): If provided, checks if the number of found
|
29
|
+
nodes is exactly equal to this value.
|
30
|
+
"""
|
31
|
+
|
32
|
+
@log_initialization(level=LogLevel.TRACE)
|
33
|
+
def __init__(self, **kwargs: Any):
|
34
|
+
"""Initializes the constraint.
|
35
|
+
|
36
|
+
Args:
|
37
|
+
**kwargs: Configuration for the constraint, e.g., 'count'.
|
38
|
+
"""
|
39
|
+
self.expected_count = kwargs.get("count")
|
40
|
+
|
41
|
+
def check(self, nodes: list[ast.AST]) -> bool:
|
42
|
+
"""Checks if the list of nodes is not empty or matches expected count."""
|
43
|
+
if self.expected_count is not None:
|
44
|
+
return len(nodes) == self.expected_count
|
45
|
+
return len(nodes) > 0
|
46
|
+
|
47
|
+
|
48
|
+
class IsForbiddenConstraint(Constraint):
|
49
|
+
"""Checks that no nodes were found by the selector.
|
50
|
+
|
51
|
+
This is the inverse of `IsRequiredConstraint` and is used to forbid certain
|
52
|
+
constructs, such as specific function calls or imports.
|
53
|
+
"""
|
54
|
+
|
55
|
+
@log_initialization(level=LogLevel.TRACE)
|
56
|
+
def __init__(self, **kwargs: Any):
|
57
|
+
"""Initializes the constraint."""
|
58
|
+
pass
|
59
|
+
|
60
|
+
def check(self, nodes: list[ast.AST]) -> bool:
|
61
|
+
"""Checks if the list of nodes is empty."""
|
62
|
+
return not nodes
|
63
|
+
|
64
|
+
|
65
|
+
class MustInheritFromConstraint(Constraint):
|
66
|
+
"""Checks that a ClassDef node inherits from a specific parent class.
|
67
|
+
|
68
|
+
This constraint is designed to work with a selector that returns a single
|
69
|
+
`ast.ClassDef` node. It can resolve both simple names (e.g., `Exception`)
|
70
|
+
and attribute-based names (e.g., `arcade.Window`).
|
71
|
+
|
72
|
+
JSON Params:
|
73
|
+
parent_name (str): The expected name of the parent class.
|
74
|
+
"""
|
75
|
+
|
76
|
+
@log_initialization(level=LogLevel.TRACE)
|
77
|
+
def __init__(self, **kwargs: Any):
|
78
|
+
"""Initializes the constraint.
|
79
|
+
|
80
|
+
Args:
|
81
|
+
**kwargs: Keyword arguments from the JSON rule's constraint config.
|
82
|
+
Expects `parent_name` (str) specifying the required parent class.
|
83
|
+
"""
|
84
|
+
self.parent_name_to_find: str | None = kwargs.get("parent_name")
|
85
|
+
|
86
|
+
def check(self, nodes: list[ast.AST]) -> bool:
|
87
|
+
"""Checks if the found class node inherits from the specified parent."""
|
88
|
+
if not self.parent_name_to_find or len(nodes) != 1:
|
89
|
+
return False
|
90
|
+
|
91
|
+
node = nodes[0]
|
92
|
+
if not isinstance(node, ast.ClassDef):
|
93
|
+
return False
|
94
|
+
|
95
|
+
for base in node.bases:
|
96
|
+
full_name = self._get_full_attribute_name(base)
|
97
|
+
if full_name == self.parent_name_to_find:
|
98
|
+
return True
|
99
|
+
return False
|
100
|
+
|
101
|
+
@staticmethod
|
102
|
+
def _get_full_attribute_name(node: ast.AST) -> str | None:
|
103
|
+
"""Recursively builds the full attribute name from a base class node."""
|
104
|
+
if isinstance(node, ast.Name):
|
105
|
+
return node.id
|
106
|
+
if isinstance(node, ast.Attribute):
|
107
|
+
base = MustInheritFromConstraint._get_full_attribute_name(node.value)
|
108
|
+
return f"{base}.{node.attr}" if base else node.attr
|
109
|
+
return None
|
110
|
+
|
111
|
+
|
112
|
+
class MustBeTypeConstraint(Constraint):
|
113
|
+
"""Checks the type of the value in an assignment statement.
|
114
|
+
|
115
|
+
It works for simple literals (numbers, strings, lists, etc.) and for
|
116
|
+
calls to built-in type constructors (e.g., `list()`, `dict()`).
|
117
|
+
|
118
|
+
JSON Params:
|
119
|
+
expected_type (str): The name of the type, e.g., "str", "int", "list".
|
120
|
+
"""
|
121
|
+
|
122
|
+
@log_initialization(level=LogLevel.TRACE)
|
123
|
+
def __init__(self, **kwargs: Any):
|
124
|
+
"""Initializes the constraint.
|
125
|
+
|
126
|
+
Args:
|
127
|
+
**kwargs: Keyword arguments from the JSON rule's constraint config.
|
128
|
+
Expects `expected_type` (str) with the name of the required type.
|
129
|
+
"""
|
130
|
+
self.expected_type_str: str | None = kwargs.get("expected_type")
|
131
|
+
self.type_map = {
|
132
|
+
"str": str,
|
133
|
+
"int": int,
|
134
|
+
"float": float,
|
135
|
+
"list": list,
|
136
|
+
"dict": dict,
|
137
|
+
"bool": bool,
|
138
|
+
"set": set,
|
139
|
+
"tuple": tuple,
|
140
|
+
}
|
141
|
+
self.constructor_map = {t: t for t in self.type_map}
|
142
|
+
|
143
|
+
def check(self, nodes: list[ast.AST]) -> bool:
|
144
|
+
"""Checks if the assigned value has the expected Python type."""
|
145
|
+
if not nodes or not self.expected_type_str:
|
146
|
+
return False
|
147
|
+
expected_py_type = self.type_map.get(self.expected_type_str)
|
148
|
+
if not expected_py_type:
|
149
|
+
return False
|
150
|
+
|
151
|
+
for node in nodes:
|
152
|
+
value_node = getattr(node, "value", None)
|
153
|
+
if value_node is None:
|
154
|
+
continue
|
155
|
+
|
156
|
+
if self._is_correct_type(value_node, expected_py_type):
|
157
|
+
continue
|
158
|
+
return False
|
159
|
+
return True
|
160
|
+
|
161
|
+
def _is_correct_type(self, value_node: ast.AST, expected_py_type: type) -> bool:
|
162
|
+
"""Checks a single value node against the expected type."""
|
163
|
+
try:
|
164
|
+
assigned_value = ast.literal_eval(value_node)
|
165
|
+
if isinstance(assigned_value, expected_py_type):
|
166
|
+
return True
|
167
|
+
except (ValueError, TypeError, SyntaxError):
|
168
|
+
pass
|
169
|
+
|
170
|
+
if isinstance(value_node, ast.Call):
|
171
|
+
func_name = getattr(value_node.func, "id", None)
|
172
|
+
expected_constructor = self.constructor_map.get(self.expected_type_str)
|
173
|
+
if func_name == expected_constructor:
|
174
|
+
return True
|
175
|
+
return False
|
176
|
+
|
177
|
+
|
178
|
+
class NameMustBeInConstraint(Constraint):
|
179
|
+
"""Checks if the name of a found node is in an allowed list of names.
|
180
|
+
|
181
|
+
This is useful for rules like restricting global variables to a pre-defined
|
182
|
+
set of constants.
|
183
|
+
|
184
|
+
JSON Params:
|
185
|
+
allowed_names (list[str]): A list of strings containing the allowed names.
|
186
|
+
"""
|
187
|
+
|
188
|
+
@log_initialization(level=LogLevel.TRACE)
|
189
|
+
def __init__(self, **kwargs: Any):
|
190
|
+
"""Initializes the constraint.
|
191
|
+
|
192
|
+
Args:
|
193
|
+
**kwargs: Keyword arguments from the JSON rule's constraint config.
|
194
|
+
Expects `allowed_names` (list[str]) containing the valid names.
|
195
|
+
"""
|
196
|
+
self.allowed_names = set(kwargs.get("allowed_names", []))
|
197
|
+
|
198
|
+
@staticmethod
|
199
|
+
def _get_name(node: ast.AST) -> str | None:
|
200
|
+
"""Gets a name from various node types."""
|
201
|
+
if isinstance(node, (ast.Assign, ast.AnnAssign)):
|
202
|
+
target = node.targets[0] if isinstance(node, ast.Assign) else node.target
|
203
|
+
return get_full_name(target)
|
204
|
+
return getattr(node, "name", getattr(node, "id", None))
|
205
|
+
|
206
|
+
def check(self, nodes: list[ast.AST]) -> bool:
|
207
|
+
"""Checks if all found node names are in the allowed set."""
|
208
|
+
for node in nodes:
|
209
|
+
name_to_check = self._get_name(node)
|
210
|
+
if name_to_check and name_to_check not in self.allowed_names:
|
211
|
+
return False
|
212
|
+
return True
|
213
|
+
|
214
|
+
|
215
|
+
class ValueMustBeInConstraint(Constraint):
|
216
|
+
"""Checks if the value of a found literal node is in an allowed list.
|
217
|
+
|
218
|
+
This is primarily used to check for "magic numbers" or "magic strings",
|
219
|
+
allowing only a specific set of literal values to be present.
|
220
|
+
|
221
|
+
JSON Params:
|
222
|
+
allowed_values (list): A list of allowed literal values (e.g., [0, 1]).
|
223
|
+
"""
|
224
|
+
|
225
|
+
@log_initialization(level=LogLevel.TRACE)
|
226
|
+
def __init__(self, **kwargs: Any):
|
227
|
+
"""Initializes the constraint.
|
228
|
+
|
229
|
+
Args:
|
230
|
+
**kwargs: Keyword arguments from the JSON rule's constraint config.
|
231
|
+
Expects `allowed_values` (list) containing the valid literal values.
|
232
|
+
"""
|
233
|
+
self.allowed_values = set(kwargs.get("allowed_values", []))
|
234
|
+
|
235
|
+
def check(self, nodes: list[ast.AST]) -> bool:
|
236
|
+
"""Checks if all found literal values are in the allowed set."""
|
237
|
+
if not self.allowed_values:
|
238
|
+
return not nodes
|
239
|
+
|
240
|
+
for node in nodes:
|
241
|
+
if isinstance(node, ast.Constant):
|
242
|
+
if node.value not in self.allowed_values:
|
243
|
+
return False
|
244
|
+
else:
|
245
|
+
return False
|
246
|
+
return True
|
247
|
+
|
248
|
+
|
249
|
+
class MustHaveArgsConstraint(Constraint):
|
250
|
+
"""Checks that a FunctionDef node has a specific signature.
|
251
|
+
|
252
|
+
This constraint can check for an exact number of arguments or for an
|
253
|
+
exact sequence of argument names, ignoring `self` or `cls` in methods.
|
254
|
+
|
255
|
+
JSON Params:
|
256
|
+
count (int, optional): The exact number of arguments required.
|
257
|
+
names (list[str], optional): The exact list of argument names in order.
|
258
|
+
exact_match (bool, optional): Used with `names`. If False, only checks
|
259
|
+
for presence, not for exact list match. Defaults to True.
|
260
|
+
"""
|
261
|
+
|
262
|
+
@log_initialization(level=LogLevel.TRACE)
|
263
|
+
def __init__(self, **kwargs: Any):
|
264
|
+
"""Initializes the constraint.
|
265
|
+
|
266
|
+
Args:
|
267
|
+
**kwargs: Keyword arguments from the JSON rule's constraint config.
|
268
|
+
Can accept `count` (int), `names` (list[str]), and
|
269
|
+
`exact_match` (bool).
|
270
|
+
"""
|
271
|
+
self.expected_count: int | None = kwargs.get("count")
|
272
|
+
self.expected_names: list[str] | None = kwargs.get("names")
|
273
|
+
self.exact_match: bool = kwargs.get("exact_match", True)
|
274
|
+
|
275
|
+
def check(self, nodes: list[ast.AST]) -> bool:
|
276
|
+
"""Checks if the function signature matches the criteria."""
|
277
|
+
if not nodes:
|
278
|
+
return True
|
279
|
+
if not all(isinstance(node, ast.FunctionDef) for node in nodes):
|
280
|
+
return False
|
281
|
+
|
282
|
+
for node in nodes:
|
283
|
+
actual_arg_names = [arg.arg for arg in node.args.args]
|
284
|
+
if hasattr(node, "parent") and isinstance(node.parent, ast.ClassDef):
|
285
|
+
if actual_arg_names:
|
286
|
+
actual_arg_names.pop(0)
|
287
|
+
|
288
|
+
if not self._check_single_node(actual_arg_names):
|
289
|
+
return False
|
290
|
+
return True
|
291
|
+
|
292
|
+
def _check_single_node(self, actual_arg_names: list[str]) -> bool:
|
293
|
+
"""Checks the argument list of a single function."""
|
294
|
+
if self.expected_names is not None:
|
295
|
+
if self.exact_match:
|
296
|
+
return actual_arg_names == self.expected_names
|
297
|
+
else:
|
298
|
+
return set(self.expected_names).issubset(set(actual_arg_names))
|
299
|
+
elif self.expected_count is not None:
|
300
|
+
return len(actual_arg_names) == self.expected_count
|
301
|
+
return False
|