Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Redundant Tenary Check Added #2902

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open
3 changes: 3 additions & 0 deletions tests/fixtures/noqa/noqa.py
Original file line number Diff line number Diff line change
Expand Up @@ -774,6 +774,9 @@ def many_raises_function(parameter): # noqa: WPS238
raise IndexError('3')
raise TypeError('4')

ternary_a = 1
ternary_b = 2
ternary_result = ternary_a if ... else ternary_a # noqa: WPS475

my_print("""
text
Expand Down
1 change: 1 addition & 0 deletions tests/test_checker/test_noqa.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@
'WPS472': 1,
'WPS473': 0,
'WPS474': 1,
'WPS475': 1,

'WPS500': 1,
'WPS501': 1,
Expand Down
108 changes: 108 additions & 0 deletions tests/test_visitors/test_ast/test_redundancy/test_redundant_ternary.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import pytest

from wemake_python_styleguide.violations.best_practices import (
RedundantTernaryViolation,
)
from wemake_python_styleguide.visitors.ast.redundancy import (
RedundantTernaryVisitor,
)

# Correct:
correct_ternary_elipses = """
a if ... else c
"""

correct_not_equal = """
a if a != b else c
"""

non_compare = """
a if a | b else None
"""

two_operation_compare = """
a == b != c
"""

correct_ternary_none = """
a.split() if a is not None else None
"""

# Wrong:
wrong_ternary_same_values = """
a if ... else a
"""

wrong_ternary_useless_comparison_not_eq = """
a if a != b else b
"""
wrong_ternary_useless_comparison_eq = """
b if a == b else a
"""

wrong_ternarny_none = """
a if a is not None else None
"""


@pytest.mark.parametrize('code', [
correct_ternary_elipses,
correct_not_equal,
correct_ternary_none,
])
def test_correct_ternary(
code,
assert_errors,
parse_ast_tree,
default_options,
mode,
):
"""Ensures that correct ternary expressions are fine."""
tree = parse_ast_tree(mode(code))

visitor = RedundantTernaryVisitor(default_options, tree=tree)
visitor.run()

assert_errors(visitor, [])


@pytest.mark.parametrize('code', [
wrong_ternary_same_values,
wrong_ternary_useless_comparison_not_eq,
wrong_ternary_useless_comparison_eq,
wrong_ternarny_none,
])
def test_wrong_ternary(
code,
assert_errors,
parse_ast_tree,
default_options,
mode,
):
"""Ensures that redundant ternary expressions are forbidden."""
tree = parse_ast_tree(mode(code))

visitor = RedundantTernaryVisitor(default_options, tree=tree)
visitor.run()

assert_errors(visitor, [RedundantTernaryViolation])


@pytest.mark.parametrize('code', [
non_compare,
two_operation_compare,
])
def safety_check_ternary(
code,
assert_errors,
parse_ast_tree,
default_options,
mode,
FieldMarshallObvious marked this conversation as resolved.
Show resolved Hide resolved
):
"""Ensures that conditions for checking are met."""
tree = parse_ast_tree(mode(code))

visitor = RedundantTernaryVisitor(default_options, tree=tree)
visitor.run()

assert_errors(visitor, [])
1 change: 1 addition & 0 deletions wemake_python_styleguide/presets/types/tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@
decorators.WrongDecoratorVisitor,

redundancy.RedundantEnumerateVisitor,
redundancy.RedundantTernaryVisitor,

function_empty_lines.WrongEmptyLinesCountVisitor,

Expand Down
33 changes: 33 additions & 0 deletions wemake_python_styleguide/violations/best_practices.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
GettingElementByUnpackingViolation
WrongEmptyLinesCountViolation
ImportObjectCollisionViolation
RedundantTernaryViolation

Best practices
--------------
Expand Down Expand Up @@ -170,6 +171,7 @@
.. autoclass:: GettingElementByUnpackingViolation
.. autoclass:: WrongEmptyLinesCountViolation
.. autoclass:: ImportObjectCollisionViolation
.. autoclass:: RedundantTernaryViolation

"""

Expand Down Expand Up @@ -2849,3 +2851,34 @@ class ImportObjectCollisionViolation(ASTViolation):

error_template = 'Found import object collision: {0}'
code = 474


@final
class RedundantTernaryViolation(ASTViolation):
"""
Forbid the redundant ternary expressions.

Reasoning:
If the ternary expression can be simplified,
it should be simplified.

Solution:
Instead of using a ternary operator, use the value directly.

Example::
Correct:
a if b is None else c
a if a != b else c

Wrong:
a if ... else a
a if a is not None else None
a if a != b else b
b if a == b else a

.. versionadded:: 0.19.0
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.. versionadded:: 0.19.0
.. versionadded:: 0.20.0


"""

error_template = 'Found redundant ternary operator'
code = 475
44 changes: 44 additions & 0 deletions wemake_python_styleguide/visitors/ast/redundancy.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from wemake_python_styleguide.types import AnyComprehension, AnyFor
from wemake_python_styleguide.violations.best_practices import (
RedundantEnumerateViolation,
RedundantTernaryViolation,
)
from wemake_python_styleguide.visitors import decorators
from wemake_python_styleguide.visitors.base import BaseNodeVisitor
Expand Down Expand Up @@ -53,3 +54,46 @@ def _check_for_redundant_enumerate(self, node: Union[AnyFor, ast.comprehension])

if isinstance(index_receiver, ast.Name) and index_receiver.id == '_':
self.add_violation(RedundantEnumerateViolation(index_receiver))


@final
class RedundantTernaryVisitor(BaseNodeVisitor):
"""Finds useless ternary operators."""

def visit_IfExp(self, node: ast.IfExp) -> None:
"""Finds useless ternary operators."""
body = ast.unparse(node.body)
orelse = ast.unparse(node.orelse)

# Check for specific patterns.
self.body_orelse_check(node, body, orelse)
self.check_computed_equal(node)

self.generic_visit(node) # Visit all other node types.

def body_orelse_check(
self, node: ast.IfExp, body: str, orelse: str,
) -> None:
"""Check if body and orelse are the same."""
if body == orelse:
self.add_violation(RedundantTernaryViolation(node))

def check_computed_equal(self, node: ast.IfExp) -> None:
"""Used to check if the computed branches are equal."""
if not isinstance(node.test, ast.Compare):
return

left = ast.unparse(node.test.left)
right = ast.unparse(node.test.comparators[0])

self.compare_operands(node, left, right)

def compare_operands(self, node: ast.IfExp, left: str, right: str) -> None:
"""Compare each operand to see if will result in same values."""
body_str = ast.unparse(node.body)
orelse_str = ast.unparse(node.orelse)

if body_str == left and orelse_str == right:
self.add_violation(RedundantTernaryViolation(node))
elif body_str == right and orelse_str == left:
self.add_violation(RedundantTernaryViolation(node))