From 80510ae02eb0318f5ff7463b2ab159161608bb8a Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 30 Sep 2025 00:18:13 +0200 Subject: [PATCH 1/8] Add improve-conditionals check in the CodeStyle extension --- .../messages/i/improve-conditionals/bad.py | 4 + .../messages/i/improve-conditionals/good.py | 3 + .../messages/i/improve-conditionals/pylintrc | 2 + doc/user_guide/checkers/extensions.rst | 2 + doc/user_guide/messages/messages_overview.rst | 1 + doc/whatsnew/fragments/10600.new_check | 3 + pylint/extensions/code_style.py | 81 ++++++++++++++++++- .../ext/code_style/cs_improve_conditionals.py | 26 ++++++ .../ext/code_style/cs_improve_conditionals.rc | 3 + .../code_style/cs_improve_conditionals.txt | 6 ++ 10 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 doc/data/messages/i/improve-conditionals/bad.py create mode 100644 doc/data/messages/i/improve-conditionals/good.py create mode 100644 doc/data/messages/i/improve-conditionals/pylintrc create mode 100644 doc/whatsnew/fragments/10600.new_check create mode 100644 tests/functional/ext/code_style/cs_improve_conditionals.py create mode 100644 tests/functional/ext/code_style/cs_improve_conditionals.rc create mode 100644 tests/functional/ext/code_style/cs_improve_conditionals.txt diff --git a/doc/data/messages/i/improve-conditionals/bad.py b/doc/data/messages/i/improve-conditionals/bad.py new file mode 100644 index 0000000000..85d0473b29 --- /dev/null +++ b/doc/data/messages/i/improve-conditionals/bad.py @@ -0,0 +1,4 @@ +def func(expr, node_cls): + # +1:[improve-conditionals] + if not isinstance(expr, node_cls) or expr.attrname != "__init__": + ... diff --git a/doc/data/messages/i/improve-conditionals/good.py b/doc/data/messages/i/improve-conditionals/good.py new file mode 100644 index 0000000000..55e3eedf40 --- /dev/null +++ b/doc/data/messages/i/improve-conditionals/good.py @@ -0,0 +1,3 @@ +def func(expr, node_cls): + if not (isinstance(expr, node_cls) and expr.attrname == "__init__"): + ... diff --git a/doc/data/messages/i/improve-conditionals/pylintrc b/doc/data/messages/i/improve-conditionals/pylintrc new file mode 100644 index 0000000000..8663ab085d --- /dev/null +++ b/doc/data/messages/i/improve-conditionals/pylintrc @@ -0,0 +1,2 @@ +[MAIN] +load-plugins=pylint.extensions.code_style diff --git a/doc/user_guide/checkers/extensions.rst b/doc/user_guide/checkers/extensions.rst index c9dead7ca3..e289cc7bcd 100644 --- a/doc/user_guide/checkers/extensions.rst +++ b/doc/user_guide/checkers/extensions.rst @@ -92,6 +92,8 @@ Code Style checker Messages Using math.inf or math.nan permits to benefit from typing and it is up to 4 times faster than a float call (after the initial import of math). This check also catches typos in float calls as a side effect. +:improve-conditionals (R6107): *Rewrite conditional expression to '%s'* + Rewrite negated if expressions to improve readability. .. _pylint.extensions.comparison_placement: diff --git a/doc/user_guide/messages/messages_overview.rst b/doc/user_guide/messages/messages_overview.rst index 87685835bb..dc97341598 100644 --- a/doc/user_guide/messages/messages_overview.rst +++ b/doc/user_guide/messages/messages_overview.rst @@ -516,6 +516,7 @@ All messages in the refactor category: refactor/duplicate-code refactor/else-if-used refactor/empty-comment + refactor/improve-conditionals refactor/inconsistent-return-statements refactor/literal-comparison refactor/magic-value-comparison diff --git a/doc/whatsnew/fragments/10600.new_check b/doc/whatsnew/fragments/10600.new_check new file mode 100644 index 0000000000..e641880445 --- /dev/null +++ b/doc/whatsnew/fragments/10600.new_check @@ -0,0 +1,3 @@ +Add :ref:`improve-conditionals` check to the Code Style extension. + +Refs #10600 diff --git a/pylint/extensions/code_style.py b/pylint/extensions/code_style.py index d8ea869cb9..c77c908a6c 100644 --- a/pylint/extensions/code_style.py +++ b/pylint/extensions/code_style.py @@ -5,13 +5,14 @@ from __future__ import annotations import difflib +from copy import copy from typing import TYPE_CHECKING, TypeGuard, cast from astroid import nodes from pylint.checkers import BaseChecker, utils from pylint.checkers.utils import only_required_for_messages, safe_infer -from pylint.interfaces import INFERENCE +from pylint.interfaces import HIGH, INFERENCE if TYPE_CHECKING: from pylint.lint import PyLinter @@ -82,6 +83,14 @@ class CodeStyleChecker(BaseChecker): "to 4 times faster than a float call (after the initial import of math). " "This check also catches typos in float calls as a side effect.", ), + "R6107": ( + "Rewrite conditional expression to '%s'", + "improve-conditionals", + "Rewrite negated if expressions to improve readability.", + { + # "default_enabled": False, + }, + ), } options = ( ( @@ -356,6 +365,76 @@ def visit_assign(self, node: nodes.Assign) -> None: confidence=INFERENCE, ) + @staticmethod + def _can_be_inverted(node: nodes.NodeNG) -> bool: + match node: + case nodes.UnaryOp(op="not"): + return True + case nodes.Compare( + ops=[("!=" | "not in", _)] + | [("<" | "<=" | ">" | ">=", nodes.Const(value=int()))] + ): + return True + return False + + @staticmethod + def _invert_node(node: nodes.NodeNG) -> nodes.NodeNG: + match node: + case nodes.UnaryOp(op="not"): + new_node = copy(node.operand) + new_node.parent = node + return new_node + case nodes.Compare(left=left, ops=[(op, n)]): + new_node = copy(node) + match op: + case "!=": + new_op = "==" + case "not in": + new_op = "in" + case "<": + new_op = ">=" + case "<=": + new_op = ">" + case ">": + new_op = "<=" + case ">=": + new_op = "<" + case _: # pragma: no cover + raise AssertionError + new_node.postinit(left=left, ops=[(new_op, n)]) + return new_node + case _: # pragma: no cover + raise AssertionError + + @only_required_for_messages("improve-conditionals") + def visit_boolop(self, node: nodes.BoolOp) -> None: + if node.op == "or" and all(self._can_be_inverted(val) for val in node.values): + new_boolop = copy(node) + new_boolop.op = "and" + new_boolop.postinit([self._invert_node(val) for val in node.values]) + + if isinstance(node.parent, nodes.UnaryOp) and node.parent.op == "not": + target_node = node.parent + new_node = new_boolop + else: + target_node = node + new_node = nodes.UnaryOp( + op="not", + lineno=0, + col_offset=0, + end_lineno=None, + end_col_offset=None, + parent=node.parent, + ) + new_node.postinit(operand=new_boolop) + + self.add_message( + "improve-conditionals", + node=target_node, + args=(new_node.as_string(),), + confidence=HIGH, + ) + def register(linter: PyLinter) -> None: linter.register_checker(CodeStyleChecker(linter)) diff --git a/tests/functional/ext/code_style/cs_improve_conditionals.py b/tests/functional/ext/code_style/cs_improve_conditionals.py new file mode 100644 index 0000000000..9fee26a4fe --- /dev/null +++ b/tests/functional/ext/code_style/cs_improve_conditionals.py @@ -0,0 +1,26 @@ +# pylint: disable=missing-docstring + +def f1(expr, node_cls, x, y, z): + if isinstance(expr, node_cls) and expr.attrname == "__init__": + ... + elif isinstance(expr, node_cls) or expr.attrname == "__init__": + ... + elif not isinstance(expr, node_cls) and expr.attrname == "__init__": + ... + elif not isinstance(expr, node_cls) and expr.attrname != "__init__": + ... + elif not isinstance(expr, node_cls) or expr.attrname == "__init__": + ... + + if not isinstance(expr, node_cls) or expr.attrname != "__init__": # [improve-conditionals] + ... + elif x > 0 or y >= 1: # [improve-conditionals] + ... + elif x < 0 or y <= 1: # [improve-conditionals] + ... + elif not x or y not in z: # [improve-conditionals] + ... + elif not (not x or not y): # [improve-conditionals] + ... + elif x and (not y or not z): # [improve-conditionals] + ... diff --git a/tests/functional/ext/code_style/cs_improve_conditionals.rc b/tests/functional/ext/code_style/cs_improve_conditionals.rc new file mode 100644 index 0000000000..74d18e403f --- /dev/null +++ b/tests/functional/ext/code_style/cs_improve_conditionals.rc @@ -0,0 +1,3 @@ +[MAIN] +load-plugins=pylint.extensions.code_style +enable=improve-conditionals diff --git a/tests/functional/ext/code_style/cs_improve_conditionals.txt b/tests/functional/ext/code_style/cs_improve_conditionals.txt new file mode 100644 index 0000000000..c2e00086e3 --- /dev/null +++ b/tests/functional/ext/code_style/cs_improve_conditionals.txt @@ -0,0 +1,6 @@ +improve-conditionals:15:7:15:68:f1:Rewrite conditional expression to 'not (isinstance(expr, node_cls) and expr.attrname == '__init__')':HIGH +improve-conditionals:17:9:17:24:f1:Rewrite conditional expression to 'not (x <= 0 and y < 1)':HIGH +improve-conditionals:19:9:19:24:f1:Rewrite conditional expression to 'not (x >= 0 and y > 1)':HIGH +improve-conditionals:21:9:21:28:f1:Rewrite conditional expression to 'not (x and y in z)':HIGH +improve-conditionals:23:9:23:29:f1:Rewrite conditional expression to 'x and y':HIGH +improve-conditionals:25:16:25:30:f1:Rewrite conditional expression to 'not (y and z)':HIGH From 2b8af0e2cfc3263142f33316fe34ea1357ac1fd6 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 1 Oct 2025 21:12:55 +0200 Subject: [PATCH 2/8] Don't emit message for 'x < 0 or x > 100' --- pylint/extensions/code_style.py | 38 +++++++++++++------ .../ext/code_style/cs_improve_conditionals.py | 9 +++-- .../code_style/cs_improve_conditionals.txt | 10 ++--- 3 files changed, 37 insertions(+), 20 deletions(-) diff --git a/pylint/extensions/code_style.py b/pylint/extensions/code_style.py index c77c908a6c..0f55b1cafd 100644 --- a/pylint/extensions/code_style.py +++ b/pylint/extensions/code_style.py @@ -6,6 +6,7 @@ import difflib from copy import copy +from enum import IntFlag, auto from typing import TYPE_CHECKING, TypeGuard, cast from astroid import nodes @@ -18,6 +19,12 @@ from pylint.lint import PyLinter +class InvertibleValues(IntFlag): + NO = 0 + YES = auto() + EXPLICIT_NEGATION = auto() + + class CodeStyleChecker(BaseChecker): """Checkers that can improve code consistency. @@ -366,16 +373,21 @@ def visit_assign(self, node: nodes.Assign) -> None: ) @staticmethod - def _can_be_inverted(node: nodes.NodeNG) -> bool: - match node: - case nodes.UnaryOp(op="not"): - return True - case nodes.Compare( - ops=[("!=" | "not in", _)] - | [("<" | "<=" | ">" | ">=", nodes.Const(value=int()))] - ): - return True - return False + def _can_be_inverted(values: list[nodes.NodeNG]) -> InvertibleValues: + invertible = InvertibleValues.NO + for node in values: + match node: + case nodes.UnaryOp(op="not") | nodes.Compare( + ops=[("!=" | "not in", _)] + ): + invertible |= InvertibleValues.EXPLICIT_NEGATION + case nodes.Compare( + ops=[("<" | "<=" | ">" | ">=", nodes.Const(value=int()))] + ): + invertible |= InvertibleValues.YES + case _: + return InvertibleValues.NO + return invertible @staticmethod def _invert_node(node: nodes.NodeNG) -> nodes.NodeNG: @@ -408,7 +420,11 @@ def _invert_node(node: nodes.NodeNG) -> nodes.NodeNG: @only_required_for_messages("improve-conditionals") def visit_boolop(self, node: nodes.BoolOp) -> None: - if node.op == "or" and all(self._can_be_inverted(val) for val in node.values): + if ( + node.op == "or" + and (invertible := self._can_be_inverted(node.values)) + and invertible & InvertibleValues.EXPLICIT_NEGATION + ): new_boolop = copy(node) new_boolop.op = "and" new_boolop.postinit([self._invert_node(val) for val in node.values]) diff --git a/tests/functional/ext/code_style/cs_improve_conditionals.py b/tests/functional/ext/code_style/cs_improve_conditionals.py index 9fee26a4fe..42ad04249e 100644 --- a/tests/functional/ext/code_style/cs_improve_conditionals.py +++ b/tests/functional/ext/code_style/cs_improve_conditionals.py @@ -12,11 +12,14 @@ def f1(expr, node_cls, x, y, z): elif not isinstance(expr, node_cls) or expr.attrname == "__init__": ... - if not isinstance(expr, node_cls) or expr.attrname != "__init__": # [improve-conditionals] + if x < 0 or x > 100: + ... + elif x > 0 or y >= 1: ... - elif x > 0 or y >= 1: # [improve-conditionals] + elif x < 0 or y <= 1: ... - elif x < 0 or y <= 1: # [improve-conditionals] + + if not isinstance(expr, node_cls) or expr.attrname != "__init__": # [improve-conditionals] ... elif not x or y not in z: # [improve-conditionals] ... diff --git a/tests/functional/ext/code_style/cs_improve_conditionals.txt b/tests/functional/ext/code_style/cs_improve_conditionals.txt index c2e00086e3..e19ff77f3f 100644 --- a/tests/functional/ext/code_style/cs_improve_conditionals.txt +++ b/tests/functional/ext/code_style/cs_improve_conditionals.txt @@ -1,6 +1,4 @@ -improve-conditionals:15:7:15:68:f1:Rewrite conditional expression to 'not (isinstance(expr, node_cls) and expr.attrname == '__init__')':HIGH -improve-conditionals:17:9:17:24:f1:Rewrite conditional expression to 'not (x <= 0 and y < 1)':HIGH -improve-conditionals:19:9:19:24:f1:Rewrite conditional expression to 'not (x >= 0 and y > 1)':HIGH -improve-conditionals:21:9:21:28:f1:Rewrite conditional expression to 'not (x and y in z)':HIGH -improve-conditionals:23:9:23:29:f1:Rewrite conditional expression to 'x and y':HIGH -improve-conditionals:25:16:25:30:f1:Rewrite conditional expression to 'not (y and z)':HIGH +improve-conditionals:22:7:22:68:f1:Rewrite conditional expression to 'not (isinstance(expr, node_cls) and expr.attrname == '__init__')':HIGH +improve-conditionals:24:9:24:28:f1:Rewrite conditional expression to 'not (x and y in z)':HIGH +improve-conditionals:26:9:26:29:f1:Rewrite conditional expression to 'x and y':HIGH +improve-conditionals:28:16:28:30:f1:Rewrite conditional expression to 'not (y and z)':HIGH From 6a40da3839125ae3be0cdd84e380e3ef3f8943a6 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 1 Oct 2025 21:17:15 +0200 Subject: [PATCH 3/8] Code review Co-authored-by: Pierre Sassoulas --- doc/data/messages/i/improve-conditionals/bad.py | 9 +++++---- doc/data/messages/i/improve-conditionals/good.py | 6 +++--- doc/user_guide/checkers/extensions.rst | 4 +++- doc/user_guide/configuration/all-options.rst | 2 +- pylint/extensions/code_style.py | 6 ++++-- 5 files changed, 16 insertions(+), 11 deletions(-) diff --git a/doc/data/messages/i/improve-conditionals/bad.py b/doc/data/messages/i/improve-conditionals/bad.py index 85d0473b29..93bc0dd739 100644 --- a/doc/data/messages/i/improve-conditionals/bad.py +++ b/doc/data/messages/i/improve-conditionals/bad.py @@ -1,4 +1,5 @@ -def func(expr, node_cls): - # +1:[improve-conditionals] - if not isinstance(expr, node_cls) or expr.attrname != "__init__": - ... +def is_penguin(animal): + # Penguins are the only flightless, kneeless sea birds + return animal.is_seabird() and ( + not animal.can_fly() or not animal.has_visible_knee() # [improve-conditionals] + ) diff --git a/doc/data/messages/i/improve-conditionals/good.py b/doc/data/messages/i/improve-conditionals/good.py index 55e3eedf40..4fa3a1d8ae 100644 --- a/doc/data/messages/i/improve-conditionals/good.py +++ b/doc/data/messages/i/improve-conditionals/good.py @@ -1,3 +1,3 @@ -def func(expr, node_cls): - if not (isinstance(expr, node_cls) and expr.attrname == "__init__"): - ... +def is_penguin(animal): + # Penguins are the only flightless, kneeless sea birds + return animal.is_seabird() and not (animal.can_fly() or animal.has_visible_knee()) diff --git a/doc/user_guide/checkers/extensions.rst b/doc/user_guide/checkers/extensions.rst index e289cc7bcd..96c31f1619 100644 --- a/doc/user_guide/checkers/extensions.rst +++ b/doc/user_guide/checkers/extensions.rst @@ -93,7 +93,9 @@ Code Style checker Messages times faster than a float call (after the initial import of math). This check also catches typos in float calls as a side effect. :improve-conditionals (R6107): *Rewrite conditional expression to '%s'* - Rewrite negated if expressions to improve readability. + Rewrite negated if expressions to improve readability. This style is simpler + and also permits converting long if/elif chains to match case with more ease. + Disabled by default! .. _pylint.extensions.comparison_placement: diff --git a/doc/user_guide/configuration/all-options.rst b/doc/user_guide/configuration/all-options.rst index 309714a0de..e912a4f77b 100644 --- a/doc/user_guide/configuration/all-options.rst +++ b/doc/user_guide/configuration/all-options.rst @@ -233,7 +233,7 @@ Standard Checkers confidence = ["HIGH", "CONTROL_FLOW", "INFERENCE", "INFERENCE_FAILURE", "UNDEFINED"] - disable = ["bad-inline-option", "consider-using-augmented-assign", "deprecated-pragma", "file-ignored", "locally-disabled", "prefer-typing-namedtuple", "raw-checker-failed", "suppressed-message", "use-implicit-booleaness-not-comparison-to-string", "use-implicit-booleaness-not-comparison-to-zero", "use-symbolic-message-instead", "useless-suppression"] + disable = ["bad-inline-option", "consider-using-augmented-assign", "deprecated-pragma", "file-ignored", "improve-conditionals", "locally-disabled", "prefer-typing-namedtuple", "raw-checker-failed", "suppressed-message", "use-implicit-booleaness-not-comparison-to-string", "use-implicit-booleaness-not-comparison-to-zero", "use-symbolic-message-instead", "useless-suppression"] enable = [] diff --git a/pylint/extensions/code_style.py b/pylint/extensions/code_style.py index 0f55b1cafd..0aa0a726eb 100644 --- a/pylint/extensions/code_style.py +++ b/pylint/extensions/code_style.py @@ -93,9 +93,11 @@ class CodeStyleChecker(BaseChecker): "R6107": ( "Rewrite conditional expression to '%s'", "improve-conditionals", - "Rewrite negated if expressions to improve readability.", + "Rewrite negated if expressions to improve readability. This style is simpler " + "and also permits converting long if/elif chains to match case with more ease.\n" + "Disabled by default!", { - # "default_enabled": False, + "default_enabled": False, }, ), } From 1b7090c87e5eb5db64e6d654aada7aa7ed95b0c7 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 2 Oct 2025 20:55:28 +0200 Subject: [PATCH 4/8] Enable improve-conditionals check for pylint itself --- pylintrc | 1 + 1 file changed, 1 insertion(+) diff --git a/pylintrc b/pylintrc index fe653a092c..f824db6466 100644 --- a/pylintrc +++ b/pylintrc @@ -81,6 +81,7 @@ clear-cache-post-run=no # multiple time (only on the command line, not in the configuration file where # it should appear only once). See also the "--disable" option for examples. enable= + improve-conditionals, use-symbolic-message-instead, useless-suppression, From 91cbba1ee86825966b2772795291fdd845fe21fd Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 13 Oct 2025 12:34:19 +0200 Subject: [PATCH 5/8] Deal with 'is not' but exclude 'is not None' --- pylint/extensions/code_style.py | 8 ++++++-- .../functional/ext/code_style/cs_improve_conditionals.py | 9 ++++++++- .../ext/code_style/cs_improve_conditionals.txt | 9 +++++---- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/pylint/extensions/code_style.py b/pylint/extensions/code_style.py index 0aa0a726eb..a280e9673b 100644 --- a/pylint/extensions/code_style.py +++ b/pylint/extensions/code_style.py @@ -379,8 +379,10 @@ def _can_be_inverted(values: list[nodes.NodeNG]) -> InvertibleValues: invertible = InvertibleValues.NO for node in values: match node: - case nodes.UnaryOp(op="not") | nodes.Compare( - ops=[("!=" | "not in", _)] + case nodes.UnaryOp(op="not"): + invertible |= InvertibleValues.EXPLICIT_NEGATION + case nodes.Compare(ops=[("!=" | "not in" | "is not" as op, n)]) if not ( + op == "is not" and isinstance(n, nodes.Const) and n.value is None ): invertible |= InvertibleValues.EXPLICIT_NEGATION case nodes.Compare( @@ -405,6 +407,8 @@ def _invert_node(node: nodes.NodeNG) -> nodes.NodeNG: new_op = "==" case "not in": new_op = "in" + case "is not": + new_op = "is" case "<": new_op = ">=" case "<=": diff --git a/tests/functional/ext/code_style/cs_improve_conditionals.py b/tests/functional/ext/code_style/cs_improve_conditionals.py index 42ad04249e..383c6dc00d 100644 --- a/tests/functional/ext/code_style/cs_improve_conditionals.py +++ b/tests/functional/ext/code_style/cs_improve_conditionals.py @@ -1,4 +1,4 @@ -# pylint: disable=missing-docstring +# pylint: disable=missing-docstring,too-many-branches def f1(expr, node_cls, x, y, z): if isinstance(expr, node_cls) and expr.attrname == "__init__": @@ -19,10 +19,17 @@ def f1(expr, node_cls, x, y, z): elif x < 0 or y <= 1: ... + if x is not None or y is not None: + ... + elif not isinstance(expr, node_cls) or x is not None: + ... + if not isinstance(expr, node_cls) or expr.attrname != "__init__": # [improve-conditionals] ... elif not x or y not in z: # [improve-conditionals] ... + elif not x or y is not z: # [improve-conditionals] + ... elif not (not x or not y): # [improve-conditionals] ... elif x and (not y or not z): # [improve-conditionals] diff --git a/tests/functional/ext/code_style/cs_improve_conditionals.txt b/tests/functional/ext/code_style/cs_improve_conditionals.txt index e19ff77f3f..54440f751a 100644 --- a/tests/functional/ext/code_style/cs_improve_conditionals.txt +++ b/tests/functional/ext/code_style/cs_improve_conditionals.txt @@ -1,4 +1,5 @@ -improve-conditionals:22:7:22:68:f1:Rewrite conditional expression to 'not (isinstance(expr, node_cls) and expr.attrname == '__init__')':HIGH -improve-conditionals:24:9:24:28:f1:Rewrite conditional expression to 'not (x and y in z)':HIGH -improve-conditionals:26:9:26:29:f1:Rewrite conditional expression to 'x and y':HIGH -improve-conditionals:28:16:28:30:f1:Rewrite conditional expression to 'not (y and z)':HIGH +improve-conditionals:27:7:27:68:f1:Rewrite conditional expression to 'not (isinstance(expr, node_cls) and expr.attrname == '__init__')':HIGH +improve-conditionals:29:9:29:28:f1:Rewrite conditional expression to 'not (x and y in z)':HIGH +improve-conditionals:31:9:31:28:f1:Rewrite conditional expression to 'not (x and y is z)':HIGH +improve-conditionals:33:9:33:29:f1:Rewrite conditional expression to 'x and y':HIGH +improve-conditionals:35:16:35:30:f1:Rewrite conditional expression to 'not (y and z)':HIGH From 094f05530cfa5f971890653f832547fbac87144b Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 13 Oct 2025 12:34:45 +0200 Subject: [PATCH 6/8] Increase test coverage --- tests/functional/ext/code_style/cs_improve_conditionals.py | 4 ++++ tests/functional/ext/code_style/cs_improve_conditionals.txt | 2 ++ 2 files changed, 6 insertions(+) diff --git a/tests/functional/ext/code_style/cs_improve_conditionals.py b/tests/functional/ext/code_style/cs_improve_conditionals.py index 383c6dc00d..7c1c57195d 100644 --- a/tests/functional/ext/code_style/cs_improve_conditionals.py +++ b/tests/functional/ext/code_style/cs_improve_conditionals.py @@ -34,3 +34,7 @@ def f1(expr, node_cls, x, y, z): ... elif x and (not y or not z): # [improve-conditionals] ... + elif not x or y < 0 or z <= 0: # [improve-conditionals] + ... + elif not x or y > 0 or z >= 0: # [improve-conditionals] + ... diff --git a/tests/functional/ext/code_style/cs_improve_conditionals.txt b/tests/functional/ext/code_style/cs_improve_conditionals.txt index 54440f751a..887f9b82ab 100644 --- a/tests/functional/ext/code_style/cs_improve_conditionals.txt +++ b/tests/functional/ext/code_style/cs_improve_conditionals.txt @@ -3,3 +3,5 @@ improve-conditionals:29:9:29:28:f1:Rewrite conditional expression to 'not (x and improve-conditionals:31:9:31:28:f1:Rewrite conditional expression to 'not (x and y is z)':HIGH improve-conditionals:33:9:33:29:f1:Rewrite conditional expression to 'x and y':HIGH improve-conditionals:35:16:35:30:f1:Rewrite conditional expression to 'not (y and z)':HIGH +improve-conditionals:37:9:37:33:f1:Rewrite conditional expression to 'not (x and y >= 0 and z > 0)':HIGH +improve-conditionals:39:9:39:33:f1:Rewrite conditional expression to 'not (x and y <= 0 and z < 0)':HIGH From e94d54e2d9cafdc7bf07220bd3df50690f4d3980 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 13 Oct 2025 12:54:35 +0200 Subject: [PATCH 7/8] Change name to 'consider-rewriting-conditional' Co-authored-by: Pierre Sassoulas --- .../consider-rewriting-conditional}/bad.py | 4 +++- .../consider-rewriting-conditional}/good.py | 0 .../consider-rewriting-conditional}/pylintrc | 0 doc/user_guide/checkers/extensions.rst | 2 +- doc/user_guide/configuration/all-options.rst | 2 +- doc/user_guide/messages/messages_overview.rst | 2 +- doc/whatsnew/fragments/10600.new_check | 5 ++++- pylint/extensions/code_style.py | 6 +++--- pylintrc | 2 +- ...ls.py => cs_consider_rewriting_conditional.py} | 15 ++++++++------- ...ls.rc => cs_consider_rewriting_conditional.rc} | 2 +- .../cs_consider_rewriting_conditional.txt | 7 +++++++ .../ext/code_style/cs_improve_conditionals.txt | 7 ------- 13 files changed, 30 insertions(+), 24 deletions(-) rename doc/data/messages/{i/improve-conditionals => c/consider-rewriting-conditional}/bad.py (51%) rename doc/data/messages/{i/improve-conditionals => c/consider-rewriting-conditional}/good.py (100%) rename doc/data/messages/{i/improve-conditionals => c/consider-rewriting-conditional}/pylintrc (100%) rename tests/functional/ext/code_style/{cs_improve_conditionals.py => cs_consider_rewriting_conditional.py} (65%) rename tests/functional/ext/code_style/{cs_improve_conditionals.rc => cs_consider_rewriting_conditional.rc} (56%) create mode 100644 tests/functional/ext/code_style/cs_consider_rewriting_conditional.txt delete mode 100644 tests/functional/ext/code_style/cs_improve_conditionals.txt diff --git a/doc/data/messages/i/improve-conditionals/bad.py b/doc/data/messages/c/consider-rewriting-conditional/bad.py similarity index 51% rename from doc/data/messages/i/improve-conditionals/bad.py rename to doc/data/messages/c/consider-rewriting-conditional/bad.py index 93bc0dd739..e498b6cb7c 100644 --- a/doc/data/messages/i/improve-conditionals/bad.py +++ b/doc/data/messages/c/consider-rewriting-conditional/bad.py @@ -1,5 +1,7 @@ def is_penguin(animal): # Penguins are the only flightless, kneeless sea birds return animal.is_seabird() and ( - not animal.can_fly() or not animal.has_visible_knee() # [improve-conditionals] + # +1: [consider-rewriting-conditional] + not animal.can_fly() + or not animal.has_visible_knee() ) diff --git a/doc/data/messages/i/improve-conditionals/good.py b/doc/data/messages/c/consider-rewriting-conditional/good.py similarity index 100% rename from doc/data/messages/i/improve-conditionals/good.py rename to doc/data/messages/c/consider-rewriting-conditional/good.py diff --git a/doc/data/messages/i/improve-conditionals/pylintrc b/doc/data/messages/c/consider-rewriting-conditional/pylintrc similarity index 100% rename from doc/data/messages/i/improve-conditionals/pylintrc rename to doc/data/messages/c/consider-rewriting-conditional/pylintrc diff --git a/doc/user_guide/checkers/extensions.rst b/doc/user_guide/checkers/extensions.rst index 96c31f1619..14e5ae0107 100644 --- a/doc/user_guide/checkers/extensions.rst +++ b/doc/user_guide/checkers/extensions.rst @@ -92,7 +92,7 @@ Code Style checker Messages Using math.inf or math.nan permits to benefit from typing and it is up to 4 times faster than a float call (after the initial import of math). This check also catches typos in float calls as a side effect. -:improve-conditionals (R6107): *Rewrite conditional expression to '%s'* +:consider-rewriting-conditional (R6107): *Rewrite conditional expression to '%s'* Rewrite negated if expressions to improve readability. This style is simpler and also permits converting long if/elif chains to match case with more ease. Disabled by default! diff --git a/doc/user_guide/configuration/all-options.rst b/doc/user_guide/configuration/all-options.rst index e912a4f77b..084388d6a2 100644 --- a/doc/user_guide/configuration/all-options.rst +++ b/doc/user_guide/configuration/all-options.rst @@ -233,7 +233,7 @@ Standard Checkers confidence = ["HIGH", "CONTROL_FLOW", "INFERENCE", "INFERENCE_FAILURE", "UNDEFINED"] - disable = ["bad-inline-option", "consider-using-augmented-assign", "deprecated-pragma", "file-ignored", "improve-conditionals", "locally-disabled", "prefer-typing-namedtuple", "raw-checker-failed", "suppressed-message", "use-implicit-booleaness-not-comparison-to-string", "use-implicit-booleaness-not-comparison-to-zero", "use-symbolic-message-instead", "useless-suppression"] + disable = ["bad-inline-option", "consider-rewriting-conditional", "consider-using-augmented-assign", "deprecated-pragma", "file-ignored", "locally-disabled", "prefer-typing-namedtuple", "raw-checker-failed", "suppressed-message", "use-implicit-booleaness-not-comparison-to-string", "use-implicit-booleaness-not-comparison-to-zero", "use-symbolic-message-instead", "useless-suppression"] enable = [] diff --git a/doc/user_guide/messages/messages_overview.rst b/doc/user_guide/messages/messages_overview.rst index dc97341598..4112ca47a3 100644 --- a/doc/user_guide/messages/messages_overview.rst +++ b/doc/user_guide/messages/messages_overview.rst @@ -494,6 +494,7 @@ All messages in the refactor category: refactor/consider-math-not-float refactor/consider-merging-isinstance refactor/consider-refactoring-into-while-condition + refactor/consider-rewriting-conditional refactor/consider-swap-variables refactor/consider-using-alias refactor/consider-using-assignment-expr @@ -516,7 +517,6 @@ All messages in the refactor category: refactor/duplicate-code refactor/else-if-used refactor/empty-comment - refactor/improve-conditionals refactor/inconsistent-return-statements refactor/literal-comparison refactor/magic-value-comparison diff --git a/doc/whatsnew/fragments/10600.new_check b/doc/whatsnew/fragments/10600.new_check index e641880445..135951842b 100644 --- a/doc/whatsnew/fragments/10600.new_check +++ b/doc/whatsnew/fragments/10600.new_check @@ -1,3 +1,6 @@ -Add :ref:`improve-conditionals` check to the Code Style extension. +Add ``consider-rewriting-conditional`` check to the Code Style extension to suggest +``not (x and y)`` instead of ``not x or not y`` in order to facilitate the detection +of match case refactors. The code style extension must be enabled and +``consider-rewriting-conditional`` itself needs to be explicitly enabled. Refs #10600 diff --git a/pylint/extensions/code_style.py b/pylint/extensions/code_style.py index a280e9673b..e17c9358c0 100644 --- a/pylint/extensions/code_style.py +++ b/pylint/extensions/code_style.py @@ -92,7 +92,7 @@ class CodeStyleChecker(BaseChecker): ), "R6107": ( "Rewrite conditional expression to '%s'", - "improve-conditionals", + "consider-rewriting-conditional", "Rewrite negated if expressions to improve readability. This style is simpler " "and also permits converting long if/elif chains to match case with more ease.\n" "Disabled by default!", @@ -424,7 +424,7 @@ def _invert_node(node: nodes.NodeNG) -> nodes.NodeNG: case _: # pragma: no cover raise AssertionError - @only_required_for_messages("improve-conditionals") + @only_required_for_messages("consider-rewriting-conditional") def visit_boolop(self, node: nodes.BoolOp) -> None: if ( node.op == "or" @@ -451,7 +451,7 @@ def visit_boolop(self, node: nodes.BoolOp) -> None: new_node.postinit(operand=new_boolop) self.add_message( - "improve-conditionals", + "consider-rewriting-conditional", node=target_node, args=(new_node.as_string(),), confidence=HIGH, diff --git a/pylintrc b/pylintrc index f824db6466..43fba9fa4c 100644 --- a/pylintrc +++ b/pylintrc @@ -81,7 +81,7 @@ clear-cache-post-run=no # multiple time (only on the command line, not in the configuration file where # it should appear only once). See also the "--disable" option for examples. enable= - improve-conditionals, + consider-rewriting-conditional, use-symbolic-message-instead, useless-suppression, diff --git a/tests/functional/ext/code_style/cs_improve_conditionals.py b/tests/functional/ext/code_style/cs_consider_rewriting_conditional.py similarity index 65% rename from tests/functional/ext/code_style/cs_improve_conditionals.py rename to tests/functional/ext/code_style/cs_consider_rewriting_conditional.py index 7c1c57195d..3e72d62a4b 100644 --- a/tests/functional/ext/code_style/cs_improve_conditionals.py +++ b/tests/functional/ext/code_style/cs_consider_rewriting_conditional.py @@ -24,17 +24,18 @@ def f1(expr, node_cls, x, y, z): elif not isinstance(expr, node_cls) or x is not None: ... - if not isinstance(expr, node_cls) or expr.attrname != "__init__": # [improve-conditionals] + # +1: [consider-rewriting-conditional] + if not isinstance(expr, node_cls) or expr.attrname != "__init__": ... - elif not x or y not in z: # [improve-conditionals] + elif not x or y not in z: # [consider-rewriting-conditional] ... - elif not x or y is not z: # [improve-conditionals] + elif not x or y is not z: # [consider-rewriting-conditional] ... - elif not (not x or not y): # [improve-conditionals] + elif not (not x or not y): # [consider-rewriting-conditional] ... - elif x and (not y or not z): # [improve-conditionals] + elif x and (not y or not z): # [consider-rewriting-conditional] ... - elif not x or y < 0 or z <= 0: # [improve-conditionals] + elif not x or y < 0 or z <= 0: # [consider-rewriting-conditional] ... - elif not x or y > 0 or z >= 0: # [improve-conditionals] + elif not x or y > 0 or z >= 0: # [consider-rewriting-conditional] ... diff --git a/tests/functional/ext/code_style/cs_improve_conditionals.rc b/tests/functional/ext/code_style/cs_consider_rewriting_conditional.rc similarity index 56% rename from tests/functional/ext/code_style/cs_improve_conditionals.rc rename to tests/functional/ext/code_style/cs_consider_rewriting_conditional.rc index 74d18e403f..794559fb63 100644 --- a/tests/functional/ext/code_style/cs_improve_conditionals.rc +++ b/tests/functional/ext/code_style/cs_consider_rewriting_conditional.rc @@ -1,3 +1,3 @@ [MAIN] load-plugins=pylint.extensions.code_style -enable=improve-conditionals +enable=consider-rewriting-conditional diff --git a/tests/functional/ext/code_style/cs_consider_rewriting_conditional.txt b/tests/functional/ext/code_style/cs_consider_rewriting_conditional.txt new file mode 100644 index 0000000000..2ada42ac0d --- /dev/null +++ b/tests/functional/ext/code_style/cs_consider_rewriting_conditional.txt @@ -0,0 +1,7 @@ +consider-rewriting-conditional:28:7:28:68:f1:Rewrite conditional expression to 'not (isinstance(expr, node_cls) and expr.attrname == '__init__')':HIGH +consider-rewriting-conditional:30:9:30:28:f1:Rewrite conditional expression to 'not (x and y in z)':HIGH +consider-rewriting-conditional:32:9:32:28:f1:Rewrite conditional expression to 'not (x and y is z)':HIGH +consider-rewriting-conditional:34:9:34:29:f1:Rewrite conditional expression to 'x and y':HIGH +consider-rewriting-conditional:36:16:36:30:f1:Rewrite conditional expression to 'not (y and z)':HIGH +consider-rewriting-conditional:38:9:38:33:f1:Rewrite conditional expression to 'not (x and y >= 0 and z > 0)':HIGH +consider-rewriting-conditional:40:9:40:33:f1:Rewrite conditional expression to 'not (x and y <= 0 and z < 0)':HIGH diff --git a/tests/functional/ext/code_style/cs_improve_conditionals.txt b/tests/functional/ext/code_style/cs_improve_conditionals.txt deleted file mode 100644 index 887f9b82ab..0000000000 --- a/tests/functional/ext/code_style/cs_improve_conditionals.txt +++ /dev/null @@ -1,7 +0,0 @@ -improve-conditionals:27:7:27:68:f1:Rewrite conditional expression to 'not (isinstance(expr, node_cls) and expr.attrname == '__init__')':HIGH -improve-conditionals:29:9:29:28:f1:Rewrite conditional expression to 'not (x and y in z)':HIGH -improve-conditionals:31:9:31:28:f1:Rewrite conditional expression to 'not (x and y is z)':HIGH -improve-conditionals:33:9:33:29:f1:Rewrite conditional expression to 'x and y':HIGH -improve-conditionals:35:16:35:30:f1:Rewrite conditional expression to 'not (y and z)':HIGH -improve-conditionals:37:9:37:33:f1:Rewrite conditional expression to 'not (x and y >= 0 and z > 0)':HIGH -improve-conditionals:39:9:39:33:f1:Rewrite conditional expression to 'not (x and y <= 0 and z < 0)':HIGH From 0f72f23d12559dc00a08acbec41c2ebcf484d0c0 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 13 Oct 2025 13:51:14 +0200 Subject: [PATCH 8/8] Update message --- doc/user_guide/checkers/extensions.rst | 2 +- pylint/extensions/code_style.py | 2 +- .../cs_consider_rewriting_conditional.txt | 14 +++++++------- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/doc/user_guide/checkers/extensions.rst b/doc/user_guide/checkers/extensions.rst index 14e5ae0107..07d3588a29 100644 --- a/doc/user_guide/checkers/extensions.rst +++ b/doc/user_guide/checkers/extensions.rst @@ -92,7 +92,7 @@ Code Style checker Messages Using math.inf or math.nan permits to benefit from typing and it is up to 4 times faster than a float call (after the initial import of math). This check also catches typos in float calls as a side effect. -:consider-rewriting-conditional (R6107): *Rewrite conditional expression to '%s'* +:consider-rewriting-conditional (R6107): *Consider rewriting conditional expression to '%s'* Rewrite negated if expressions to improve readability. This style is simpler and also permits converting long if/elif chains to match case with more ease. Disabled by default! diff --git a/pylint/extensions/code_style.py b/pylint/extensions/code_style.py index e17c9358c0..0005084c63 100644 --- a/pylint/extensions/code_style.py +++ b/pylint/extensions/code_style.py @@ -91,7 +91,7 @@ class CodeStyleChecker(BaseChecker): "This check also catches typos in float calls as a side effect.", ), "R6107": ( - "Rewrite conditional expression to '%s'", + "Consider rewriting conditional expression to '%s'", "consider-rewriting-conditional", "Rewrite negated if expressions to improve readability. This style is simpler " "and also permits converting long if/elif chains to match case with more ease.\n" diff --git a/tests/functional/ext/code_style/cs_consider_rewriting_conditional.txt b/tests/functional/ext/code_style/cs_consider_rewriting_conditional.txt index 2ada42ac0d..fb1c1e026c 100644 --- a/tests/functional/ext/code_style/cs_consider_rewriting_conditional.txt +++ b/tests/functional/ext/code_style/cs_consider_rewriting_conditional.txt @@ -1,7 +1,7 @@ -consider-rewriting-conditional:28:7:28:68:f1:Rewrite conditional expression to 'not (isinstance(expr, node_cls) and expr.attrname == '__init__')':HIGH -consider-rewriting-conditional:30:9:30:28:f1:Rewrite conditional expression to 'not (x and y in z)':HIGH -consider-rewriting-conditional:32:9:32:28:f1:Rewrite conditional expression to 'not (x and y is z)':HIGH -consider-rewriting-conditional:34:9:34:29:f1:Rewrite conditional expression to 'x and y':HIGH -consider-rewriting-conditional:36:16:36:30:f1:Rewrite conditional expression to 'not (y and z)':HIGH -consider-rewriting-conditional:38:9:38:33:f1:Rewrite conditional expression to 'not (x and y >= 0 and z > 0)':HIGH -consider-rewriting-conditional:40:9:40:33:f1:Rewrite conditional expression to 'not (x and y <= 0 and z < 0)':HIGH +consider-rewriting-conditional:28:7:28:68:f1:Consider rewriting conditional expression to 'not (isinstance(expr, node_cls) and expr.attrname == '__init__')':HIGH +consider-rewriting-conditional:30:9:30:28:f1:Consider rewriting conditional expression to 'not (x and y in z)':HIGH +consider-rewriting-conditional:32:9:32:28:f1:Consider rewriting conditional expression to 'not (x and y is z)':HIGH +consider-rewriting-conditional:34:9:34:29:f1:Consider rewriting conditional expression to 'x and y':HIGH +consider-rewriting-conditional:36:16:36:30:f1:Consider rewriting conditional expression to 'not (y and z)':HIGH +consider-rewriting-conditional:38:9:38:33:f1:Consider rewriting conditional expression to 'not (x and y >= 0 and z > 0)':HIGH +consider-rewriting-conditional:40:9:40:33:f1:Consider rewriting conditional expression to 'not (x and y <= 0 and z < 0)':HIGH