forked from wemake-services/wemake-python-styleguide
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbuiltins.py
More file actions
391 lines (341 loc) · 12.5 KB
/
builtins.py
File metadata and controls
391 lines (341 loc) · 12.5 KB
1
2
3
4
5
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
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
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
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
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
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
import ast
import string
from collections.abc import Sequence
from typing import ClassVar, Final, TypeAlias, final
from wemake_python_styleguide import constants
from wemake_python_styleguide.compat.aliases import (
AssignNodesWithWalrus,
FunctionNodes,
)
from wemake_python_styleguide.logic import nodes, source, walk
from wemake_python_styleguide.logic.tree import (
attributes,
operators,
variables,
)
from wemake_python_styleguide.types import (
AnyChainable,
AnyFor,
AnyNodes,
AnyWith,
)
from wemake_python_styleguide.violations import (
best_practices,
complexity,
consistency,
)
from wemake_python_styleguide.visitors import base, decorators
#: Items that can be inside a hash.
_HashItems: TypeAlias = Sequence[ast.AST | None]
@final
@decorators.alias(
'visit_any_string',
(
'visit_Str',
'visit_Bytes',
),
)
class WrongStringVisitor(base.BaseNodeVisitor):
"""Restricts several string usages."""
_string_constants: ClassVar[frozenset[str]] = frozenset(
(
string.ascii_letters,
string.ascii_lowercase,
string.ascii_uppercase,
string.digits,
string.octdigits,
string.hexdigits,
string.printable,
string.whitespace,
string.punctuation,
),
)
def visit_any_string(self, node: ast.Constant) -> None:
"""Forbids incorrect usage of strings."""
text_data = source.render_string(node.value)
self._check_is_alphabet(node, text_data)
self.generic_visit(node)
def _check_is_alphabet(
self,
node: ast.Constant,
text_data: str | None,
) -> None:
if text_data in self._string_constants:
self.add_violation(
best_practices.StringConstantRedefinedViolation(
node,
text=text_data,
),
)
@final
class WrongFormatStringVisitor(base.BaseNodeVisitor):
"""Restricts usage of ``f`` strings."""
_valid_format_index: ClassVar[AnyNodes] = (
ast.Constant,
ast.Name,
)
_single_use_types: ClassVar[AnyNodes] = (
ast.Call,
ast.Subscript,
)
_chainable_types: Final = (
ast.Call,
ast.Subscript,
ast.Attribute,
)
_max_chained_items = 3
def visit_JoinedStr(self, node: ast.JoinedStr) -> None:
"""Forbids use of ``f`` strings and too complex ``f`` strings."""
if not isinstance(nodes.get_parent(node), ast.FormattedValue):
# We need this condition to make sure that this
# is not a part of complex string format like `f"Count={count:,}"`:
self._check_complex_formatted_string(node)
self.generic_visit(node)
def _check_complex_formatted_string(self, node: ast.JoinedStr) -> None:
"""Allows all simple uses of `f` strings."""
for string_component in node.values:
if isinstance(string_component, ast.FormattedValue):
# Test if possible chaining is invalid
if self._is_valid_formatted_value(string_component.value):
continue
self.add_violation( # Everything else is too complex:
complexity.TooComplexFormattedStringViolation(node),
)
return
def _is_valid_formatted_value(self, format_value: ast.AST) -> bool:
if isinstance(
format_value,
self._chainable_types,
) and not self._is_valid_chaining(format_value):
return False
return self._is_valid_final_value(format_value)
def _is_valid_final_value(self, format_value: ast.AST) -> bool:
# Variable lookup is okay and a single attribute is okay
if isinstance(format_value, ast.Name | ast.Attribute) or (
isinstance(format_value, ast.Call) and len(format_value.args) <= 3
):
return True
# Named lookup, Index lookup & Dict key is okay
if isinstance(format_value, ast.Subscript):
return isinstance(
format_value.slice,
self._valid_format_index,
)
return False
def _is_valid_chaining(self, format_value: AnyChainable) -> bool:
chained_parts: list[ast.AST] = list(attributes.parts(format_value))
if len(chained_parts) <= self._max_chained_items:
return self._is_valid_chain_structure(chained_parts)
return False
def _is_valid_chain_structure(self, chained_parts: list[ast.AST]) -> bool:
"""Helper method for ``_is_valid_chaining``."""
has_invalid_parts = any(
not self._is_valid_final_value(part) for part in chained_parts
)
if has_invalid_parts:
return False
if len(chained_parts) == self._max_chained_items:
# If there are 3 elements, exactly one must be subscript or
# call. This is because we don't allow name.attr.attr
return (
sum(
isinstance(part, self._single_use_types)
for part in chained_parts
)
== 1
)
return True # All chaining with fewer elements is fine!
@final
class WrongNumberVisitor(base.BaseNodeTokenVisitor):
"""Checks wrong numbers used in the code."""
_allowed_parents: ClassVar[AnyNodes] = (
*AssignNodesWithWalrus,
# Constructor usages:
*FunctionNodes,
ast.arguments,
# Primitives:
ast.List,
ast.Dict,
ast.Set,
ast.Tuple,
)
_non_magic_modulo: ClassVar[int] = 10
def visit_Num(self, node: ast.Constant) -> None:
"""Checks wrong constants inside the code."""
self._check_is_magic(node)
self._check_is_approximate_constant(node)
self.generic_visit(node)
def _check_is_magic(self, node: ast.Constant) -> None:
parent = operators.get_parent_ignoring_unary(node)
is_non_magic = (
isinstance(node.value, int) and node.value <= self._non_magic_modulo
)
if (
isinstance(parent, self._allowed_parents)
or node.value in constants.MAGIC_NUMBERS_WHITELIST
or is_non_magic
or self._check_is_number_in_typing_literal(parent)
):
return
try:
token = self._token_dict[node.lineno, node.col_offset]
except KeyError: # pragma: no cover
# For some reason, the token was not found.
# We are not sure that this will actually happen,
# and cannot really replicate this. Yet. But, better be safe.
real_value = str(node.value)
else:
real_value = token.string
self.add_violation(
best_practices.MagicNumberViolation(node, text=real_value),
)
def _check_is_number_in_typing_literal(self, node: ast.AST | None) -> bool:
if isinstance(node, ast.Subscript):
if (
isinstance(node.value, ast.Attribute)
and isinstance(node.value.value, ast.Name)
and node.value.value.id in {'typing', 'typing_extensions'}
and node.value.attr == 'Literal'
):
return True
if isinstance(node.value, ast.Name) and node.value.id in 'Literal':
return True
return False
def _check_is_approximate_constant(self, node: ast.Constant) -> None:
try:
precision = len(str(node.value).split('.')[1])
except IndexError:
precision = 0
if precision < 2:
return
for constant in constants.MATH_APPROXIMATE_CONSTANTS:
if str(constant).startswith(str(node.value)):
self.add_violation(
best_practices.ApproximateConstantViolation(
node,
text=str(node.value),
),
)
@final
@decorators.alias(
'visit_any_for',
(
'visit_For',
'visit_AsyncFor',
),
)
@decorators.alias(
'visit_any_with',
(
'visit_With',
'visit_AsyncWith',
),
)
class WrongAssignmentVisitor(base.BaseNodeVisitor):
"""Visits all assign nodes."""
def visit_any_with(self, node: AnyWith) -> None:
"""Checks assignments inside context managers to be correct."""
for withitem in node.items:
self._check_unpacking_target_types(withitem.optional_vars)
if isinstance(withitem.optional_vars, ast.Tuple):
self._check_unpacking_targets(
node,
withitem.optional_vars.elts,
)
self.generic_visit(node)
def visit_comprehension(self, node: ast.comprehension) -> None:
"""Checks comprehensions for the correct assignments."""
self._check_unpacking_target_types(node.target)
if isinstance(node.target, ast.Tuple):
self._check_unpacking_targets(node.target, node.target.elts)
self.generic_visit(node)
def visit_any_for(self, node: AnyFor) -> None:
"""Checks assignments inside ``for`` loops to be correct."""
self._check_unpacking_target_types(node.target)
if isinstance(node.target, ast.Tuple):
self._check_unpacking_targets(node, node.target.elts)
self.generic_visit(node)
def visit_Assign(self, node: ast.Assign) -> None:
"""
Checks assignments to be correct.
We do not check ``AnnAssign`` here,
because it does not have problems that we check.
"""
self._check_assign_targets(node)
for target in node.targets:
self._check_unpacking_target_types(target)
if isinstance(node.targets[0], ast.Tuple | ast.List):
self._check_unpacking_targets(node, node.targets[0].elts)
self.generic_visit(node)
def _check_assign_targets(self, node: ast.Assign) -> None:
if len(node.targets) > 1:
self.add_violation(
best_practices.MultipleAssignmentsViolation(node),
)
def _check_unpacking_targets(
self,
node: ast.AST,
targets: list[ast.expr],
) -> None:
if len(targets) == 1:
self.add_violation(
best_practices.SingleElementDestructuringViolation(node),
)
elif variables.is_getting_element_by_unpacking(targets):
self.add_violation(
best_practices.GettingElementByUnpackingViolation(
node,
),
)
for target in targets:
if not variables.is_valid_unpacking_target(target):
self.add_violation(
best_practices.WrongUnpackingViolation(node),
)
def _check_unpacking_target_types(self, node: ast.AST | None) -> None:
if not node:
return
for subnode in walk.get_subnodes_by_type(node, ast.List):
self.add_violation(
consistency.UnpackingIterableToListViolation(subnode),
)
@final
class WrongCollectionVisitor(base.BaseNodeVisitor):
"""Ensures that collection definitions are correct."""
_unhashable_types: ClassVar[AnyNodes] = (
ast.List,
ast.ListComp,
ast.Set,
ast.SetComp,
ast.Dict,
ast.DictComp,
ast.GeneratorExp,
)
def visit_Set(self, node: ast.Set) -> None:
"""Ensures that set literals do not have any duplicate items."""
self._check_unhashable_elements(node.elts)
self.generic_visit(node)
def visit_Dict(self, node: ast.Dict) -> None:
"""Ensures that dict literals do not have any duplicate keys."""
self._check_unhashable_elements(node.keys)
self._check_float_keys(node.keys)
self.generic_visit(node)
def _check_float_keys(self, keys: _HashItems) -> None:
for dict_key in keys:
if dict_key is None:
continue
real_key = operators.unwrap_unary_node(dict_key)
if isinstance(real_key, ast.Constant) and isinstance(
real_key.value,
float,
):
self.add_violation(best_practices.FloatKeyViolation(dict_key))
def _check_unhashable_elements(
self,
keys_or_elts: _HashItems,
) -> None:
for set_item in keys_or_elts:
if isinstance(set_item, self._unhashable_types):
self.add_violation(
best_practices.UnhashableTypeInHashViolation(set_item),
)