Skip to content

Commit 30e24c0

Browse files
committed
Add improve-conditionals check in the CodeStyle extension
1 parent 0871a4d commit 30e24c0

File tree

9 files changed

+128
-1
lines changed

9 files changed

+128
-1
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
def func(expr, node_cls):
2+
# +1:[improve-conditional]
3+
if not isinstance(expr, node_cls) or expr.attrname != "__init__":
4+
...
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
def func(expr, node_cls):
2+
if not (isinstance(expr, node_cls) and expr.attrname == "__init__"):
3+
...

doc/user_guide/checkers/extensions.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ Code Style checker Messages
8888
'typing.NamedTuple' uses the well-known 'class' keyword with type-hints for
8989
readability (it's also faster as it avoids an internal exec call). Disabled
9090
by default!
91+
:improve-conditionals (R6106): *Rewrite conditional expression to '%s'*
92+
Rewrite negated if expressions to improve readability.
9193

9294

9395
.. _pylint.extensions.comparison_placement:

doc/user_guide/messages/messages_overview.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,7 @@ All messages in the refactor category:
515515
refactor/duplicate-code
516516
refactor/else-if-used
517517
refactor/empty-comment
518+
refactor/improve-conditionals
518519
refactor/inconsistent-return-statements
519520
refactor/literal-comparison
520521
refactor/magic-value-comparison
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Add :ref:`improve-conditionals` check to the Code Style extension.
2+
3+
Refs #10600

pylint/extensions/code_style.py

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@
44

55
from __future__ import annotations
66

7+
from copy import copy
78
from typing import TYPE_CHECKING, TypeGuard, cast
89

910
from astroid import nodes
1011

1112
from pylint.checkers import BaseChecker, utils
1213
from pylint.checkers.utils import only_required_for_messages, safe_infer
13-
from pylint.interfaces import INFERENCE
14+
from pylint.interfaces import HIGH, INFERENCE
1415

1516
if TYPE_CHECKING:
1617
from pylint.lint import PyLinter
@@ -74,6 +75,14 @@ class CodeStyleChecker(BaseChecker):
7475
"default_enabled": False,
7576
},
7677
),
78+
"R6106": (
79+
"Rewrite conditional expression to '%s'",
80+
"improve-conditionals",
81+
"Rewrite negated if expressions to improve readability.",
82+
{
83+
# "default_enabled": False,
84+
},
85+
),
7786
}
7887
options = (
7988
(
@@ -320,6 +329,76 @@ def visit_assign(self, node: nodes.Assign) -> None:
320329
confidence=INFERENCE,
321330
)
322331

332+
@staticmethod
333+
def _can_be_inverted(node: nodes.NodeNG) -> bool:
334+
match node:
335+
case nodes.UnaryOp(op="not"):
336+
return True
337+
case nodes.Compare(
338+
ops=[("!=" | "not in", _)]
339+
| [("<" | "<=" | ">" | ">=", nodes.Const(value=int()))]
340+
):
341+
return True
342+
return False
343+
344+
@staticmethod
345+
def _invert_node(node: nodes.NodeNG) -> nodes.NodeNG:
346+
match node:
347+
case nodes.UnaryOp(op="not"):
348+
new_node = copy(node.operand)
349+
new_node.parent = node
350+
return new_node
351+
case nodes.Compare(left=left, ops=[(op, n)]):
352+
new_node = copy(node)
353+
match op:
354+
case "!=":
355+
new_op = "=="
356+
case "not in":
357+
new_op = "in"
358+
case "<":
359+
new_op = ">="
360+
case "<=":
361+
new_op = ">"
362+
case ">":
363+
new_op = "<="
364+
case ">=":
365+
new_op = "<"
366+
case _: # pragma: no cover
367+
raise AssertionError
368+
new_node.postinit(left=left, ops=[(new_op, n)])
369+
return new_node
370+
case _: # pragma: no cover
371+
raise AssertionError
372+
373+
@only_required_for_messages("improve-conditionals")
374+
def visit_boolop(self, node: nodes.BoolOp) -> None:
375+
if node.op == "or" and all(self._can_be_inverted(val) for val in node.values):
376+
new_boolop = copy(node)
377+
new_boolop.op = "and"
378+
new_boolop.postinit([self._invert_node(val) for val in node.values])
379+
380+
if isinstance(node.parent, nodes.UnaryOp) and node.parent.op == "not":
381+
target_node = node.parent
382+
new_node = new_boolop
383+
else:
384+
target_node = node
385+
new_node = nodes.UnaryOp(
386+
op="not",
387+
lineno=0,
388+
col_offset=0,
389+
end_lineno=None,
390+
end_col_offset=None,
391+
parent=node.parent,
392+
)
393+
new_node.postinit(operand=new_boolop)
394+
395+
self.add_message(
396+
"improve-conditionals",
397+
node=target_node,
398+
args=(new_node.as_string(),),
399+
confidence=HIGH,
400+
)
401+
323402

324403
def register(linter: PyLinter) -> None:
325404
linter.register_checker(CodeStyleChecker(linter))
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# pylint: disable=missing-docstring
2+
3+
def f1(expr, node_cls, x, y, z):
4+
if isinstance(expr, node_cls) and expr.attrname == "__init__":
5+
...
6+
elif isinstance(expr, node_cls) or expr.attrname == "__init__":
7+
...
8+
elif not isinstance(expr, node_cls) and expr.attrname == "__init__":
9+
...
10+
elif not isinstance(expr, node_cls) and expr.attrname != "__init__":
11+
...
12+
elif not isinstance(expr, node_cls) or expr.attrname == "__init__":
13+
...
14+
15+
if not isinstance(expr, node_cls) or expr.attrname != "__init__": # [improve-conditionals]
16+
...
17+
elif x > 0 or y >= 1: # [improve-conditionals]
18+
...
19+
elif x < 0 or y <= 1: # [improve-conditionals]
20+
...
21+
elif not x or y not in z: # [improve-conditionals]
22+
...
23+
elif not (not x or not y): # [improve-conditionals]
24+
...
25+
elif x and (not y or not z): # [improve-conditionals]
26+
...
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[MAIN]
2+
load-plugins=pylint.extensions.code_style
3+
enable=improve-conditionals
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
improve-conditionals:15:7:15:68:f1:Rewrite conditional expression to 'not (isinstance(expr, node_cls) and expr.attrname == '__init__')':HIGH
2+
improve-conditionals:17:9:17:24:f1:Rewrite conditional expression to 'not (x <= 0 and y < 1)':HIGH
3+
improve-conditionals:19:9:19:24:f1:Rewrite conditional expression to 'not (x >= 0 and y > 1)':HIGH
4+
improve-conditionals:21:9:21:28:f1:Rewrite conditional expression to 'not (x and y in z)':HIGH
5+
improve-conditionals:23:9:23:29:f1:Rewrite conditional expression to 'x and y':HIGH
6+
improve-conditionals:25:16:25:30:f1:Rewrite conditional expression to 'not (y and z)':HIGH

0 commit comments

Comments
 (0)