Skip to content

Commit a20351d

Browse files
committed
WPS345: forbid symmetric bitwise operations (#3593)
1 parent 7d76031 commit a20351d

File tree

4 files changed

+114
-1
lines changed

4 files changed

+114
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ Semantic versioning in our case means:
2727
- Fixes false positive `WPS457` for ``while True`` loop with ``await`` expressions, #3753
2828
- Fixes the false positive `WPS617` by assigning a function that receives a lambda expression as a parameter.
2929
- Fixes false positive `WPS430` for whitelisted nested functions, #3589
30+
- Fixes `WPS345` to forbid symmetric bitwise operations, #3593
3031

3132

3233
## 1.5.0

tests/test_visitors/test_ast/test_operators/test_useless_math.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from wemake_python_styleguide.violations.consistency import (
44
MeaninglessNumberOperationViolation,
5+
UselessOperatorsViolation,
56
)
67
from wemake_python_styleguide.visitors.ast.operators import (
78
UselessOperatorsVisitor,
@@ -105,6 +106,67 @@ def test_useful_math(
105106
assert_errors(visitor, [])
106107

107108

109+
@pytest.mark.parametrize(
110+
'expression',
111+
[
112+
'6 & 6',
113+
'1 & True',
114+
'(not -3) | (not -3)',
115+
'-~8 ^ -~8',
116+
'+7 & +++7',
117+
'-~-7 | -~-7',
118+
'(not not --7) ^ 7',
119+
],
120+
)
121+
def test_meaningless_symmetric_bitwise_math(
122+
assert_errors,
123+
parse_ast_tree,
124+
expression,
125+
default_options,
126+
):
127+
"""Testing that symmetric bitwise operations are forbidden."""
128+
tree = parse_ast_tree(expression)
129+
130+
visitor = UselessOperatorsVisitor(default_options, tree=tree)
131+
visitor.run()
132+
133+
assert_errors(
134+
visitor,
135+
[MeaninglessNumberOperationViolation],
136+
ignored_types=UselessOperatorsViolation,
137+
)
138+
139+
140+
@pytest.mark.parametrize(
141+
'expression',
142+
[
143+
'5 & 6',
144+
'(not -2) | 3',
145+
'-~7 ^ -~8',
146+
'value | 2',
147+
'-value & -3',
148+
'value ^ -1',
149+
'+6 & +++7',
150+
'-~-4 | -~-7',
151+
'(not not --8) ^ 7',
152+
'value | ~~4',
153+
],
154+
)
155+
def test_useful_asymmetric_bitwise_math(
156+
assert_errors,
157+
parse_ast_tree,
158+
expression,
159+
default_options,
160+
):
161+
"""Testing that asymmetric bitwise operations are allowed."""
162+
tree = parse_ast_tree(expression)
163+
164+
visitor = UselessOperatorsVisitor(default_options, tree=tree)
165+
visitor.run()
166+
167+
assert_errors(visitor, [], ignored_types=UselessOperatorsViolation)
168+
169+
108170
@pytest.mark.parametrize(
109171
'expression',
110172
[

wemake_python_styleguide/logic/tree/operators.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,25 @@ def count_unary_operator(
4747
if isinstance(parent.op, operator):
4848
return count_unary_operator(parent, operator, amount + 1)
4949
return count_unary_operator(parent, operator, amount)
50+
51+
52+
def get_reduced_unary_operators(
53+
node: ast.AST,
54+
opchain: list[type[ast.unaryop]] | None = None,
55+
) -> list[type[ast.unaryop]]:
56+
"""Returns a sequence of significant unary operators."""
57+
if opchain is None:
58+
opchain = []
59+
60+
parent = get_parent(node)
61+
if not isinstance(parent, ast.UnaryOp):
62+
return opchain
63+
64+
if not isinstance(parent.op, ast.UAdd):
65+
lastop = opchain[-1] if opchain else None
66+
if lastop and isinstance(parent.op, lastop):
67+
opchain.pop()
68+
else:
69+
opchain.append(type(parent.op))
70+
71+
return get_reduced_unary_operators(parent, opchain)

wemake_python_styleguide/visitors/ast/operators.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from wemake_python_styleguide.logic.nodes import get_parent
88
from wemake_python_styleguide.logic.tree.operators import (
99
count_unary_operator,
10+
get_reduced_unary_operators,
1011
unwrap_unary_node,
1112
)
1213
from wemake_python_styleguide.types import AnyNodes
@@ -31,7 +32,7 @@
3132
'visit_NameConstant',
3233
),
3334
)
34-
class UselessOperatorsVisitor(base.BaseNodeVisitor):
35+
class UselessOperatorsVisitor(base.BaseNodeVisitor): # noqa: WPS214
3536
"""Checks operators used in the code."""
3637

3738
_unary_limits: ClassVar[_OperatorLimits] = {
@@ -85,6 +86,7 @@ def visit_BinOp(self, node: ast.BinOp) -> None:
8586
"""Visits binary operators."""
8687
self._check_zero_division(node.op, node.right)
8788
self._check_useless_math_operator(node.op, node.left, node.right)
89+
self._check_useless_symmetric_operator(node.op, node.left, node.right)
8890
self.generic_visit(node)
8991

9092
def visit_AugAssign(self, node: ast.AugAssign) -> None:
@@ -137,6 +139,32 @@ def _check_useless_math_operator(
137139
consistency.MeaninglessNumberOperationViolation(number),
138140
)
139141

142+
def _check_useless_symmetric_operator(
143+
self,
144+
op: ast.operator,
145+
left: ast.AST,
146+
right: ast.AST,
147+
) -> None:
148+
real_left = unwrap_unary_node(left)
149+
real_right = unwrap_unary_node(right)
150+
if not (
151+
isinstance(real_left, ast.Constant)
152+
and isinstance(real_right, ast.Constant)
153+
):
154+
return
155+
156+
left_unary_ops = get_reduced_unary_operators(real_left)
157+
right_unary_ops = get_reduced_unary_operators(real_right)
158+
if isinstance(op, (ast.BitAnd, ast.BitOr, ast.BitXor)):
159+
is_identical_constants = (
160+
real_left.value == real_right.value
161+
and left_unary_ops == right_unary_ops
162+
)
163+
if is_identical_constants:
164+
self.add_violation(
165+
consistency.MeaninglessNumberOperationViolation(right)
166+
)
167+
140168
def _get_non_negative_nodes(
141169
self,
142170
left: ast.AST | None,

0 commit comments

Comments
 (0)