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.
@@ -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 found by a Selector.
6
- """
7
-
8
- import ast
9
- from typing import Any
10
-
11
- from ..components.ast_utils import get_full_name
12
- from ..components.definitions import Constraint
13
-
14
-
15
- class IsRequiredConstraint(Constraint):
16
- """Checks that at least one node was found by the selector.
17
-
18
- This constraint is used to enforce the presence of a required language
19
- construct. It can also check for an exact number of occurrences.
20
-
21
- JSON Params:
22
- count (int, optional): If provided, checks if the number of found
23
- nodes is exactly equal to this value.
24
- """
25
-
26
- def __init__(self, **kwargs: Any):
27
- """Initializes the constraint.
28
-
29
- Args:
30
- **kwargs: Configuration for the constraint, e.g., 'count'.
31
- """
32
- self.expected_count = kwargs.get("count")
33
-
34
- def check(self, nodes: list[ast.AST]) -> bool:
35
- """Checks if the list of nodes is not empty or matches expected count."""
36
- if self.expected_count is not None:
37
- return len(nodes) == self.expected_count
38
- return len(nodes) > 0
39
-
40
-
41
- class IsForbiddenConstraint(Constraint):
42
- """Checks that no nodes were found by the selector.
43
-
44
- This is the inverse of `IsRequiredConstraint` and is used to forbid certain
45
- constructs, such as specific function calls or imports.
46
- """
47
-
48
- def __init__(self, **kwargs: Any):
49
- """Initializes the constraint."""
50
- pass
51
-
52
- def check(self, nodes: list[ast.AST]) -> bool:
53
- """Checks if the list of nodes is empty."""
54
- return not nodes
55
-
56
-
57
- class MustInheritFromConstraint(Constraint):
58
- """Checks that a ClassDef node inherits from a specific parent class.
59
-
60
- This constraint is designed to work with a selector that returns a single
61
- `ast.ClassDef` node. It can resolve both simple names (e.g., `Exception`)
62
- and attribute-based names (e.g., `arcade.Window`).
63
-
64
- JSON Params:
65
- parent_name (str): The expected name of the parent class.
66
- """
67
-
68
- def __init__(self, **kwargs: Any):
69
- self.parent_name_to_find: str | None = kwargs.get("parent_name")
70
-
71
- def check(self, nodes: list[ast.AST]) -> bool:
72
- """Checks if the found class node inherits from the specified parent."""
73
- if not self.parent_name_to_find or len(nodes) != 1:
74
- return False
75
-
76
- node = nodes[0]
77
- if not isinstance(node, ast.ClassDef):
78
- return False
79
-
80
- for base in node.bases:
81
- full_name = self._get_full_attribute_name(base)
82
- if full_name == self.parent_name_to_find:
83
- return True
84
- return False
85
-
86
- @staticmethod
87
- def _get_full_attribute_name(node: ast.AST) -> str | None:
88
- """Recursively builds the full attribute name from a base class node."""
89
- if isinstance(node, ast.Name):
90
- return node.id
91
- if isinstance(node, ast.Attribute):
92
- base = MustInheritFromConstraint._get_full_attribute_name(node.value)
93
- return f"{base}.{node.attr}" if base else node.attr
94
- return None
95
-
96
-
97
- class MustBeTypeConstraint(Constraint):
98
- """Checks the type of the value in an assignment statement.
99
-
100
- It works for simple literals (numbers, strings, lists, etc.) and for
101
- calls to built-in type constructors (e.g., `list()`, `dict()`).
102
-
103
- JSON Params:
104
- expected_type (str): The name of the type, e.g., "str", "int", "list".
105
- """
106
-
107
- def __init__(self, **kwargs: Any):
108
- self.expected_type_str: str | None = kwargs.get("expected_type")
109
- self.type_map = {
110
- "str": str,
111
- "int": int,
112
- "float": float,
113
- "list": list,
114
- "dict": dict,
115
- "bool": bool,
116
- "set": set,
117
- "tuple": tuple,
118
- }
119
- self.constructor_map = {t: t for t in self.type_map}
120
-
121
- def check(self, nodes: list[ast.AST]) -> bool:
122
- """Checks if the assigned value has the expected Python type."""
123
- if not nodes or not self.expected_type_str:
124
- return False
125
- expected_py_type = self.type_map.get(self.expected_type_str)
126
- if not expected_py_type:
127
- return False
128
-
129
- for node in nodes:
130
- value_node = getattr(node, "value", None)
131
- if value_node is None:
132
- continue
133
-
134
- if self._is_correct_type(value_node, expected_py_type):
135
- continue
136
- return False
137
- return True
138
-
139
- def _is_correct_type(self, value_node: ast.AST, expected_py_type: type) -> bool:
140
- """Checks a single value node against the expected type."""
141
- try:
142
- assigned_value = ast.literal_eval(value_node)
143
- if isinstance(assigned_value, expected_py_type):
144
- return True
145
- except (ValueError, TypeError, SyntaxError):
146
- pass
147
-
148
- if isinstance(value_node, ast.Call):
149
- func_name = getattr(value_node.func, "id", None)
150
- expected_constructor = self.constructor_map.get(self.expected_type_str)
151
- if func_name == expected_constructor:
152
- return True
153
- return False
154
-
155
-
156
- class NameMustBeInConstraint(Constraint):
157
- """Checks if the name of a found node is in an allowed list of names.
158
-
159
- This is useful for rules like restricting global variables to a pre-defined
160
- set of constants.
161
-
162
- JSON Params:
163
- allowed_names (list[str]): A list of strings containing the allowed names.
164
- """
165
-
166
- def __init__(self, **kwargs: Any):
167
- self.allowed_names = set(kwargs.get("allowed_names", []))
168
-
169
- @staticmethod
170
- def _get_name(node: ast.AST) -> str | None:
171
- """Gets a name from various node types."""
172
- if isinstance(node, (ast.Assign, ast.AnnAssign)):
173
- target = node.targets[0] if isinstance(node, ast.Assign) else node.target
174
- return get_full_name(target)
175
- return getattr(node, "name", getattr(node, "id", None))
176
-
177
- def check(self, nodes: list[ast.AST]) -> bool:
178
- """Checks if all found node names are in the allowed set."""
179
- for node in nodes:
180
- name_to_check = self._get_name(node)
181
- if name_to_check and name_to_check not in self.allowed_names:
182
- return False
183
- return True
184
-
185
-
186
- class ValueMustBeInConstraint(Constraint):
187
- """Checks if the value of a found literal node is in an allowed list.
188
-
189
- This is primarily used to check for "magic numbers" or "magic strings",
190
- allowing only a specific set of literal values to be present.
191
-
192
- JSON Params:
193
- allowed_values (list): A list of allowed literal values (e.g., [0, 1]).
194
- """
195
-
196
- def __init__(self, **kwargs: Any):
197
- self.allowed_values = set(kwargs.get("allowed_values", []))
198
-
199
- def check(self, nodes: list[ast.AST]) -> bool:
200
- """Checks if all found literal values are in the allowed set."""
201
- if not self.allowed_values:
202
- return not nodes
203
-
204
- for node in nodes:
205
- if isinstance(node, ast.Constant):
206
- if node.value not in self.allowed_values:
207
- return False
208
- else:
209
- return False
210
- return True
211
-
212
-
213
- class MustHaveArgsConstraint(Constraint):
214
- """Checks that a FunctionDef node has a specific signature.
215
-
216
- This constraint can check for an exact number of arguments or for an
217
- exact sequence of argument names, ignoring `self` or `cls` in methods.
218
-
219
- JSON Params:
220
- count (int, optional): The exact number of arguments required.
221
- names (list[str], optional): The exact list of argument names in order.
222
- exact_match (bool, optional): Used with `names`. If False, only checks
223
- for presence, not for exact list match. Defaults to True.
224
- """
225
-
226
- def __init__(self, **kwargs: Any):
227
- self.expected_count: int | None = kwargs.get("count")
228
- self.expected_names: list[str] | None = kwargs.get("names")
229
- self.exact_match: bool = kwargs.get("exact_match", True)
230
-
231
- def check(self, nodes: list[ast.AST]) -> bool:
232
- """Checks if the function signature matches the criteria."""
233
- if not nodes:
234
- return True
235
- if not all(isinstance(node, ast.FunctionDef) for node in nodes):
236
- return False
237
-
238
- for node in nodes:
239
- actual_arg_names = [arg.arg for arg in node.args.args]
240
- if hasattr(node, "parent") and isinstance(node.parent, ast.ClassDef):
241
- if actual_arg_names:
242
- actual_arg_names.pop(0)
243
-
244
- if not self._check_single_node(actual_arg_names):
245
- return False
246
- return True
247
-
248
- def _check_single_node(self, actual_arg_names: list[str]) -> bool:
249
- """Checks the argument list of a single function."""
250
- if self.expected_names is not None:
251
- if self.exact_match:
252
- return actual_arg_names == self.expected_names
253
- else:
254
- return set(self.expected_names).issubset(set(actual_arg_names))
255
- elif self.expected_count is not None:
256
- return len(actual_arg_names) == self.expected_count
257
- return False
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