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..93bc0dd739 --- /dev/null +++ b/doc/data/messages/i/improve-conditionals/bad.py @@ -0,0 +1,5 @@ +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 new file mode 100644 index 0000000000..4fa3a1d8ae --- /dev/null +++ b/doc/data/messages/i/improve-conditionals/good.py @@ -0,0 +1,3 @@ +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/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 1ff270a6e4..f7e4a614ce 100644 --- a/doc/user_guide/checkers/extensions.rst +++ b/doc/user_guide/checkers/extensions.rst @@ -88,6 +88,10 @@ Code Style checker Messages 'typing.NamedTuple' uses the well-known 'class' keyword with type-hints for readability (it's also faster as it avoids an internal exec call). Disabled by default! +:improve-conditionals (R6106): *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! .. _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/doc/user_guide/messages/messages_overview.rst b/doc/user_guide/messages/messages_overview.rst index 5b1378474e..b8d4c45b40 100644 --- a/doc/user_guide/messages/messages_overview.rst +++ b/doc/user_guide/messages/messages_overview.rst @@ -515,6 +515,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/checkers/base/name_checker/checker.py b/pylint/checkers/base/name_checker/checker.py index 44b916be22..c08d4bc779 100644 --- a/pylint/checkers/base/name_checker/checker.py +++ b/pylint/checkers/base/name_checker/checker.py @@ -99,7 +99,7 @@ def _redefines_import(node: nodes.AssignName) -> bool: current = node while current and not isinstance(current.parent, nodes.ExceptHandler): current = current.parent - if not current or not utils.error_of_type(current.parent, ImportError): + if not (current and utils.error_of_type(current.parent, ImportError)): return False try_block = current.parent.parent for import_node in try_block.nodes_of_class((nodes.ImportFrom, nodes.Import)): @@ -160,7 +160,7 @@ def _is_multi_naming_match( match is not None and match.lastgroup is not None and match.lastgroup not in EXEMPT_NAME_CATEGORIES - and (node_type != "method" or confidence != interfaces.INFERENCE_FAILURE) + and not (node_type == "method" and confidence == interfaces.INFERENCE_FAILURE) ) diff --git a/pylint/checkers/classes/class_checker.py b/pylint/checkers/classes/class_checker.py index 5bafb0673b..54e90a3ae3 100644 --- a/pylint/checkers/classes/class_checker.py +++ b/pylint/checkers/classes/class_checker.py @@ -186,10 +186,10 @@ def _is_trivial_super_delegation(function: nodes.FunctionDef) -> bool: # Should be a super call with the MRO pointer being the # current class and the type being the current instance. current_scope = function.parent.scope() - if ( - super_call.mro_pointer != current_scope - or not isinstance(super_call.type, astroid.Instance) - or super_call.type.name != current_scope.name + if not ( + super_call.mro_pointer == current_scope + and isinstance(super_call.type, astroid.Instance) + and super_call.type.name == current_scope.name ): return False @@ -1139,8 +1139,9 @@ def _check_unused_private_variables(self, node: nodes.ClassDef) -> None: def _check_unused_private_attributes(self, node: nodes.ClassDef) -> None: for assign_attr in node.nodes_of_class(nodes.AssignAttr): - if not is_attr_private(assign_attr.attrname) or not isinstance( - assign_attr.expr, nodes.Name + if not ( + is_attr_private(assign_attr.attrname) + and isinstance(assign_attr.expr, nodes.Name) ): continue @@ -1928,7 +1929,7 @@ def _check_protected_attribute_access( callee = node.expr.as_string() parents_callee = callee.split(".") for callee in reversed(parents_callee): - if not outer_klass or callee != outer_klass.name: + if not (outer_klass and callee == outer_klass.name): inside_klass = False break diff --git a/pylint/checkers/dataclass_checker.py b/pylint/checkers/dataclass_checker.py index 60b1b23cd4..81399f6256 100644 --- a/pylint/checkers/dataclass_checker.py +++ b/pylint/checkers/dataclass_checker.py @@ -82,7 +82,7 @@ def _check_invalid_field_call(self, node: nodes.Call) -> None: self._check_invalid_field_call_within_call(node, scope_node) return - if not scope_node or not scope_node.is_dataclass: + if not (scope_node and scope_node.is_dataclass): self.add_message( "invalid-field-call", node=node, diff --git a/pylint/checkers/design_analysis.py b/pylint/checkers/design_analysis.py index 49324e07f7..9085c1a4bd 100644 --- a/pylint/checkers/design_analysis.py +++ b/pylint/checkers/design_analysis.py @@ -660,8 +660,8 @@ def visit_if(self, node: nodes.If) -> None: self._check_boolean_expressions(node) branches = 1 # don't double count If nodes coming from some 'elif' - if node.orelse and ( - len(node.orelse) > 1 or not isinstance(node.orelse[0], nodes.If) + if node.orelse and not ( + len(node.orelse) == 1 and isinstance(node.orelse[0], nodes.If) ): branches += 1 self._inc_branch(node, branches) diff --git a/pylint/checkers/exceptions.py b/pylint/checkers/exceptions.py index ae6f8814f1..b8897a17f0 100644 --- a/pylint/checkers/exceptions.py +++ b/pylint/checkers/exceptions.py @@ -348,7 +348,7 @@ def _check_misplaced_bare_raise(self, node: nodes.Raise) -> None: current = current.parent expected = (nodes.ExceptHandler,) - if not current or not isinstance(current.parent, expected): + if not (current and isinstance(current.parent, expected)): self.add_message("misplaced-bare-raise", node=node, confidence=HIGH) def _check_bad_exception_cause(self, node: nodes.Raise) -> None: diff --git a/pylint/checkers/imports.py b/pylint/checkers/imports.py index 1ac5dd8241..8c9fd57dd6 100644 --- a/pylint/checkers/imports.py +++ b/pylint/checkers/imports.py @@ -1172,10 +1172,13 @@ def _report_dependencies_graph( ) -> None: """Write dependencies as a dot (graphviz) file.""" dep_info = self.linter.stats.dependencies - if not dep_info or not ( - self.linter.config.import_graph - or self.linter.config.ext_import_graph - or self.linter.config.int_import_graph + if not ( + dep_info + and ( + self.linter.config.import_graph + or self.linter.config.ext_import_graph + or self.linter.config.int_import_graph + ) ): raise EmptyReportError() filename = self.linter.config.import_graph diff --git a/pylint/checkers/refactoring/recommendation_checker.py b/pylint/checkers/refactoring/recommendation_checker.py index 366d061e47..68bfccbb93 100644 --- a/pylint/checkers/refactoring/recommendation_checker.py +++ b/pylint/checkers/refactoring/recommendation_checker.py @@ -281,10 +281,10 @@ def _check_consider_using_dict_items(self, node: nodes.For) -> None: continue value = subscript.slice - if ( - not isinstance(value, nodes.Name) - or value.name != node.target.name - or iterating_object_name != subscript.value.as_string() + if not ( + isinstance(value, nodes.Name) + and value.name == node.target.name + and iterating_object_name == subscript.value.as_string() ): continue last_definition_lineno = value.lookup(value.name)[1][-1].lineno @@ -334,10 +334,10 @@ def _check_consider_using_dict_items_comprehension( continue value = subscript.slice - if ( - not isinstance(value, nodes.Name) - or value.name != node.target.name - or iterating_object_name != subscript.value.as_string() + if not ( + isinstance(value, nodes.Name) + and value.name == node.target.name + and iterating_object_name == subscript.value.as_string() ): continue @@ -428,8 +428,9 @@ def _detect_replacable_format_call(self, node: nodes.Const) -> None: return # If % applied to another type than str, it's modulo and can't be replaced by formatting - if not hasattr(node.parent.left, "value") or not isinstance( - node.parent.left.value, str + if not ( + hasattr(node.parent.left, "value") + and isinstance(node.parent.left.value, str) ): return diff --git a/pylint/checkers/refactoring/refactoring_checker.py b/pylint/checkers/refactoring/refactoring_checker.py index 5e8c109730..953629c659 100644 --- a/pylint/checkers/refactoring/refactoring_checker.py +++ b/pylint/checkers/refactoring/refactoring_checker.py @@ -634,7 +634,7 @@ def _check_simplifiable_if(self, node: nodes.If) -> None: for target in else_branch.targets if isinstance(target, nodes.AssignName) ] - if not first_branch_targets or not else_branch_targets: + if not (first_branch_targets and else_branch_targets): return if sorted(first_branch_targets) != sorted(else_branch_targets): return @@ -645,7 +645,7 @@ def _check_simplifiable_if(self, node: nodes.If) -> None: case _: return - if not first_branch_is_bool or not else_branch_is_bool: + if not (first_branch_is_bool and else_branch_is_bool): return if not first_branch.value.value: # This is a case that can't be easily simplified and @@ -1053,7 +1053,7 @@ def visit_raise(self, node: nodes.Raise) -> None: def _check_stop_iteration_inside_generator(self, node: nodes.Raise) -> None: """Check if an exception of type StopIteration is raised inside a generator.""" frame = node.frame() - if not isinstance(frame, nodes.FunctionDef) or not frame.is_generator(): + if not (isinstance(frame, nodes.FunctionDef) and frame.is_generator()): return if utils.node_ignores_exception(node, StopIteration): return @@ -1319,11 +1319,11 @@ def _duplicated_isinstance_types(node: nodes.BoolOp) -> dict[str, set[str]]: all_types: collections.defaultdict[str, set[str]] = collections.defaultdict(set) for call in node.values: - if not isinstance(call, nodes.Call) or len(call.args) != 2: + if not (isinstance(call, nodes.Call) and len(call.args) == 2): continue inferred = utils.safe_infer(call.func) - if not inferred or not utils.is_builtin_object(inferred): + if not (inferred and utils.is_builtin_object(inferred)): continue if inferred.name != "isinstance": @@ -1365,7 +1365,7 @@ def _check_consider_merging_isinstance(self, node: nodes.BoolOp) -> None: def _check_consider_using_in(self, node: nodes.BoolOp) -> None: allowed_ops = {"or": "==", "and": "!="} - if node.op not in allowed_ops or len(node.values) < 2: + if not (node.op in allowed_ops and len(node.values) >= 2): return for value in node.values: @@ -1416,7 +1416,7 @@ def _check_chained_comparison(self, node: nodes.BoolOp) -> None: Care is taken to avoid simplifying a < b < c and b < d. """ - if node.op != "and" or len(node.values) < 2: + if not (node.op == "and" and len(node.values) >= 2): return def _find_lower_upper_bounds( @@ -1566,7 +1566,7 @@ def _is_simple_assignment(node: nodes.NodeNG | None) -> bool: return False def _check_swap_variables(self, node: nodes.Return | nodes.Assign) -> None: - if not node.next_sibling() or not node.next_sibling().next_sibling(): + if not (node.next_sibling() and node.next_sibling().next_sibling()): return assignments = [node, node.next_sibling(), node.next_sibling().next_sibling()] if not all(self._is_simple_assignment(node) for node in assignments): @@ -1643,10 +1643,10 @@ def _append_context_managers_to_stack(self, node: nodes.Assign) -> None: if not isinstance(value, nodes.Call): continue inferred = utils.safe_infer(value.func) - if ( - not inferred - or inferred.qname() not in CALLS_RETURNING_CONTEXT_MANAGERS - or not isinstance(assignee, (nodes.AssignName, nodes.AssignAttr)) + if not ( + inferred + and inferred.qname() in CALLS_RETURNING_CONTEXT_MANAGERS + and isinstance(assignee, (nodes.AssignName, nodes.AssignAttr)) ): continue stack = self._consider_using_with_stack.get_stack_for_frame(node.frame()) @@ -1685,8 +1685,11 @@ def _check_consider_using_with(self, node: nodes.Call) -> None: # checked when leaving the scope. return inferred = utils.safe_infer(node.func) - if not inferred or not isinstance( - inferred, (nodes.FunctionDef, nodes.ClassDef, bases.BoundMethod) + if not ( + inferred + and isinstance( + inferred, (nodes.FunctionDef, nodes.ClassDef, bases.BoundMethod) + ) ): return could_be_used_in_with = ( @@ -2178,12 +2181,12 @@ def _check_unnecessary_dict_index_lookup( # Case where .items is assigned to k,v (i.e., for k, v in d.items()) if isinstance(value, nodes.Name): - if ( - not isinstance(node.target, nodes.Tuple) + if not ( + isinstance(node.target, nodes.Tuple) # Ignore 1-tuples: for k, in d.items() - or len(node.target.elts) < 2 - or value.name != node.target.elts[0].name - or iterating_object_name != subscript.value.as_string() + and len(node.target.elts) >= 2 + and value.name == node.target.elts[0].name + and iterating_object_name == subscript.value.as_string() ): continue @@ -2213,11 +2216,11 @@ def _check_unnecessary_dict_index_lookup( # Case where .items is assigned to single var (i.e., for item in d.items()) elif isinstance(value, nodes.Subscript): - if ( - not isinstance(node.target, nodes.AssignName) - or not isinstance(value.value, nodes.Name) - or node.target.name != value.value.name - or iterating_object_name != subscript.value.as_string() + if not ( + isinstance(node.target, nodes.AssignName) + and isinstance(value.value, nodes.Name) + and node.target.name == value.value.name + and iterating_object_name == subscript.value.as_string() ): continue @@ -2234,7 +2237,7 @@ def _check_unnecessary_dict_index_lookup( # check if subscripted by 0 (key) inferred = utils.safe_infer(value.slice) - if not isinstance(inferred, nodes.Const) or inferred.value != 0: + if not (isinstance(inferred, nodes.Const) and inferred.value == 0): continue if has_nested_loops: @@ -2350,9 +2353,9 @@ def _check_unnecessary_list_index_lookup( index = subscript.slice if isinstance(index, nodes.Name): - if ( - index.name != name1 - or iterating_object_name != subscript.value.as_string() + if not ( + index.name == name1 + and iterating_object_name == subscript.value.as_string() ): continue diff --git a/pylint/checkers/spelling.py b/pylint/checkers/spelling.py index 5a0595cb42..c689de585b 100644 --- a/pylint/checkers/spelling.py +++ b/pylint/checkers/spelling.py @@ -161,11 +161,11 @@ def next(self) -> tuple[str, int]: pre_text, post_text = self._text.split("/", 1) self._text = post_text self._offset = 0 - if ( - not pre_text - or not post_text - or not pre_text[-1].isalpha() - or not post_text[0].isalpha() + if not ( + pre_text + and post_text + and pre_text[-1].isalpha() + and post_text[0].isalpha() ): self._text = "" self._offset = 0 @@ -177,9 +177,9 @@ def _next(self) -> tuple[str, Literal[0]]: if "/" not in self._text: return self._text, 0 pre_text, post_text = self._text.split("/", 1) - if not pre_text or not post_text: + if not (pre_text and post_text): break - if not pre_text[-1].isalpha() or not post_text[0].isalpha(): + if not (pre_text[-1].isalpha() and post_text[0].isalpha()): raise StopIteration() self._text = pre_text + " " + post_text raise StopIteration() diff --git a/pylint/checkers/stdlib.py b/pylint/checkers/stdlib.py index df5bbf1250..7bdaa34200 100644 --- a/pylint/checkers/stdlib.py +++ b/pylint/checkers/stdlib.py @@ -641,7 +641,7 @@ def _check_bad_thread_instantiation(self, node: nodes.Call) -> None: if "target" in func_kwargs: return - if len(node.args) < 2 and (not node.kwargs or "target" not in func_kwargs): + if len(node.args) < 2 and not (node.kwargs and "target" in func_kwargs): self.add_message( "bad-thread-instantiation", node=node, confidence=interfaces.HIGH ) @@ -885,7 +885,7 @@ def _check_open_call( if not mode_arg or ( isinstance(mode_arg, nodes.Const) - and (not mode_arg.value or "b" not in str(mode_arg.value)) + and not (mode_arg.value and "b" in str(mode_arg.value)) ): confidence = HIGH try: diff --git a/pylint/checkers/strings.py b/pylint/checkers/strings.py index 5cd620fbeb..0260798b46 100644 --- a/pylint/checkers/strings.py +++ b/pylint/checkers/strings.py @@ -423,8 +423,8 @@ def visit_call(self, node: nodes.Call) -> None: ): if func.name in {"strip", "lstrip", "rstrip"} and node.args: arg = utils.safe_infer(node.args[0]) - if not isinstance(arg, nodes.Const) or not isinstance( - arg.value, str + if not ( + isinstance(arg, nodes.Const) and isinstance(arg.value, str) ): return if len(arg.value) != len(set(arg.value)): @@ -567,7 +567,7 @@ def _check_new_format_specifiers( argument = utils.safe_infer(argname) except astroid.InferenceError: continue - if not specifiers or not argument: + if not (specifiers and argument): # No need to check this key if it doesn't # use attribute / item access continue @@ -769,7 +769,7 @@ def _is_parenthesized(self, index: int, tokens: list[tokenize.TokenInfo]) -> boo prev_token = self._find_prev_token( index, tokens, ignore=(*_PAREN_IGNORE_TOKEN_TYPES, tokenize.STRING) ) - if not prev_token or prev_token.type != tokenize.OP or prev_token[1] != "(": + if not (prev_token and prev_token.type == tokenize.OP and prev_token[1] == "("): return False next_token = self._find_next_token( index, tokens, ignore=(*_PAREN_IGNORE_TOKEN_TYPES, tokenize.STRING) diff --git a/pylint/checkers/typecheck.py b/pylint/checkers/typecheck.py index 0f7535ef6b..5402bfb770 100644 --- a/pylint/checkers/typecheck.py +++ b/pylint/checkers/typecheck.py @@ -1717,9 +1717,10 @@ def _check_invalid_sequence_index(self, subscript: nodes.Subscript) -> None: # If the types can be determined, only allow indices to be int, # slice or instances with __index__. parent_type = safe_infer(subscript.value) - if not isinstance( - parent_type, (nodes.ClassDef, astroid.Instance) - ) or not has_known_bases(parent_type): + if not ( + isinstance(parent_type, (nodes.ClassDef, astroid.Instance)) + and has_known_bases(parent_type) + ): return None # Determine what method on the parent this index will use @@ -1746,11 +1747,11 @@ def _check_invalid_sequence_index(self, subscript: nodes.Subscript) -> None: IndexError, ): return None - if ( - not isinstance(itemmethod, nodes.FunctionDef) - or itemmethod.root().name != "builtins" - or not itemmethod.parent - or itemmethod.parent.frame().name not in SEQUENCE_TYPES + if not ( + isinstance(itemmethod, nodes.FunctionDef) + and itemmethod.root().name == "builtins" + and itemmethod.parent + and itemmethod.parent.frame().name in SEQUENCE_TYPES ): return None diff --git a/pylint/checkers/variables.py b/pylint/checkers/variables.py index c46db5676b..38ceb6a60c 100644 --- a/pylint/checkers/variables.py +++ b/pylint/checkers/variables.py @@ -1704,7 +1704,7 @@ def visit_excepthandler(self, node: nodes.ExceptHandler) -> None: @utils.only_required_for_messages("redefined-outer-name") def leave_excepthandler(self, node: nodes.ExceptHandler) -> None: - if not node.name or not isinstance(node.name, nodes.AssignName): + if not (node.name and isinstance(node.name, nodes.AssignName)): return self._except_handler_names_queue.pop() @@ -2975,7 +2975,7 @@ def _check_late_binding_closure(self, node: nodes.Name) -> None: return assign_scope, stmts = node.lookup(node.name) - if not stmts or not assign_scope.parent_of(node_scope): + if not (stmts and assign_scope.parent_of(node_scope)): return if utils.is_comprehension(assign_scope): @@ -3243,8 +3243,8 @@ def _check_all( if not elt_name.parent: continue - if not isinstance(elt_name, nodes.Const) or not isinstance( - elt_name.value, str + if not ( + isinstance(elt_name, nodes.Const) and isinstance(elt_name.value, str) ): self.add_message("invalid-all-object", args=elt.as_string(), node=elt) continue diff --git a/pylint/extensions/code_style.py b/pylint/extensions/code_style.py index db80a2a2f1..58b08daf80 100644 --- a/pylint/extensions/code_style.py +++ b/pylint/extensions/code_style.py @@ -4,18 +4,26 @@ from __future__ import annotations +from copy import copy +from enum import IntFlag, auto 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 +class InvertibleValues(IntFlag): + NO = 0 + YES = auto() + EXPLICIT_NEGATION = auto() + + class CodeStyleChecker(BaseChecker): """Checkers that can improve code consistency. @@ -74,6 +82,16 @@ class CodeStyleChecker(BaseChecker): "default_enabled": False, }, ), + "R6106": ( + "Rewrite conditional expression to '%s'", + "improve-conditionals", + "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, + }, + ), } options = ( ( @@ -320,6 +338,85 @@ def visit_assign(self, node: nodes.Assign) -> None: confidence=INFERENCE, ) + @staticmethod + 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: + 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 (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]) + + 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/pylint/extensions/consider_refactoring_into_while_condition.py b/pylint/extensions/consider_refactoring_into_while_condition.py index b7e905e8a1..5f37bf195e 100644 --- a/pylint/extensions/consider_refactoring_into_while_condition.py +++ b/pylint/extensions/consider_refactoring_into_while_condition.py @@ -47,7 +47,7 @@ def visit_while(self, node: nodes.While) -> None: def _check_breaking_after_while_true(self, node: nodes.While) -> None: """Check that any loop with an ``if`` clause has a break statement.""" - if not isinstance(node.test, nodes.Const) or not node.test.bool_value(): + if not (isinstance(node.test, nodes.Const) and node.test.bool_value()): return pri_candidates: list[nodes.If] = [] for n in node.body: diff --git a/pylint/extensions/mccabe.py b/pylint/extensions/mccabe.py index 3f54c52c87..8a3f66a67f 100644 --- a/pylint/extensions/mccabe.py +++ b/pylint/extensions/mccabe.py @@ -110,7 +110,7 @@ def visitMatch(self, node: nodes.Match) -> None: self._subgraph(node, f"match_{id(node)}", node.cases) def _append_node(self, node: _AppendableNodeT) -> _AppendableNodeT | None: - if not self.tail or not self.graph: + if not (self.tail and self.graph): return None self.graph.connect(self.tail, node) self.tail = node diff --git a/pylint/extensions/private_import.py b/pylint/extensions/private_import.py index 7bcecda9dc..288a40c51b 100644 --- a/pylint/extensions/private_import.py +++ b/pylint/extensions/private_import.py @@ -108,7 +108,7 @@ def _name_is_private(name: str) -> bool: return ( bool(name) and name[0] == "_" - and (len(name) <= 4 or name[1] != "_" or name[-2:] != "__") + and not (len(name) > 4 and name[1] == "_" and name[-2:] == "__") ) def _get_type_annotation_names( diff --git a/pylint/message/message_id_store.py b/pylint/message/message_id_store.py index b07a9c3f70..07ec42d040 100644 --- a/pylint/message/message_id_store.py +++ b/pylint/message/message_id_store.py @@ -149,7 +149,7 @@ def get_active_msgids(self, msgid_or_symbol: str) -> list[str]: deletion_reason = is_deleted_symbol(symbol) if deletion_reason is None: moved_reason = is_moved_symbol(symbol) - if not msgid or not symbol: + if not (msgid and symbol): if deletion_reason is not None: raise DeletedMessageError(msgid_or_symbol, deletion_reason) if moved_reason is not None: diff --git a/pylint/pyreverse/utils.py b/pylint/pyreverse/utils.py index 0c1af3b69e..2b6bde87ae 100644 --- a/pylint/pyreverse/utils.py +++ b/pylint/pyreverse/utils.py @@ -198,9 +198,9 @@ def get_annotation( ann and getattr(default, "value", "value") is None and not label.startswith("Optional") - and ( - not isinstance(ann, nodes.BinOp) - or not any( + and not ( + isinstance(ann, nodes.BinOp) + and any( isinstance(child, nodes.Const) and child.value is None for child in ann.get_children() ) 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, 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..42ad04249e --- /dev/null +++ b/tests/functional/ext/code_style/cs_improve_conditionals.py @@ -0,0 +1,29 @@ +# 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 x < 0 or x > 100: + ... + elif x > 0 or y >= 1: + ... + elif x < 0 or y <= 1: + ... + + if not isinstance(expr, node_cls) or expr.attrname != "__init__": # [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..e19ff77f3f --- /dev/null +++ b/tests/functional/ext/code_style/cs_improve_conditionals.txt @@ -0,0 +1,4 @@ +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