From 6373074bf74032bcc8df17c0a6e87994b5a2eea7 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 30 Aug 2025 00:18:02 +0200 Subject: [PATCH 1/5] Use match statement in checkers (2) --- pylint/checkers/async_checker.py | 65 ++- pylint/checkers/exceptions.py | 30 +- pylint/checkers/format.py | 162 ++++---- pylint/checkers/imports.py | 139 +++---- pylint/checkers/lambda_expressions.py | 56 +-- pylint/checkers/logging.py | 52 +-- pylint/checkers/modified_iterating_checker.py | 14 +- pylint/checkers/non_ascii_names.py | 44 +-- pylint/checkers/stdlib.py | 36 +- pylint/checkers/strings.py | 112 +++--- pylint/checkers/typecheck.py | 369 +++++++++--------- pylint/checkers/utils.py | 278 +++++++------ pylint/checkers/variables.py | 282 ++++++------- 13 files changed, 831 insertions(+), 808 deletions(-) diff --git a/pylint/checkers/async_checker.py b/pylint/checkers/async_checker.py index a8ee773023..395d81b0d4 100644 --- a/pylint/checkers/async_checker.py +++ b/pylint/checkers/async_checker.py @@ -10,7 +10,7 @@ from typing import TYPE_CHECKING import astroid -from astroid import nodes, util +from astroid import nodes from pylint import checkers from pylint.checkers import utils as checker_utils @@ -54,39 +54,38 @@ def visit_asyncfunctiondef(self, node: nodes.AsyncFunctionDef) -> None: @checker_utils.only_required_for_messages("not-async-context-manager") def visit_asyncwith(self, node: nodes.AsyncWith) -> None: for ctx_mgr, _ in node.items: - inferred = checker_utils.safe_infer(ctx_mgr) - if inferred is None or isinstance(inferred, util.UninferableBase): - continue - - if isinstance(inferred, nodes.AsyncFunctionDef): - # Check if we are dealing with a function decorated - # with contextlib.asynccontextmanager. - if decorated_with(inferred, self._async_generators): - continue - elif isinstance(inferred, astroid.bases.AsyncGenerator): - # Check if we are dealing with a function decorated - # with contextlib.asynccontextmanager. - if decorated_with(inferred.parent, self._async_generators): - continue - else: - try: - inferred.getattr("__aenter__") - inferred.getattr("__aexit__") - except astroid.exceptions.NotFoundError: - if isinstance(inferred, astroid.Instance): - # If we do not know the bases of this class, - # just skip it. - if not checker_utils.has_known_bases(inferred): - continue - # Ignore mixin classes if they match the rgx option. - if ( - "not-async-context-manager" - in self.linter.config.ignored_checks_for_mixins - and self._mixin_class_rgx.match(inferred.name) - ): - continue - else: + match inferred := checker_utils.safe_infer(ctx_mgr): + case _ if not inferred: continue + case nodes.AsyncFunctionDef(): + # Check if we are dealing with a function decorated + # with contextlib.asynccontextmanager. + if decorated_with(inferred, self._async_generators): + continue + case astroid.bases.AsyncGenerator(): + # Check if we are dealing with a function decorated + # with contextlib.asynccontextmanager. + if decorated_with(inferred.parent, self._async_generators): + continue + case _: + try: + inferred.getattr("__aenter__") + inferred.getattr("__aexit__") + except astroid.exceptions.NotFoundError: + if isinstance(inferred, astroid.Instance): + # If we do not know the bases of this class, + # just skip it. + if not checker_utils.has_known_bases(inferred): + continue + # Ignore mixin classes if they match the rgx option. + if ( + "not-async-context-manager" + in self.linter.config.ignored_checks_for_mixins + and self._mixin_class_rgx.match(inferred.name) + ): + continue + else: + continue self.add_message( "not-async-context-manager", node=node, args=(inferred.name,) ) diff --git a/pylint/checkers/exceptions.py b/pylint/checkers/exceptions.py index 7c88c44c9b..cf52beb795 100644 --- a/pylint/checkers/exceptions.py +++ b/pylint/checkers/exceptions.py @@ -447,25 +447,27 @@ def _check_catching_non_exception( # Don't emit the warning if the inferred stmt # is None, but the exception handler is something else, # maybe it was redefined. - if isinstance(exc, nodes.Const) and exc.value is None: - if ( - isinstance(handler.type, nodes.Const) and handler.type.value is None - ) or handler.type.parent_of(exc): - # If the exception handler catches None or - # the exception component, which is None, is - # defined by the entire exception handler, then - # emit a warning. + match exc: + case nodes.Const(value=None): + if ( + isinstance(handler.type, nodes.Const) + and handler.type.value is None + ) or handler.type.parent_of(exc): + # If the exception handler catches None or + # the exception component, which is None, is + # defined by the entire exception handler, then + # emit a warning. + self.add_message( + "catching-non-exception", + node=handler.type, + args=(part.as_string(),), + ) + case _: self.add_message( "catching-non-exception", node=handler.type, args=(part.as_string(),), ) - else: - self.add_message( - "catching-non-exception", - node=handler.type, - args=(part.as_string(),), - ) return if ( diff --git a/pylint/checkers/format.py b/pylint/checkers/format.py index a7375859e3..3b02028751 100644 --- a/pylint/checkers/format.py +++ b/pylint/checkers/format.py @@ -268,7 +268,7 @@ def new_line(self, tokens: TokenWrapper, line_end: int, line_start: int) -> None def process_module(self, node: nodes.Module) -> None: pass - # pylint: disable-next = too-many-return-statements, too-many-branches + # pylint: disable-next = too-many-return-statements def _check_keyword_parentheses( self, tokens: list[tokenize.TokenInfo], start: int ) -> None: @@ -347,30 +347,31 @@ def _check_keyword_parentheses( ) return elif depth == 1: - # This is a tuple, which is always acceptable. - if token[1] == ",": - return - # 'and' and 'or' are the only boolean operators with lower precedence - # than 'not', so parens are only required when they are found. - if token[1] in {"and", "or"}: - found_and_or = True - # A yield inside an expression must always be in parentheses, - # quit early without error. - elif token[1] == "yield": - return - # A generator expression always has a 'for' token in it, and - # the 'for' token is only legal inside parens when it is in a - # generator expression. The parens are necessary here, so bail - # without an error. - elif token[1] == "for": - return - # A generator expression can have an 'else' token in it. - # We check the rest of the tokens to see if any problems occur after - # the 'else'. - elif token[1] == "else": - if "(" in (i.string for i in tokens[i:]): - self._check_keyword_parentheses(tokens[i:], 0) - return + match token[1]: + case ",": + # This is a tuple, which is always acceptable. + return + case "and" | "or": + # 'and' and 'or' are the only boolean operators with lower precedence + # than 'not', so parens are only required when they are found. + found_and_or = True + case "yield": + # A yield inside an expression must always be in parentheses, + # quit early without error. + return + case "for": + # A generator expression always has a 'for' token in it, and + # the 'for' token is only legal inside parens when it is in a + # generator expression. The parens are necessary here, so bail + # without an error. + return + case "else": + # A generator expression can have an 'else' token in it. + # We check the rest of the tokens to see if any problems occur after + # the 'else'. + if "(" in (i.string for i in tokens[i:]): + self._check_keyword_parentheses(tokens[i:], 0) + return def process_tokens(self, tokens: list[tokenize.TokenInfo]) -> None: """Process tokens and search for: @@ -397,39 +398,42 @@ def process_tokens(self, tokens: list[tokenize.TokenInfo]) -> None: else: self.new_line(TokenWrapper(tokens), idx - 1, idx) - if tok_type == tokenize.NEWLINE: - # a program statement, or ENDMARKER, will eventually follow, - # after some (possibly empty) run of tokens of the form - # (NL | COMMENT)* (INDENT | DEDENT+)? - # If an INDENT appears, setting check_equal is wrong, and will - # be undone when we see the INDENT. - check_equal = True - self._check_line_ending(string, line_num) - elif tok_type == tokenize.INDENT: - check_equal = False - self.check_indent_level(string, indents[-1] + 1, line_num) - indents.append(indents[-1] + 1) - elif tok_type == tokenize.DEDENT: - # there's nothing we need to check here! what's important is - # that when the run of DEDENTs ends, the indentation of the - # program statement (or ENDMARKER) that triggered the run is - # equal to what's left at the top of the indents stack - check_equal = True - if len(indents) > 1: - del indents[-1] - elif tok_type == tokenize.NL: - if not line.strip("\r\n"): - last_blank_line_num = line_num - elif tok_type not in (tokenize.COMMENT, tokenize.ENCODING): - # This is the first concrete token following a NEWLINE, so it - # must be the first token of the next program statement, or an - # ENDMARKER; the "line" argument exposes the leading white-space - # for this statement; in the case of ENDMARKER, line is an empty - # string, so will properly match the empty string with which the - # "indents" stack was seeded - if check_equal: + match tok_type: + case tokenize.NEWLINE: + # a program statement, or ENDMARKER, will eventually follow, + # after some (possibly empty) run of tokens of the form + # (NL | COMMENT)* (INDENT | DEDENT+)? + # If an INDENT appears, setting check_equal is wrong, and will + # be undone when we see the INDENT. + check_equal = True + self._check_line_ending(string, line_num) + case tokenize.INDENT: check_equal = False - self.check_indent_level(line, indents[-1], line_num) + self.check_indent_level(string, indents[-1] + 1, line_num) + indents.append(indents[-1] + 1) + case tokenize.DEDENT: + # there's nothing we need to check here! what's important is + # that when the run of DEDENTs ends, the indentation of the + # program statement (or ENDMARKER) that triggered the run is + # equal to what's left at the top of the indents stack + check_equal = True + if len(indents) > 1: + del indents[-1] + case tokenize.NL: + if not line.strip("\r\n"): + last_blank_line_num = line_num + case tokenize.COMMENT | tokenize.ENCODING: + pass + case _: + # This is the first concrete token following a NEWLINE, so it + # must be the first token of the next program statement, or an + # ENDMARKER; the "line" argument exposes the leading white-space + # for this statement; in the case of ENDMARKER, line is an empty + # string, so will properly match the empty string with which the + # "indents" stack was seeded + if check_equal: + check_equal = False + self.check_indent_level(line, indents[-1], line_num) if tok_type == tokenize.NUMBER and string.endswith("l"): self.add_message("lowercase-l-suffix", line=line_num) @@ -546,30 +550,26 @@ def _infer_else_finally_line_number( def _check_multi_statement_line(self, node: nodes.NodeNG, line: int) -> None: """Check for lines containing multiple statements.""" - if isinstance(node, nodes.With): - # Do not warn about multiple nested context managers in with statements. - return - if ( - isinstance(node.parent, nodes.If) - and not node.parent.orelse - and self.linter.config.single_line_if_stmt - ): - return - if ( - isinstance(node.parent, nodes.ClassDef) - and len(node.parent.body) == 1 - and self.linter.config.single_line_class_stmt - ): - return - - # Functions stubs and class with ``Ellipsis`` as body are exempted. - if ( - isinstance(node, nodes.Expr) - and isinstance(node.parent, (nodes.FunctionDef, nodes.ClassDef)) - and isinstance(node.value, nodes.Const) - and node.value.value is Ellipsis - ): - return + match node: + case nodes.With(): + # Do not warn about multiple nested context managers in with statements. + return + case nodes.NodeNG( + parent=nodes.If(orelse=[]) + ) if self.linter.config.single_line_if_stmt: + return + case nodes.NodeNG( + parent=nodes.ClassDef(body=[_]) + ) if self.linter.config.single_line_class_stmt: + return + case nodes.Expr( + parent=nodes.FunctionDef() | nodes.ClassDef(), + value=nodes.Const(value=value), + ) if ( + value is Ellipsis + ): + # Functions stubs and class with ``Ellipsis`` as body are exempted. + return self.add_message("multiple-statements", node=node, confidence=HIGH) self._visited_lines[line] = 2 diff --git a/pylint/checkers/imports.py b/pylint/checkers/imports.py index 83435efabd..cdda5d9665 100644 --- a/pylint/checkers/imports.py +++ b/pylint/checkers/imports.py @@ -777,81 +777,82 @@ def _check_imports_order(self, _module_node: nodes.Module) -> tuple[ import_category = isort_driver.place_module(package) node_and_package_import = (node, package) - if import_category in {"FUTURE", "STDLIB"}: - std_imports.append(node_and_package_import) - wrong_import = ( - third_party_not_ignored - or first_party_not_ignored - or local_not_ignored - ) - if self._is_fallback_import(node, wrong_import): - continue - if wrong_import and not nested: - self.add_message( - "wrong-import-order", - node=node, - args=( ## TODO - this isn't right for multiple on the same line... - f'standard import "{self._get_full_import_name((node, package))}"', - self._get_out_of_order_string( - third_party_not_ignored, - first_party_not_ignored, - local_not_ignored, - ), - ), + match import_category: + case "FUTURE" | "STDLIB": + std_imports.append(node_and_package_import) + wrong_import = ( + third_party_not_ignored + or first_party_not_ignored + or local_not_ignored ) - elif import_category == "THIRDPARTY": - third_party_imports.append(node_and_package_import) - external_imports.append(node_and_package_import) - if not nested: - if not ignore_for_import_order: - third_party_not_ignored.append(node_and_package_import) - else: - self.linter.add_ignored_message( - "wrong-import-order", node.fromlineno, node + if self._is_fallback_import(node, wrong_import): + continue + if wrong_import and not nested: + self.add_message( + "wrong-import-order", + node=node, + args=( ## TODO - this isn't right for multiple on the same line... + f'standard import "{self._get_full_import_name((node, package))}"', + self._get_out_of_order_string( + third_party_not_ignored, + first_party_not_ignored, + local_not_ignored, + ), + ), ) - wrong_import = first_party_not_ignored or local_not_ignored - if wrong_import and not nested: - self.add_message( - "wrong-import-order", - node=node, - args=( - f'third party import "{self._get_full_import_name((node, package))}"', - self._get_out_of_order_string( - None, first_party_not_ignored, local_not_ignored + case "THIRDPARTY": + third_party_imports.append(node_and_package_import) + external_imports.append(node_and_package_import) + if not nested: + if not ignore_for_import_order: + third_party_not_ignored.append(node_and_package_import) + else: + self.linter.add_ignored_message( + "wrong-import-order", node.fromlineno, node + ) + wrong_import = first_party_not_ignored or local_not_ignored + if wrong_import and not nested: + self.add_message( + "wrong-import-order", + node=node, + args=( + f'third party import "{self._get_full_import_name((node, package))}"', + self._get_out_of_order_string( + None, first_party_not_ignored, local_not_ignored + ), ), - ), - ) - elif import_category == "FIRSTPARTY": - first_party_imports.append(node_and_package_import) - external_imports.append(node_and_package_import) - if not nested: - if not ignore_for_import_order: - first_party_not_ignored.append(node_and_package_import) - else: - self.linter.add_ignored_message( - "wrong-import-order", node.fromlineno, node ) - wrong_import = local_not_ignored - if wrong_import and not nested: - self.add_message( - "wrong-import-order", - node=node, - args=( - f'first party import "{self._get_full_import_name((node, package))}"', - self._get_out_of_order_string( - None, None, local_not_ignored + case "FIRSTPARTY": + first_party_imports.append(node_and_package_import) + external_imports.append(node_and_package_import) + if not nested: + if not ignore_for_import_order: + first_party_not_ignored.append(node_and_package_import) + else: + self.linter.add_ignored_message( + "wrong-import-order", node.fromlineno, node + ) + wrong_import = local_not_ignored + if wrong_import and not nested: + self.add_message( + "wrong-import-order", + node=node, + args=( + f'first party import "{self._get_full_import_name((node, package))}"', + self._get_out_of_order_string( + None, None, local_not_ignored + ), ), - ), - ) - elif import_category == "LOCALFOLDER": - local_imports.append((node, package)) - if not nested: - if not ignore_for_import_order: - local_not_ignored.append((node, package)) - else: - self.linter.add_ignored_message( - "wrong-import-order", node.fromlineno, node ) + case "LOCALFOLDER": + local_imports.append((node, package)) + if not nested: + if not ignore_for_import_order: + local_not_ignored.append((node, package)) + else: + self.linter.add_ignored_message( + "wrong-import-order", node.fromlineno, node + ) return std_imports, external_imports, local_imports def _get_out_of_order_string( diff --git a/pylint/checkers/lambda_expressions.py b/pylint/checkers/lambda_expressions.py index 18c03060d8..05df002582 100644 --- a/pylint/checkers/lambda_expressions.py +++ b/pylint/checkers/lambda_expressions.py @@ -39,35 +39,35 @@ class LambdaExpressionChecker(BaseChecker): def visit_assign(self, node: nodes.Assign) -> None: """Check if lambda expression is assigned to a variable.""" - if isinstance(node.targets[0], nodes.AssignName) and isinstance( - node.value, nodes.Lambda - ): - self.add_message( - "unnecessary-lambda-assignment", - node=node.value, - confidence=HIGH, - ) - elif isinstance(node.targets[0], nodes.Tuple) and isinstance( - node.value, (nodes.Tuple, nodes.List) - ): - # Iterate over tuple unpacking assignment elements and - # see if any lambdas are assigned to a variable. - # N.B. We may encounter W0632 (unbalanced-tuple-unpacking) - # and still need to flag the lambdas that are being assigned. - for lhs_elem, rhs_elem in zip_longest( - node.targets[0].elts, node.value.elts + match node: + case nodes.Assign( + targets=[nodes.AssignName()], value=nodes.Lambda() as value + ): + self.add_message( + "unnecessary-lambda-assignment", + node=value, + confidence=HIGH, + ) + case nodes.Assign( + targets=[nodes.Tuple() as target], + value=nodes.Tuple() | nodes.List() as value, ): - if lhs_elem is None or rhs_elem is None: - # unbalanced tuple unpacking. stop checking. - break - if isinstance(lhs_elem, nodes.AssignName) and isinstance( - rhs_elem, nodes.Lambda - ): - self.add_message( - "unnecessary-lambda-assignment", - node=rhs_elem, - confidence=HIGH, - ) + # Iterate over tuple unpacking assignment elements and + # see if any lambdas are assigned to a variable. + # N.B. We may encounter W0632 (unbalanced-tuple-unpacking) + # and still need to flag the lambdas that are being assigned. + for lhs_elem, rhs_elem in zip_longest(target.elts, value.elts): + if lhs_elem is None or rhs_elem is None: + # unbalanced tuple unpacking. stop checking. + break + if isinstance(lhs_elem, nodes.AssignName) and isinstance( + rhs_elem, nodes.Lambda + ): + self.add_message( + "unnecessary-lambda-assignment", + node=rhs_elem, + confidence=HIGH, + ) def visit_namedexpr(self, node: nodes.NamedExpr) -> None: if isinstance(node.target, nodes.AssignName) and isinstance( diff --git a/pylint/checkers/logging.py b/pylint/checkers/logging.py index 583143bc60..505619b84a 100644 --- a/pylint/checkers/logging.py +++ b/pylint/checkers/logging.py @@ -237,35 +237,37 @@ def _check_log_method(self, node: nodes.Call, name: str) -> None: else: return - format_arg = node.args[format_pos] - if isinstance(format_arg, nodes.BinOp): - binop = format_arg - emit = binop.op == "%" - if binop.op == "+" and not self._is_node_explicit_str_concatenation(binop): - total_number_of_strings = sum( - 1 - for operand in (binop.left, binop.right) - if self._is_operand_literal_str(utils.safe_infer(operand)) - ) - emit = total_number_of_strings > 0 - if emit: + match format_arg := node.args[format_pos]: + case nodes.BinOp(): + binop = format_arg + emit = binop.op == "%" + if binop.op == "+" and not self._is_node_explicit_str_concatenation( + binop + ): + total_number_of_strings = sum( + 1 + for operand in (binop.left, binop.right) + if self._is_operand_literal_str(utils.safe_infer(operand)) + ) + emit = total_number_of_strings > 0 + if emit: + self.add_message( + "logging-not-lazy", + node=node, + args=(self._helper_string(node),), + ) + case nodes.Call(): + self._check_call_func(format_arg) + case nodes.Const(): + self._check_format_string(node, format_pos) + case nodes.JoinedStr(): + if str_formatting_in_f_string(format_arg): + return self.add_message( - "logging-not-lazy", + "logging-fstring-interpolation", node=node, args=(self._helper_string(node),), ) - elif isinstance(format_arg, nodes.Call): - self._check_call_func(format_arg) - elif isinstance(format_arg, nodes.Const): - self._check_format_string(node, format_pos) - elif isinstance(format_arg, nodes.JoinedStr): - if str_formatting_in_f_string(format_arg): - return - self.add_message( - "logging-fstring-interpolation", - node=node, - args=(self._helper_string(node),), - ) def _helper_string(self, node: nodes.Call) -> str: """Create a string that lists the valid types of formatting for this node.""" diff --git a/pylint/checkers/modified_iterating_checker.py b/pylint/checkers/modified_iterating_checker.py index be8d967abb..3cddd643f5 100644 --- a/pylint/checkers/modified_iterating_checker.py +++ b/pylint/checkers/modified_iterating_checker.py @@ -74,13 +74,13 @@ def _modified_iterating_check( if isinstance(node, nodes.Delete) and any( self._deleted_iteration_target_cond(t, iter_obj) for t in node.targets ): - inferred = utils.safe_infer(iter_obj) - if isinstance(inferred, nodes.List): - msg_id = "modified-iterating-list" - elif isinstance(inferred, nodes.Dict): - msg_id = "modified-iterating-dict" - elif isinstance(inferred, nodes.Set): - msg_id = "modified-iterating-set" + match utils.safe_infer(iter_obj): + case nodes.List(): + msg_id = "modified-iterating-list" + case nodes.Dict(): + msg_id = "modified-iterating-dict" + case nodes.Set(): + msg_id = "modified-iterating-set" elif not isinstance(iter_obj, (nodes.Name, nodes.Attribute)): pass elif self._modified_iterating_list_cond(node, iter_obj): diff --git a/pylint/checkers/non_ascii_names.py b/pylint/checkers/non_ascii_names.py index 693d8529f5..ee53956305 100644 --- a/pylint/checkers/non_ascii_names.py +++ b/pylint/checkers/non_ascii_names.py @@ -73,13 +73,14 @@ def _check_name(self, node_type: str, name: str | None, node: nodes.NodeNG) -> N type_label = constants.HUMAN_READABLE_TYPES[node_type] args = (type_label.capitalize(), name) - msg = "non-ascii-name" - # Some node types have customized messages - if node_type == "file": - msg = "non-ascii-file-name" - elif node_type == "module": - msg = "non-ascii-module-import" + match node_type: + case "file": + msg = "non-ascii-file-name" + case "module": + msg = "non-ascii-module-import" + case _: + msg = "non-ascii-name" self.add_message(msg, node=node, args=args, confidence=interfaces.HIGH) @@ -125,23 +126,22 @@ def visit_assignname(self, node: nodes.AssignName) -> None: # versions of variables, i.e. constants, inline variables etc. # To simplify we use only `variable` here, as we don't need to apply different # rules to different types of variables. - frame = node.frame() - - if isinstance(frame, nodes.FunctionDef): - if node.parent in frame.body: - # Only perform the check if the assignment was done in within the body - # of the function (and not the function parameter definition - # (will be handled in visit_functiondef) - # or within a decorator (handled in visit_call) + match frame := node.frame(): + case nodes.FunctionDef(): + if node.parent in frame.body: + # Only perform the check if the assignment was done in within the body + # of the function (and not the function parameter definition + # (will be handled in visit_functiondef) + # or within a decorator (handled in visit_call) + self._check_name("variable", node.name, node) + case nodes.ClassDef(): + self._check_name("attr", node.name, node) + case _: + # Possibilities here: + # - isinstance(node.assign_type(), nodes.Comprehension) == inlinevar + # - isinstance(frame, nodes.Module) == variable (constant?) + # - some other kind of assignment missed but still most likely a variable self._check_name("variable", node.name, node) - elif isinstance(frame, nodes.ClassDef): - self._check_name("attr", node.name, node) - else: - # Possibilities here: - # - isinstance(node.assign_type(), nodes.Comprehension) == inlinevar - # - isinstance(frame, nodes.Module) == variable (constant?) - # - some other kind of assignment missed but still most likely a variable - self._check_name("variable", node.name, node) @utils.only_required_for_messages("non-ascii-name") def visit_classdef(self, node: nodes.ClassDef) -> None: diff --git a/pylint/checkers/stdlib.py b/pylint/checkers/stdlib.py index a4b89b6fd0..a6a6255eac 100644 --- a/pylint/checkers/stdlib.py +++ b/pylint/checkers/stdlib.py @@ -859,18 +859,19 @@ def _check_open_call( confidence = HIGH try: if open_module in PATHLIB_MODULE: - if node.func.attrname == "read_text": - encoding_arg = utils.get_argument_from_call( - node, position=0, keyword="encoding" - ) - elif node.func.attrname == "write_text": - encoding_arg = utils.get_argument_from_call( - node, position=1, keyword="encoding" - ) - else: - encoding_arg = utils.get_argument_from_call( - node, position=2, keyword="encoding" - ) + match node.func.attrname: + case "read_text": + encoding_arg = utils.get_argument_from_call( + node, position=0, keyword="encoding" + ) + case "write_text": + encoding_arg = utils.get_argument_from_call( + node, position=1, keyword="encoding" + ) + case _: + encoding_arg = utils.get_argument_from_call( + node, position=2, keyword="encoding" + ) else: encoding_arg = utils.get_argument_from_call( node, position=3, keyword="encoding" @@ -945,10 +946,13 @@ def _check_invalid_envvar_value( name = infer.qname() if isinstance(call_arg, nodes.Const): emit = False - if call_arg.value is None: - emit = not allow_none - elif not isinstance(call_arg.value, str): - emit = True + match call_arg.value: + case None: + emit = not allow_none + case str(): + pass + case _: + emit = True if emit: self.add_message(message, node=node, args=(name, call_arg.pytype())) else: diff --git a/pylint/checkers/strings.py b/pylint/checkers/strings.py index 90493fa006..92c0f62a63 100644 --- a/pylint/checkers/strings.py +++ b/pylint/checkers/strings.py @@ -227,14 +227,14 @@ def arg_matches_format_type( # All types can be printed with %s and %r return True if isinstance(arg_type, astroid.Instance): - arg_type = arg_type.pytype() - if arg_type == "builtins.str": - return format_type == "c" - if arg_type == "builtins.float": - return format_type in "deEfFgGn%" - if arg_type == "builtins.int": - # Integers allow all types - return True + match arg_type.pytype(): + case "builtins.str": + return format_type == "c" + case "builtins.float": + return format_type in "deEfFgGn%" + case "builtins.int": + # Integers allow all types + return True return False return True @@ -417,24 +417,24 @@ def _check_interpolation(self, node: nodes.JoinedStr) -> None: self.add_message("f-string-without-interpolation", node=node) def visit_call(self, node: nodes.Call) -> None: - func = utils.safe_infer(node.func) - if ( - isinstance(func, astroid.BoundMethod) - and isinstance(func.bound, astroid.Instance) - and func.bound.name in {"str", "unicode", "bytes"} - ): - 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): - return - if len(arg.value) != len(set(arg.value)): - self.add_message( - "bad-str-strip-call", - node=node, - args=(func.bound.name, func.name), - ) - elif func.name == "format": - self._check_new_format(node, func) + match func := utils.safe_infer(node.func): + case astroid.BoundMethod( + bound=astroid.Instance(name="str" | "unicode" | "bytes" as bound_name), + ): + 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 + ): + return + if len(arg.value) != len(set(arg.value)): + self.add_message( + "bad-str-strip-call", + node=node, + args=(bound_name, func.name), + ) + elif func.name == "format": + self._check_new_format(node, func) def _detect_vacuous_formatting( self, node: nodes.Call, positional_arguments: list[SuccessfulInferenceResult] @@ -724,31 +724,32 @@ def process_module(self, node: nodes.Module) -> None: def process_tokens(self, tokens: list[tokenize.TokenInfo]) -> None: encoding = "ascii" for i, (token_type, token, start, _, line) in enumerate(tokens): - if token_type == tokenize.ENCODING: - # this is always the first token processed - encoding = token - elif token_type == tokenize.STRING: - # 'token' is the whole un-parsed token; we can look at the start - # of it to see whether it's a raw or unicode string etc. - self.process_string_token(token, start[0], start[1]) - # We figure the next token, ignoring comments & newlines: - j = i + 1 - while j < len(tokens) and tokens[j].type in ( - tokenize.NEWLINE, - tokenize.NL, - tokenize.COMMENT, - ): - j += 1 - next_token = tokens[j] if j < len(tokens) else None - if encoding != "ascii": - # We convert `tokenize` character count into a byte count, - # to match with astroid `.col_offset` - start = (start[0], len(line[: start[1]].encode(encoding))) - self.string_tokens[start] = (str_eval(token), next_token) - is_parenthesized = self._is_initial_string_token( - i, tokens - ) and self._is_parenthesized(i, tokens) - self._parenthesized_string_tokens[start] = is_parenthesized + match token_type: + case tokenize.ENCODING: + # this is always the first token processed + encoding = token + case tokenize.STRING: + # 'token' is the whole un-parsed token; we can look at the start + # of it to see whether it's a raw or unicode string etc. + self.process_string_token(token, start[0], start[1]) + # We figure the next token, ignoring comments & newlines: + j = i + 1 + while j < len(tokens) and tokens[j].type in ( + tokenize.NEWLINE, + tokenize.NL, + tokenize.COMMENT, + ): + j += 1 + next_token = tokens[j] if j < len(tokens) else None + if encoding != "ascii": + # We convert `tokenize` character count into a byte count, + # to match with astroid `.col_offset` + start = (start[0], len(line[: start[1]].encode(encoding))) + self.string_tokens[start] = (str_eval(token), next_token) + is_parenthesized = self._is_initial_string_token( + i, tokens + ) and self._is_parenthesized(i, tokens) + self._parenthesized_string_tokens[start] = is_parenthesized if self.linter.config.check_quote_consistency: self.check_for_consistent_string_delimiters(tokens) @@ -842,10 +843,11 @@ def check_for_consistent_string_delimiters( for tok_type, token, _, _, _ in tokens: if sys.version_info[:2] >= (3, 12): # pylint: disable=no-member,useless-suppression - if tok_type == tokenize.FSTRING_START: - inside_fstring = True - elif tok_type == tokenize.FSTRING_END: - inside_fstring = False + match tok_type: + case tokenize.FSTRING_START: + inside_fstring = True + case tokenize.FSTRING_END: + inside_fstring = False if inside_fstring and not target_py312: # skip analysis of f-string contents diff --git a/pylint/checkers/typecheck.py b/pylint/checkers/typecheck.py index 431f3ec3d1..c34f33b88b 100644 --- a/pylint/checkers/typecheck.py +++ b/pylint/checkers/typecheck.py @@ -528,20 +528,20 @@ def _get_all_attribute_assignments( attributes: set[str] = set() for child in node.nodes_of_class((nodes.Assign, nodes.AnnAssign)): targets = [] - if isinstance(child, nodes.Assign): - targets = child.targets - elif isinstance(child, nodes.AnnAssign): - targets = [child.target] + match child: + case nodes.Assign(): + targets = child.targets + case nodes.AnnAssign(): + targets = [child.target] for assign_target in targets: - if isinstance(assign_target, nodes.Tuple): - targets.extend(assign_target.elts) - continue - if ( - isinstance(assign_target, nodes.AssignAttr) - and isinstance(assign_target.expr, nodes.Name) - and (name is None or assign_target.expr.name == name) - ): - attributes.add(assign_target.attrname) + match assign_target: + case nodes.Tuple(): + targets.extend(assign_target.elts) + continue + case nodes.AssignAttr(expr=nodes.Name(name=n)) if ( + n is None or n == name + ): + attributes.add(assign_target.attrname) return attributes @@ -608,43 +608,44 @@ def _determine_callable( parameters = 0 if hasattr(callable_obj, "implicit_parameters"): parameters = callable_obj.implicit_parameters() - if isinstance(callable_obj, bases.BoundMethod): - # Bound methods have an extra implicit 'self' argument. - return callable_obj, parameters, callable_obj.type - if isinstance(callable_obj, bases.UnboundMethod): - return callable_obj, parameters, "unbound method" - if isinstance(callable_obj, nodes.FunctionDef): - return callable_obj, parameters, callable_obj.type - if isinstance(callable_obj, nodes.Lambda): - return callable_obj, parameters, "lambda" - if isinstance(callable_obj, nodes.ClassDef): - # Class instantiation, lookup __new__ instead. - # If we only find object.__new__, we can safely check __init__ - # instead. If __new__ belongs to builtins, then we look - # again for __init__ in the locals, since we won't have - # argument information for the builtin __new__ function. - try: - # Use the last definition of __new__. - new = callable_obj.local_attr("__new__")[-1] - except astroid.NotFoundError: - new = None + match callable_obj: + case bases.BoundMethod(): + # Bound methods have an extra implicit 'self' argument. + return callable_obj, parameters, callable_obj.type + case bases.UnboundMethod(): + return callable_obj, parameters, "unbound method" + case nodes.FunctionDef(): + return callable_obj, parameters, callable_obj.type + case nodes.Lambda(): + return callable_obj, parameters, "lambda" + case nodes.ClassDef(): + # Class instantiation, lookup __new__ instead. + # If we only find object.__new__, we can safely check __init__ + # instead. If __new__ belongs to builtins, then we look + # again for __init__ in the locals, since we won't have + # argument information for the builtin __new__ function. + try: + # Use the last definition of __new__. + new = callable_obj.local_attr("__new__")[-1] + except astroid.NotFoundError: + new = None - from_object = new and new.parent.scope().name == "object" - from_builtins = new and new.root().name in sys.builtin_module_names + from_object = new and new.parent.scope().name == "object" + from_builtins = new and new.root().name in sys.builtin_module_names - if not new or from_object or from_builtins: - try: - # Use the last definition of __init__. - callable_obj = callable_obj.local_attr("__init__")[-1] - except astroid.NotFoundError as e: - raise ValueError from e - else: - callable_obj = new + if not new or from_object or from_builtins: + try: + # Use the last definition of __init__. + callable_obj = callable_obj.local_attr("__init__")[-1] + except astroid.NotFoundError as e: + raise ValueError from e + else: + callable_obj = new - if not isinstance(callable_obj, nodes.FunctionDef): - raise ValueError - # both have an extra implicit 'cls'/'self' argument. - return callable_obj, parameters, "constructor" + if not isinstance(callable_obj, nodes.FunctionDef): + raise ValueError + # both have an extra implicit 'cls'/'self' argument. + return callable_obj, parameters, "constructor" raise ValueError @@ -800,21 +801,21 @@ def _is_invalid_isinstance_type(arg: nodes.NodeNG) -> bool: _is_invalid_isinstance_type(elt) and not is_none(elt) for elt in (arg.left, arg.right) ) - inferred = utils.safe_infer(arg) - if not inferred: - # Cannot infer it so skip it. - return False - if isinstance(inferred, nodes.Tuple): - return any(_is_invalid_isinstance_type(elt) for elt in inferred.elts) - if isinstance(inferred, nodes.ClassDef): - return False - if isinstance(inferred, astroid.Instance) and inferred.qname() == BUILTIN_TUPLE: - return False - if isinstance(inferred, bases.UnionType): - return any( - _is_invalid_isinstance_type(elt) and not is_none(elt) - for elt in (inferred.left, inferred.right) - ) + match inferred := utils.safe_infer(arg): + case _ if not inferred: + # Cannot infer it so skip it. + return False + case nodes.Tuple(): + return any(_is_invalid_isinstance_type(elt) for elt in inferred.elts) + case nodes.ClassDef(): + return False + case astroid.Instance() if inferred.qname() == BUILTIN_TUPLE: + return False + case bases.UnionType(): + return any( + _is_invalid_isinstance_type(elt) and not is_none(elt) + for elt in (inferred.left, inferred.right) + ) return True @@ -1313,12 +1314,13 @@ def _check_dundername_is_string(self, node: nodes.Assign) -> None: rhs = node.value if isinstance(rhs, nodes.Const) and isinstance(rhs.value, str): return - inferred = utils.safe_infer(rhs) - if not inferred: - return - if not (isinstance(inferred, nodes.Const) and isinstance(inferred.value, str)): - # Add the message - self.add_message("non-str-assignment-to-dunder-name", node=node) + match inferred := utils.safe_infer(rhs): + case _ if not inferred: + return + case nodes.Const(value=str()): + pass + case _: + self.add_message("non-str-assignment-to-dunder-name", node=node) def _check_uninferable_call(self, node: nodes.Call) -> None: """Check that the given uninferable Call node does not @@ -1750,24 +1752,25 @@ def _check_invalid_sequence_index(self, subscript: nodes.Subscript) -> None: index_type = safe_infer(subscript.slice) if index_type is None or isinstance(index_type, util.UninferableBase): return None - # Constants must be of type int - if isinstance(index_type, nodes.Const): - if isinstance(index_type.value, int): - return None - # Instance values must be int, slice, or have an __index__ method - elif isinstance(index_type, astroid.Instance): - if index_type.pytype() in {"builtins.int", "builtins.slice"}: - return None - try: - index_type.getattr("__index__") - return None - except astroid.NotFoundError: - pass - elif isinstance(index_type, nodes.Slice): - # A slice can be present - # here after inferring the index node, which could - # be a `slice(...)` call for instance. - return self._check_invalid_slice_index(index_type) + match index_type: + case nodes.Const(): + # Constants must be of type int + if isinstance(index_type.value, int): + return None + case astroid.Instance(): + # Instance values must be int, slice, or have an __index__ method + if index_type.pytype() in {"builtins.int", "builtins.slice"}: + return None + try: + index_type.getattr("__index__") + return None + except astroid.NotFoundError: + pass + case nodes.Slice(): + # A slice can be present + # here after inferring the index node, which could + # be a `slice(...)` call for instance. + return self._check_invalid_slice_index(index_type) # Anything else is an error self.add_message("invalid-sequence-index", node=subscript) @@ -1811,25 +1814,24 @@ def _check_invalid_slice_index(self, node: nodes.Slice) -> None: if index is None: continue - index_type = safe_infer(index) - if index_type is None or isinstance(index_type, util.UninferableBase): - continue - - # Constants must be of type int or None - if isinstance(index_type, nodes.Const): - if isinstance(index_type.value, (int, type(None))): - continue - # Instance values must be of type int, None or an object - # with __index__ - elif isinstance(index_type, astroid.Instance): - if index_type.pytype() in {"builtins.int", "builtins.NoneType"}: + match index_type := safe_infer(index): + case _ if not index_type: continue + case nodes.Const(): + # Constants must be of type int or None + if isinstance(index_type.value, (int, type(None))): + continue + case astroid.Instance(): + # Instance values must be of type int, None or an object + # with __index__ + if index_type.pytype() in {"builtins.int", "builtins.NoneType"}: + continue - try: - index_type.getattr("__index__") - return - except astroid.NotFoundError: - pass + try: + index_type.getattr("__index__") + return + except astroid.NotFoundError: + pass invalid_slices_nodes.append(index) invalid_slice_step = ( @@ -1876,68 +1878,67 @@ def _check_invalid_slice_index(self, node: nodes.Slice) -> None: def visit_with(self, node: nodes.With) -> None: for ctx_mgr, _ in node.items: context = astroid.context.InferenceContext() - inferred = safe_infer(ctx_mgr, context=context) - if inferred is None or isinstance(inferred, util.UninferableBase): - continue - - if isinstance(inferred, astroid.bases.Generator): - # Check if we are dealing with a function decorated - # with contextlib.contextmanager. - if decorated_with( - inferred.parent, self.linter.config.contextmanager_decorators - ): + match inferred := safe_infer(ctx_mgr, context=context): + case _ if not inferred: continue - # If the parent of the generator is not the context manager itself, - # that means that it could have been returned from another - # function which was the real context manager. - # The following approach is more of a hack rather than a real - # solution: walk all the inferred statements for the - # given *ctx_mgr* and if you find one function scope - # which is decorated, consider it to be the real - # manager and give up, otherwise emit not-context-manager. - # See the test file for not_context_manager for a couple - # of self explaining tests. - - # Retrieve node from all previously visited nodes in the - # inference history - for inferred_path, _ in context.path: - if not inferred_path: - continue - if isinstance(inferred_path, nodes.Call): - scope = safe_infer(inferred_path.func) - else: - scope = inferred_path.scope() - if not isinstance(scope, nodes.FunctionDef): - continue + case astroid.bases.Generator(): + # Check if we are dealing with a function decorated + # with contextlib.contextmanager. if decorated_with( - scope, self.linter.config.contextmanager_decorators + inferred.parent, self.linter.config.contextmanager_decorators ): - break - else: - self.add_message( - "not-context-manager", node=node, args=(inferred.name,) - ) - else: - try: - inferred.getattr("__enter__") - inferred.getattr("__exit__") - except astroid.NotFoundError: - if isinstance(inferred, astroid.Instance): - # If we do not know the bases of this class, - # just skip it. - if not has_known_bases(inferred): + continue + # If the parent of the generator is not the context manager itself, + # that means that it could have been returned from another + # function which was the real context manager. + # The following approach is more of a hack rather than a real + # solution: walk all the inferred statements for the + # given *ctx_mgr* and if you find one function scope + # which is decorated, consider it to be the real + # manager and give up, otherwise emit not-context-manager. + # See the test file for not_context_manager for a couple + # of self explaining tests. + + # Retrieve node from all previously visited nodes in the + # inference history + for inferred_path, _ in context.path: + if not inferred_path: continue - # Just ignore mixin classes. - if ( - "not-context-manager" - in self.linter.config.ignored_checks_for_mixins + if isinstance(inferred_path, nodes.Call): + scope = safe_infer(inferred_path.func) + else: + scope = inferred_path.scope() + if not isinstance(scope, nodes.FunctionDef): + continue + if decorated_with( + scope, self.linter.config.contextmanager_decorators ): - if inferred.name[-5:].lower() == "mixin": + break + else: + self.add_message( + "not-context-manager", node=node, args=(inferred.name,) + ) + case _: + try: + inferred.getattr("__enter__") + inferred.getattr("__exit__") + except astroid.NotFoundError: + if isinstance(inferred, astroid.Instance): + # If we do not know the bases of this class, + # just skip it. + if not has_known_bases(inferred): continue + # Just ignore mixin classes. + if ( + "not-context-manager" + in self.linter.config.ignored_checks_for_mixins + ): + if inferred.name[-5:].lower() == "mixin": + continue - self.add_message( - "not-context-manager", node=node, args=(inferred.name,) - ) + self.add_message( + "not-context-manager", node=node, args=(inferred.name,) + ) @only_required_for_messages("invalid-unary-operand-type") def visit_unaryop(self, node: nodes.UnaryOp) -> None: @@ -2130,31 +2131,32 @@ def visit_subscript(self, node: nodes.Subscript) -> None: self._check_invalid_sequence_index(node) supported_protocol: Callable[[Any, Any], bool] | None = None - if isinstance(node.value, (nodes.ListComp, nodes.DictComp)): - return + match node.value: + case nodes.ListComp() | nodes.DictComp(): + return - if isinstance(node.value, nodes.Dict): - # Assert dict key is hashable - if not is_hashable(node.slice): - self.add_message( - "unhashable-member", - node=node.value, - args=(node.slice.as_string(), "key", "dict"), - confidence=INFERENCE, - ) + case nodes.Dict(): + # Assert dict key is hashable + if not is_hashable(node.slice): + self.add_message( + "unhashable-member", + node=node.value, + args=(node.slice.as_string(), "key", "dict"), + confidence=INFERENCE, + ) - if node.ctx == astroid.Context.Load: - supported_protocol = supports_getitem - msg = "unsubscriptable-object" - elif node.ctx == astroid.Context.Store: - supported_protocol = supports_setitem - msg = "unsupported-assignment-operation" - elif node.ctx == astroid.Context.Del: - supported_protocol = supports_delitem - msg = "unsupported-delete-operation" + match node.ctx: + case astroid.Context.Load: + supported_protocol = supports_getitem + msg = "unsubscriptable-object" + case astroid.Context.Store: + supported_protocol = supports_setitem + msg = "unsupported-assignment-operation" + case astroid.Context.Del: + supported_protocol = supports_delitem + msg = "unsupported-delete-operation" if isinstance(node.value, nodes.SetComp): - # pylint: disable-next=possibly-used-before-assignment self.add_message(msg, args=node.value.as_string(), node=node.value) return @@ -2215,10 +2217,11 @@ def visit_await(self, node: nodes.Await) -> None: def _check_await_outside_coroutine(self, node: nodes.Await) -> None: node_scope = node.scope() while not isinstance(node_scope, nodes.Module): - if isinstance(node_scope, nodes.AsyncFunctionDef): - return - if isinstance(node_scope, (nodes.FunctionDef, nodes.Lambda)): - break + match node_scope: + case nodes.AsyncFunctionDef(): + return + case nodes.FunctionDef() | nodes.Lambda(): + break node_scope = node_scope.parent.scope() self.add_message("await-outside-async", node=node) diff --git a/pylint/checkers/utils.py b/pylint/checkers/utils.py index d87540b59a..416824d012 100644 --- a/pylint/checkers/utils.py +++ b/pylint/checkers/utils.py @@ -1005,14 +1005,15 @@ def find_except_wrapper_node_in_scope( ) -> nodes.ExceptHandler | None: """Return the ExceptHandler in which the node is, without going out of scope.""" for current in node.node_ancestors(): - if isinstance(current, astroid.scoped_nodes.LocalsDictNodeNG): - # If we're inside a function/class definition, we don't want to keep checking - # higher ancestors for `except` clauses, because if these exist, it means our - # function/class was defined in an `except` clause, rather than the current code - # actually running in an `except` clause. - return None - if isinstance(current, nodes.ExceptHandler): - return current + match current: + case astroid.scoped_nodes.LocalsDictNodeNG(): + # If we're inside a function/class definition, we don't want to keep checking + # higher ancestors for `except` clauses, because if these exist, it means our + # function/class was defined in an `except` clause, rather than the current code + # actually running in an `except` clause. + return None + case nodes.ExceptHandler(): + return current return None @@ -1083,18 +1084,18 @@ def _suppresses_exception( if not isinstance(exception, str): exception = exception.__name__ for arg in call.args: - inferred = safe_infer(arg) - if isinstance(inferred, nodes.ClassDef): - if inferred.name == exception: - return True - elif isinstance(inferred, nodes.Tuple): - for elt in inferred.elts: - inferred_elt = safe_infer(elt) - if ( - isinstance(inferred_elt, nodes.ClassDef) - and inferred_elt.name == exception - ): + match inferred := safe_infer(arg): + case nodes.ClassDef(): + if inferred.name == exception: return True + case nodes.Tuple(): + for elt in inferred.elts: + inferred_elt = safe_infer(elt) + if ( + isinstance(inferred_elt, nodes.ClassDef) + and inferred_elt.name == exception + ): + return True return False @@ -1266,32 +1267,30 @@ def is_inside_abstract_class(node: nodes.NodeNG) -> bool: def _supports_protocol( value: nodes.NodeNG, protocol_callback: Callable[[nodes.NodeNG], bool] ) -> bool: - if isinstance(value, nodes.ClassDef): - if not has_known_bases(value): - return True - # classobj can only be iterable if it has an iterable metaclass - meta = value.metaclass() - if meta is not None: - if protocol_callback(meta): + match value: + case nodes.ClassDef(): + if not has_known_bases(value): + return True + # classobj can only be iterable if it has an iterable metaclass + meta = value.metaclass() + if meta is not None: + if protocol_callback(meta): + return True + case astroid.BaseInstance(): + if not has_known_bases(value): + return True + if value.has_dynamic_getattr(): + return True + if protocol_callback(value): return True - if isinstance(value, astroid.BaseInstance): - if not has_known_bases(value): - return True - if value.has_dynamic_getattr(): - return True - if protocol_callback(value): - return True - if isinstance(value, nodes.ComprehensionScope): - return True + case nodes.ComprehensionScope(): + return True - if ( - isinstance(value, astroid.bases.Proxy) - and isinstance(value._proxied, astroid.BaseInstance) - and has_known_bases(value._proxied) - ): - value = value._proxied - return protocol_callback(value) + case astroid.bases.Proxy( + _proxied=astroid.BaseInstance() as p + ) if has_known_bases(p): + return protocol_callback(p) return False @@ -1433,14 +1432,13 @@ def function_arguments_are_ambiguous( if len(zippable_default[0]) != len(zippable_default[1]): return True for default1, default2 in zip(*zippable_default): - if isinstance(default1, nodes.Const) and isinstance(default2, nodes.Const): - if default1.value != default2.value: - return True - elif isinstance(default1, nodes.Name) and isinstance(default2, nodes.Name): - if default1.name != default2.name: + match (default1, default2): + case [nodes.Const(), nodes.Const()]: + return default1.value != default2.value # type: ignore[no-any-return] + case [nodes.Name(), nodes.Name()]: + return default1.name != default2.name # type: ignore[no-any-return] + case _: return True - else: - return True return False @@ -1616,31 +1614,21 @@ def is_node_in_type_annotation_context(node: nodes.NodeNG) -> bool: Check for 'AnnAssign', function 'Arguments', or part of function return type annotation. """ - # pylint: disable=too-many-boolean-expressions current_node, parent_node = node, node.parent while True: - if ( - ( - isinstance(parent_node, nodes.AnnAssign) - and parent_node.annotation == current_node - ) - or ( - isinstance(parent_node, nodes.Arguments) - and current_node - in ( - *parent_node.annotations, - *parent_node.posonlyargs_annotations, - *parent_node.kwonlyargs_annotations, - parent_node.varargannotation, - parent_node.kwargannotation, - ) - ) - or ( - isinstance(parent_node, nodes.FunctionDef) - and parent_node.returns == current_node - ) - ): - return True + match parent_node: + case nodes.AnnAssign(annotation=ann) if ann == current_node: + return True + case nodes.Arguments() if current_node in ( + *parent_node.annotations, + *parent_node.posonlyargs_annotations, + *parent_node.kwonlyargs_annotations, + parent_node.varargannotation, + parent_node.kwargannotation, + ): + return True + case nodes.FunctionDef(returns=ret) if ret == current_node: + return True current_node, parent_node = parent_node, parent_node.parent if isinstance(parent_node, nodes.Module): return False @@ -1721,10 +1709,11 @@ def is_test_condition( ) -> bool: """Returns true if the given node is being tested for truthiness.""" parent = parent or node.parent - if isinstance(parent, (nodes.While, nodes.If, nodes.IfExp, nodes.Assert)): - return node is parent.test or parent.test.parent_of(node) - if isinstance(parent, nodes.Comprehension): - return node in parent.ifs + match parent: + case nodes.While() | nodes.If() | nodes.IfExp() | nodes.Assert(): + return node is parent.test or parent.test.parent_of(node) + case nodes.Comprehension(): + return node in parent.ifs return is_call_of_name(parent, "bool") and parent.parent_of(node) @@ -2012,24 +2001,25 @@ def is_typing_member(node: nodes.NodeNG, names_to_check: tuple[str, ...]) -> boo """Check if `node` is a member of the `typing` module and has one of the names from `names_to_check`. """ - if isinstance(node, nodes.Name): - try: - import_from = node.lookup(node.name)[1][0] - except IndexError: - return False + match node: + case nodes.Name(): + try: + import_from = node.lookup(node.name)[1][0] + except IndexError: + return False - if isinstance(import_from, nodes.ImportFrom): + if isinstance(import_from, nodes.ImportFrom): + return ( + import_from.modname == "typing" + and import_from.real_name(node.name) in names_to_check + ) + case nodes.Attribute(): + inferred_module = safe_infer(node.expr) return ( - import_from.modname == "typing" - and import_from.real_name(node.name) in names_to_check + isinstance(inferred_module, nodes.Module) + and inferred_module.name == "typing" + and node.attrname in names_to_check ) - elif isinstance(node, nodes.Attribute): - inferred_module = safe_infer(node.expr) - return ( - isinstance(inferred_module, nodes.Module) - and inferred_module.name == "typing" - and node.attrname in names_to_check - ) return False @@ -2045,26 +2035,28 @@ def find_assigned_names_recursive( target: nodes.AssignName | nodes.BaseContainer, ) -> Iterator[str]: """Yield the names of assignment targets, accounting for nested ones.""" - if isinstance(target, nodes.AssignName): - if target.name is not None: - yield target.name - elif isinstance(target, nodes.BaseContainer): - for elt in target.elts: - yield from find_assigned_names_recursive(elt) + match target: + case nodes.AssignName(): + if target.name is not None: + yield target.name + case nodes.BaseContainer(): + for elt in target.elts: + yield from find_assigned_names_recursive(elt) def has_starred_node_recursive( - node: nodes.For | nodes.Comprehension | nodes.Set, + node: nodes.For | nodes.Comprehension | nodes.Set | nodes.Starred, ) -> Iterator[bool]: """Yield ``True`` if a Starred node is found recursively.""" - if isinstance(node, nodes.Starred): - yield True - elif isinstance(node, nodes.Set): - for elt in node.elts: - yield from has_starred_node_recursive(elt) - elif isinstance(node, (nodes.For, nodes.Comprehension)): - for elt in node.iter.elts: - yield from has_starred_node_recursive(elt) + match node: + case nodes.Starred(): + yield True + case nodes.Set(): + for elt in node.elts: + yield from has_starred_node_recursive(elt) + case nodes.For() | nodes.Comprehension(): + for elt in node.iter.elts: + yield from has_starred_node_recursive(elt) def is_hashable(node: nodes.NodeNG) -> bool: @@ -2110,16 +2102,15 @@ def _is_target_name_in_binop_side( target: nodes.AssignName | nodes.AssignAttr, side: nodes.NodeNG | None ) -> bool: """Determine whether the target name-like node is referenced in the side node.""" - if isinstance(side, nodes.Name): - if isinstance(target, nodes.AssignName): - return target.name == side.name # type: ignore[no-any-return] - return False - if isinstance(side, nodes.Attribute) and isinstance(target, nodes.AssignAttr): - return target.as_string() == side.as_string() # type: ignore[no-any-return] - if isinstance(side, nodes.Subscript) and isinstance(target, nodes.Subscript): - return subscript_chain_is_equal(target, side) - - return False + match (side, target): + case [nodes.Name(), _]: + return isinstance(target, nodes.AssignName) and target.name == side.name + case [nodes.Attribute(), nodes.AssignAttr()]: + return target.as_string() == side.as_string() # type: ignore[no-any-return] + case [nodes.Subscript(), nodes.Subscript()]: + return subscript_chain_is_equal(target, side) + case _: + return False def is_augmented_assign(node: nodes.Assign) -> tuple[bool, str]: @@ -2284,33 +2275,34 @@ def get_inverse_comparator(op: str) -> str: def not_condition_as_string( test_node: nodes.Compare | nodes.Name | nodes.UnaryOp | nodes.BoolOp | nodes.BinOp, ) -> str: - msg = f"not {test_node.as_string()}" - if isinstance(test_node, nodes.UnaryOp): - msg = test_node.operand.as_string() - elif isinstance(test_node, nodes.BoolOp): - msg = f"not ({test_node.as_string()})" - elif isinstance(test_node, nodes.Compare): - lhs = test_node.left - ops, rhs = test_node.ops[0] - lower_priority_expressions = ( - nodes.Lambda, - nodes.UnaryOp, - nodes.BoolOp, - nodes.IfExp, - nodes.NamedExpr, - ) - lhs = ( - f"({lhs.as_string()})" - if isinstance(lhs, lower_priority_expressions) - else lhs.as_string() - ) - rhs = ( - f"({rhs.as_string()})" - if isinstance(rhs, lower_priority_expressions) - else rhs.as_string() - ) - msg = f"{lhs} {get_inverse_comparator(ops)} {rhs}" - return msg + match test_node: + case nodes.UnaryOp(): + return test_node.operand.as_string() # type: ignore[no-any-return] + case nodes.BoolOp(): + return f"not ({test_node.as_string()})" + case nodes.Compare(): + lhs = test_node.left + ops, rhs = test_node.ops[0] + lower_priority_expressions = ( + nodes.Lambda, + nodes.UnaryOp, + nodes.BoolOp, + nodes.IfExp, + nodes.NamedExpr, + ) + lhs = ( + f"({lhs.as_string()})" + if isinstance(lhs, lower_priority_expressions) + else lhs.as_string() + ) + rhs = ( + f"({rhs.as_string()})" + if isinstance(rhs, lower_priority_expressions) + else rhs.as_string() + ) + return f"{lhs} {get_inverse_comparator(ops)} {rhs}" + case _: + return f"not {test_node.as_string()}" @lru_cache(maxsize=1000) diff --git a/pylint/checkers/variables.py b/pylint/checkers/variables.py index ace862ab22..5cf6c0c6bf 100644 --- a/pylint/checkers/variables.py +++ b/pylint/checkers/variables.py @@ -103,10 +103,11 @@ def _get_unpacking_extra_info(node: nodes.Assign, inferred: InferenceResult) -> """ more = "" if isinstance(inferred, DICT_TYPES): - if isinstance(node, nodes.Assign): - more = node.value.as_string() - elif isinstance(node, nodes.For): - more = node.iter.as_string() + match node: + case nodes.Assign(): + more = node.value.as_string() + case nodes.For(): + more = node.iter.as_string() return more inferred_module = inferred.root().name @@ -671,17 +672,21 @@ def _inferred_to_define_name_raise_or_return( for handler in handlers ) - if isinstance(node, (nodes.With, nodes.For, nodes.While)): - return NamesConsumer._defines_name_raises_or_returns_recursive(name, node) - - if isinstance(node, nodes.Match): - return all( - NamesConsumer._defines_name_raises_or_returns_recursive(name, case) - for case in node.cases - ) + match node: + case nodes.With() | nodes.For() | nodes.While(): + return NamesConsumer._defines_name_raises_or_returns_recursive( + name, node + ) - if not isinstance(node, nodes.If): - return False + case nodes.Match(): + return all( + NamesConsumer._defines_name_raises_or_returns_recursive(name, case) + for case in node.cases + ) + case nodes.If(): + pass + case _: + return False # Be permissive if there is a break or a continue if any(node.nodes_of_class(nodes.Break, nodes.Continue)): @@ -750,14 +755,15 @@ def _uncertain_nodes_if_tests( """ uncertain_nodes = [] for other_node in found_nodes: - if isinstance(other_node, nodes.AssignName): - name = other_node.name - elif isinstance(other_node, (nodes.Import, nodes.ImportFrom)): - name = node.name - elif isinstance(other_node, (nodes.FunctionDef, nodes.ClassDef)): - name = other_node.name - else: - continue + match other_node: + case nodes.AssignName(): + name = other_node.name + case nodes.Import() | nodes.ImportFrom(): + name = node.name + case nodes.FunctionDef() | nodes.ClassDef(): + name = other_node.name + case _: + continue all_if = [ n @@ -970,23 +976,27 @@ def _defines_name_raises_or_returns_recursive( for stmt in node.get_children(): if NamesConsumer._defines_name_raises_or_returns(name, stmt): return True - if isinstance(stmt, (nodes.If, nodes.With)): - if any( - NamesConsumer._defines_name_raises_or_returns(name, nested_stmt) - for nested_stmt in stmt.get_children() + match stmt: + case nodes.If() | nodes.With(): + if any( + NamesConsumer._defines_name_raises_or_returns(name, nested_stmt) + for nested_stmt in stmt.get_children() + ): + return True + case nodes.Try() if ( + not stmt.finalbody + and NamesConsumer._defines_name_raises_or_returns_recursive( + name, stmt + ) ): return True - if ( - isinstance(stmt, nodes.Try) - and not stmt.finalbody - and NamesConsumer._defines_name_raises_or_returns_recursive(name, stmt) - ): - return True - if isinstance(stmt, nodes.Match): - return all( - NamesConsumer._defines_name_raises_or_returns_recursive(name, case) - for case in stmt.cases - ) + case nodes.Match(): + return all( + NamesConsumer._defines_name_raises_or_returns_recursive( + name, case + ) + for case in stmt.cases + ) return False @staticmethod @@ -1347,29 +1357,30 @@ def visit_for(self, node: nodes.For) -> None: if any(isinstance(target, nodes.Starred) for target in targets): return - if isinstance(inferred, nodes.Dict): - if isinstance(node.iter, nodes.Name): - # If this a case of 'dict-items-missing-iter', we don't want to - # report it as an 'unbalanced-dict-unpacking' as well - # TODO (performance), merging both checks would streamline this - if len(targets) == 2: - return + match inferred: + case nodes.Dict(): + if isinstance(node.iter, nodes.Name): + # If this a case of 'dict-items-missing-iter', we don't want to + # report it as an 'unbalanced-dict-unpacking' as well + # TODO (performance), merging both checks would streamline this + if len(targets) == 2: + return - else: - is_starred_targets = any( - isinstance(target, nodes.Starred) for target in targets - ) - for value in values: - value_length = self._get_value_length(value) - is_valid_star_unpack = is_starred_targets and value_length >= len( - targets + case _: + is_starred_targets = any( + isinstance(target, nodes.Starred) for target in targets ) - if len(targets) != value_length and not is_valid_star_unpack: - details = _get_unpacking_extra_info(node, inferred) - self._report_unbalanced_unpacking( - node, inferred, targets, value_length, details + for value in values: + value_length = self._get_value_length(value) + is_valid_star_unpack = is_starred_targets and value_length >= len( + targets ) - break + if len(targets) != value_length and not is_valid_star_unpack: + details = _get_unpacking_extra_info(node, inferred) + self._report_unbalanced_unpacking( + node, inferred, targets, value_length, details + ) + break def leave_for(self, node: nodes.For) -> None: self._store_type_annotation_names(node) @@ -2226,18 +2237,19 @@ def _in_lambda_or_comprehension_body( while parent is not None: if parent is frame: return False - if isinstance(parent, nodes.Lambda) and child is not parent.args: - # Body of lambda should not have access to class attributes. - return True - if isinstance(parent, nodes.Comprehension) and child is not parent.iter: - # Only iter of list/set/dict/generator comprehension should have access. - return True - if isinstance(parent, nodes.ComprehensionScope) and not ( - parent.generators and child is parent.generators[0] - ): - # Body of list/set/dict/generator comprehension should not have access to class attributes. - # Furthermore, only the first generator (if multiple) in comprehension should have access. - return True + match parent: + case nodes.Lambda() if child is not parent.args: + # Body of lambda should not have access to class attributes. + return True + case nodes.Comprehension() if child is not parent.iter: + # Only iter of list/set/dict/generator comprehension should have access. + return True + case nodes.ComprehensionScope() if not ( + parent.generators and child is parent.generators[0] + ): + # Body of list/set/dict/generator comprehension should not have access to class attributes. + # Furthermore, only the first generator (if multiple) in comprehension should have access. + return True child = parent parent = parent.parent return False @@ -2415,18 +2427,20 @@ def _maybe_used_and_assigned_at_once(defstmt: _base_nodes.Statement) -> bool: for elt in defstmt.value.elts if isinstance(elt, (*NODES_WITH_VALUE_ATTR, nodes.IfExp, nodes.Match)) ) - value = defstmt.value - if isinstance(value, nodes.IfExp): - return True - if isinstance(value, nodes.Lambda) and isinstance(value.body, nodes.IfExp): - return True - if isinstance(value, nodes.Dict) and any( - isinstance(item[0], nodes.IfExp) or isinstance(item[1], nodes.IfExp) - for item in value.items - ): - return True - if not isinstance(value, nodes.Call): - return False + match value := defstmt.value: + case nodes.IfExp(): + return True + case nodes.Lambda(body=nodes.IfExp()): + return True + case nodes.Dict() if any( + isinstance(item[0], nodes.IfExp) or isinstance(item[1], nodes.IfExp) + for item in value.items + ): + return True + case nodes.Call(): + pass + case _: + return False return any( any(isinstance(kwarg.value, nodes.IfExp) for kwarg in call.keywords) or any(isinstance(arg, nodes.IfExp) for arg in call.args) @@ -2547,13 +2561,13 @@ def _is_never_evaluated( """Check if a NamedExpr is inside a side of if ... else that never gets evaluated. """ - inferred_test = utils.safe_infer(defnode_parent.test) - if isinstance(inferred_test, nodes.Const): - if inferred_test.value is True and defnode == defnode_parent.orelse: + match utils.safe_infer(defnode_parent.test): + case nodes.Const(value=True) if defnode == defnode_parent.orelse: return True - if inferred_test.value is False and defnode == defnode_parent.body: + case nodes.Const(value=False) if defnode == defnode_parent.body: return True - return False + case _: + return False @staticmethod def _is_variable_annotation_in_function(node: nodes.Name) -> bool: @@ -2828,20 +2842,21 @@ def _check_is_unused( if _has_locals_call_after_node(stmt, node.scope()): message_name = "possibly-unused-variable" else: - if isinstance(stmt, nodes.Import): - if asname is not None: - msg = f"{qname} imported as {asname}" - else: - msg = f"import {name}" - self.add_message("unused-import", args=msg, node=stmt) - return - if isinstance(stmt, nodes.ImportFrom): - if asname is not None: - msg = f"{qname} imported from {stmt.modname} as {asname}" - else: - msg = f"{name} imported from {stmt.modname}" - self.add_message("unused-import", args=msg, node=stmt) - return + match stmt: + case nodes.Import(): + if asname is not None: + msg = f"{qname} imported as {asname}" + else: + msg = f"import {name}" + self.add_message("unused-import", args=msg, node=stmt) + return + case nodes.ImportFrom(): + if asname is not None: + msg = f"{qname} imported from {stmt.modname} as {asname}" + else: + msg = f"{name} imported from {stmt.modname}" + self.add_message("unused-import", args=msg, node=stmt) + return message_name = "unused-variable" if isinstance(stmt, nodes.FunctionDef) and stmt.decorators: @@ -2863,13 +2878,11 @@ def _is_name_ignored( name: str, ) -> re.Pattern[str] | re.Match[str] | None: authorized_rgx = self.linter.config.dummy_variables_rgx - if ( - isinstance(stmt, nodes.AssignName) - and isinstance(stmt.parent, nodes.Arguments) - ) or isinstance(stmt, nodes.Arguments): - regex: re.Pattern[str] = self.linter.config.ignored_argument_names - else: - regex = authorized_rgx + match stmt: + case nodes.AssignName(parent=nodes.Arguments()) | nodes.Arguments(): + regex: re.Pattern[str] = self.linter.config.ignored_argument_names + case _: + regex = authorized_rgx # See https://stackoverflow.com/a/47007761/2519059 to # understand what this function return. Please do NOT use # this elsewhere, this is confusing for no benefit @@ -3009,16 +3022,17 @@ def _comprehension_between_frame_and_node(node: nodes.Name) -> bool: def _store_type_annotation_node(self, type_annotation: nodes.NodeNG) -> None: """Given a type annotation, store all the name nodes it refers to.""" - if isinstance(type_annotation, nodes.Name): - self._type_annotation_names.append(type_annotation.name) - return - - if isinstance(type_annotation, nodes.Attribute): - self._store_type_annotation_node(type_annotation.expr) - return - - if not isinstance(type_annotation, nodes.Subscript): - return + match type_annotation: + case nodes.Name(): + self._type_annotation_names.append(type_annotation.name) + return + case nodes.Attribute(): + self._store_type_annotation_node(type_annotation.expr) + return + case nodes.Subscript(): + pass + case _: + return if ( isinstance(type_annotation.value, nodes.Attribute) @@ -3045,12 +3059,15 @@ def _check_self_cls_assign(self, node: nodes.Assign) -> None: """Check that self/cls don't get assigned.""" assign_names: set[str | None] = set() for target in node.targets: - if isinstance(target, nodes.AssignName): - assign_names.add(target.name) - elif isinstance(target, nodes.Tuple): - assign_names.update( - elt.name for elt in target.elts if isinstance(elt, nodes.AssignName) - ) + match target: + case nodes.AssignName(): + assign_names.add(target.name) + case nodes.Tuple(): + assign_names.update( + elt.name + for elt in target.elts + if isinstance(elt, nodes.AssignName) + ) scope = node.scope() nonlocals_with_same_name = node.scope().parent and any( child for child in scope.body if isinstance(child, nodes.Nonlocal) @@ -3112,15 +3129,16 @@ def _get_value_length(value_node: nodes.NodeNG) -> int: value_subnodes = VariablesChecker._nodes_to_unpack(value_node) if value_subnodes is not None: return len(value_subnodes) - if isinstance(value_node, nodes.Const) and isinstance( - value_node.value, (str, bytes) - ): - return len(value_node.value) - if isinstance(value_node, nodes.Subscript): - step = value_node.slice.step or 1 - splice_range = value_node.slice.upper.value - value_node.slice.lower.value - # RUF046 says the return of 'math.ceil' is always an int, mypy doesn't see it - return math.ceil(splice_range / step) # type: ignore[no-any-return] + match value_node: + case nodes.Const(value=str() | bytes()): + return len(value_node.value) + case nodes.Subscript(): + step = value_node.slice.step or 1 + splice_range = ( + value_node.slice.upper.value - value_node.slice.lower.value + ) + # RUF046 says the return of 'math.ceil' is always an int, mypy doesn't see it + return math.ceil(splice_range / step) # type: ignore[no-any-return] return 1 @staticmethod From dd1dea321d91068ccd964773caefd22897e971de Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 30 Aug 2025 01:16:33 +0200 Subject: [PATCH 2/5] Add test case for skipped multi-assignments [unnecessary-lambda-assignment] --- .../functional/u/unnecessary/unnecessary_lambda_assignment.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/functional/u/unnecessary/unnecessary_lambda_assignment.py b/tests/functional/u/unnecessary/unnecessary_lambda_assignment.py index 65b735f7e5..1ee26ddebb 100644 --- a/tests/functional/u/unnecessary/unnecessary_lambda_assignment.py +++ b/tests/functional/u/unnecessary/unnecessary_lambda_assignment.py @@ -36,3 +36,7 @@ # Flag lambda expression assignments via named expressions as well. if (e := lambda: 2) and e(): # [unnecessary-lambda-assignment] pass + +# Skip checking multi-assignments +a = b = lambda: None +a, b = x = lambda: None, None From 18cc751b8ec7111c11a32ea5239c254c81ea9068 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 30 Aug 2025 18:40:03 +0200 Subject: [PATCH 3/5] Fix regression in unnecessary-lambda-assignment check --- pylint/checkers/lambda_expressions.py | 4 ++-- .../u/unnecessary/unnecessary_lambda_assignment.py | 6 +++--- .../u/unnecessary/unnecessary_lambda_assignment.txt | 2 ++ 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/pylint/checkers/lambda_expressions.py b/pylint/checkers/lambda_expressions.py index 05df002582..46e259e0cf 100644 --- a/pylint/checkers/lambda_expressions.py +++ b/pylint/checkers/lambda_expressions.py @@ -41,7 +41,7 @@ def visit_assign(self, node: nodes.Assign) -> None: """Check if lambda expression is assigned to a variable.""" match node: case nodes.Assign( - targets=[nodes.AssignName()], value=nodes.Lambda() as value + targets=[nodes.AssignName(), *_], value=nodes.Lambda() as value ): self.add_message( "unnecessary-lambda-assignment", @@ -49,7 +49,7 @@ def visit_assign(self, node: nodes.Assign) -> None: confidence=HIGH, ) case nodes.Assign( - targets=[nodes.Tuple() as target], + targets=[nodes.Tuple() as target, *_], value=nodes.Tuple() | nodes.List() as value, ): # Iterate over tuple unpacking assignment elements and diff --git a/tests/functional/u/unnecessary/unnecessary_lambda_assignment.py b/tests/functional/u/unnecessary/unnecessary_lambda_assignment.py index 1ee26ddebb..595a4a7e39 100644 --- a/tests/functional/u/unnecessary/unnecessary_lambda_assignment.py +++ b/tests/functional/u/unnecessary/unnecessary_lambda_assignment.py @@ -37,6 +37,6 @@ if (e := lambda: 2) and e(): # [unnecessary-lambda-assignment] pass -# Skip checking multi-assignments -a = b = lambda: None -a, b = x = lambda: None, None +# Multi-assignments +a = b = lambda: None # [unnecessary-lambda-assignment] +a, b = x = lambda: None, None # [unnecessary-lambda-assignment] diff --git a/tests/functional/u/unnecessary/unnecessary_lambda_assignment.txt b/tests/functional/u/unnecessary/unnecessary_lambda_assignment.txt index 150e738cdf..23edc8e1cf 100644 --- a/tests/functional/u/unnecessary/unnecessary_lambda_assignment.txt +++ b/tests/functional/u/unnecessary/unnecessary_lambda_assignment.txt @@ -13,3 +13,5 @@ unnecessary-lambda-assignment:23:4:23:15::"Lambda expression assigned to unnecessary-lambda-assignment:26:10:26:21::"Lambda expression assigned to a variable. Define a function using the ""def"" keyword instead.":HIGH unnecessary-lambda-assignment:26:23:26:34::"Lambda expression assigned to a variable. Define a function using the ""def"" keyword instead.":HIGH unnecessary-lambda-assignment:37:9:37:18::"Lambda expression assigned to a variable. Define a function using the ""def"" keyword instead.":HIGH +unnecessary-lambda-assignment:41:8:41:20::"Lambda expression assigned to a variable. Define a function using the ""def"" keyword instead.":HIGH +unnecessary-lambda-assignment:42:11:42:23::"Lambda expression assigned to a variable. Define a function using the ""def"" keyword instead.":HIGH From 933a16c14498a7b6629538b01af8b8dff86ac061 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 30 Aug 2025 21:40:33 +0200 Subject: [PATCH 4/5] Raise AssertionError for uncovered fallback case Co-authored-by: Pierre Sassoulas --- pylint/checkers/variables.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pylint/checkers/variables.py b/pylint/checkers/variables.py index 5cf6c0c6bf..18d4ffe6a0 100644 --- a/pylint/checkers/variables.py +++ b/pylint/checkers/variables.py @@ -685,8 +685,10 @@ def _inferred_to_define_name_raise_or_return( ) case nodes.If(): pass - case _: - return False + case _: # pragma: no cover + # The function is only called for Try, With, For, While, Match and + # If nodes. All of which are being handled above. + raise AssertionError # Be permissive if there is a break or a continue if any(node.nodes_of_class(nodes.Break, nodes.Continue)): From c738ed935711243aa5e4391ed3e63221fe98b1b1 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 31 Aug 2025 21:46:25 +0200 Subject: [PATCH 5/5] Cleanup --- pylint/checkers/utils.py | 4 ++-- pylint/checkers/variables.py | 41 ++++++++++++++++++------------------ 2 files changed, 22 insertions(+), 23 deletions(-) diff --git a/pylint/checkers/utils.py b/pylint/checkers/utils.py index 416824d012..e274a44c48 100644 --- a/pylint/checkers/utils.py +++ b/pylint/checkers/utils.py @@ -2103,8 +2103,8 @@ def _is_target_name_in_binop_side( ) -> bool: """Determine whether the target name-like node is referenced in the side node.""" match (side, target): - case [nodes.Name(), _]: - return isinstance(target, nodes.AssignName) and target.name == side.name + case [nodes.Name(), nodes.AssignName()]: + return target.name == side.name # type: ignore[no-any-return] case [nodes.Attribute(), nodes.AssignAttr()]: return target.as_string() == side.as_string() # type: ignore[no-any-return] case [nodes.Subscript(), nodes.Subscript()]: diff --git a/pylint/checkers/variables.py b/pylint/checkers/variables.py index 18d4ffe6a0..adf33ef843 100644 --- a/pylint/checkers/variables.py +++ b/pylint/checkers/variables.py @@ -1359,30 +1359,29 @@ def visit_for(self, node: nodes.For) -> None: if any(isinstance(target, nodes.Starred) for target in targets): return - match inferred: - case nodes.Dict(): - if isinstance(node.iter, nodes.Name): - # If this a case of 'dict-items-missing-iter', we don't want to - # report it as an 'unbalanced-dict-unpacking' as well - # TODO (performance), merging both checks would streamline this - if len(targets) == 2: - return + if isinstance(inferred, nodes.Dict): + if isinstance(node.iter, nodes.Name): + # If this a case of 'dict-items-missing-iter', we don't want to + # report it as an 'unbalanced-dict-unpacking' as well + # TODO (performance), merging both checks would streamline this + if len(targets) == 2: + return - case _: - is_starred_targets = any( - isinstance(target, nodes.Starred) for target in targets + else: + is_starred_targets = any( + isinstance(target, nodes.Starred) for target in targets + ) + for value in values: + value_length = self._get_value_length(value) + is_valid_star_unpack = is_starred_targets and value_length >= len( + targets ) - for value in values: - value_length = self._get_value_length(value) - is_valid_star_unpack = is_starred_targets and value_length >= len( - targets + if len(targets) != value_length and not is_valid_star_unpack: + details = _get_unpacking_extra_info(node, inferred) + self._report_unbalanced_unpacking( + node, inferred, targets, value_length, details ) - if len(targets) != value_length and not is_valid_star_unpack: - details = _get_unpacking_extra_info(node, inferred) - self._report_unbalanced_unpacking( - node, inferred, targets, value_length, details - ) - break + break def leave_for(self, node: nodes.For) -> None: self._store_type_annotation_names(node)