Skip to content

Commit 85e8a11

Browse files
committed
Add improve-conditionals check in the CodeStyle extension
1 parent b970208 commit 85e8a11

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
@@ -92,6 +92,8 @@ Code Style checker Messages
9292
Using math.inf or math.nan permits to benefit from typing and it is up to 4
9393
times faster than a float call (after the initial import of math). This check
9494
also catches typos in float calls as a side effect.
95+
:improve-conditionals (R6107): *Rewrite conditional expression to '%s'*
96+
Rewrite negated if expressions to improve readability.
9597

9698

9799
.. _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
@@ -516,6 +516,7 @@ All messages in the refactor category:
516516
refactor/duplicate-code
517517
refactor/else-if-used
518518
refactor/empty-comment
519+
refactor/improve-conditionals
519520
refactor/inconsistent-return-statements
520521
refactor/literal-comparison
521522
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
@@ -5,13 +5,14 @@
55
from __future__ import annotations
66

77
import difflib
8+
from copy import copy
89
from typing import TYPE_CHECKING, TypeGuard, cast
910

1011
from astroid import nodes
1112

1213
from pylint.checkers import BaseChecker, utils
1314
from pylint.checkers.utils import only_required_for_messages, safe_infer
14-
from pylint.interfaces import INFERENCE
15+
from pylint.interfaces import HIGH, INFERENCE
1516

1617
if TYPE_CHECKING:
1718
from pylint.lint import PyLinter
@@ -82,6 +83,14 @@ class CodeStyleChecker(BaseChecker):
8283
"to 4 times faster than a float call (after the initial import of math). "
8384
"This check also catches typos in float calls as a side effect.",
8485
),
86+
"R6107": (
87+
"Rewrite conditional expression to '%s'",
88+
"improve-conditionals",
89+
"Rewrite negated if expressions to improve readability.",
90+
{
91+
# "default_enabled": False,
92+
},
93+
),
8594
}
8695
options = (
8796
(
@@ -356,6 +365,76 @@ def visit_assign(self, node: nodes.Assign) -> None:
356365
confidence=INFERENCE,
357366
)
358367

368+
@staticmethod
369+
def _can_be_inverted(node: nodes.NodeNG) -> bool:
370+
match node:
371+
case nodes.UnaryOp(op="not"):
372+
return True
373+
case nodes.Compare(
374+
ops=[("!=" | "not in", _)]
375+
| [("<" | "<=" | ">" | ">=", nodes.Const(value=int()))]
376+
):
377+
return True
378+
return False
379+
380+
@staticmethod
381+
def _invert_node(node: nodes.NodeNG) -> nodes.NodeNG:
382+
match node:
383+
case nodes.UnaryOp(op="not"):
384+
new_node = copy(node.operand)
385+
new_node.parent = node
386+
return new_node
387+
case nodes.Compare(left=left, ops=[(op, n)]):
388+
new_node = copy(node)
389+
match op:
390+
case "!=":
391+
new_op = "=="
392+
case "not in":
393+
new_op = "in"
394+
case "<":
395+
new_op = ">="
396+
case "<=":
397+
new_op = ">"
398+
case ">":
399+
new_op = "<="
400+
case ">=":
401+
new_op = "<"
402+
case _: # pragma: no cover
403+
raise AssertionError
404+
new_node.postinit(left=left, ops=[(new_op, n)])
405+
return new_node
406+
case _: # pragma: no cover
407+
raise AssertionError
408+
409+
@only_required_for_messages("improve-conditionals")
410+
def visit_boolop(self, node: nodes.BoolOp) -> None:
411+
if node.op == "or" and all(self._can_be_inverted(val) for val in node.values):
412+
new_boolop = copy(node)
413+
new_boolop.op = "and"
414+
new_boolop.postinit([self._invert_node(val) for val in node.values])
415+
416+
if isinstance(node.parent, nodes.UnaryOp) and node.parent.op == "not":
417+
target_node = node.parent
418+
new_node = new_boolop
419+
else:
420+
target_node = node
421+
new_node = nodes.UnaryOp(
422+
op="not",
423+
lineno=0,
424+
col_offset=0,
425+
end_lineno=None,
426+
end_col_offset=None,
427+
parent=node.parent,
428+
)
429+
new_node.postinit(operand=new_boolop)
430+
431+
self.add_message(
432+
"improve-conditionals",
433+
node=target_node,
434+
args=(new_node.as_string(),),
435+
confidence=HIGH,
436+
)
437+
359438

360439
def register(linter: PyLinter) -> None:
361440
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)