From 0e5ef7c407a1f122edb74e6f73daf079aaa2ce2f Mon Sep 17 00:00:00 2001 From: Mark Byrne Date: Wed, 11 Jun 2025 16:26:09 +0200 Subject: [PATCH 1/5] Add ``match-statements`` checker and the following message: ``unreachable-match-patterns``. This will emit an error message when a name capture pattern is used in a match statement which would make the remaining patterns unreachable. This code is a SyntaxError at runtime. Closes #7128 --- .../u/unreachable-match-patterns/bad.py | 13 ++++ .../u/unreachable-match-patterns/good.py | 15 +++++ .../u/unreachable-match-patterns/related.rst | 1 + doc/user_guide/checkers/features.rst | 12 ++++ doc/user_guide/messages/messages_overview.rst | 1 + doc/whatsnew/fragments/7128.new_check | 6 ++ pylint/checkers/match_statements_checker.py | 59 +++++++++++++++++++ .../u/unreachable_match_patterns.py | 15 +++++ .../u/unreachable_match_patterns.rc | 2 + .../u/unreachable_match_patterns.txt | 2 + 10 files changed, 126 insertions(+) create mode 100644 doc/data/messages/u/unreachable-match-patterns/bad.py create mode 100644 doc/data/messages/u/unreachable-match-patterns/good.py create mode 100644 doc/data/messages/u/unreachable-match-patterns/related.rst create mode 100644 doc/whatsnew/fragments/7128.new_check create mode 100644 pylint/checkers/match_statements_checker.py create mode 100644 tests/functional/u/unreachable_match_patterns.py create mode 100644 tests/functional/u/unreachable_match_patterns.rc create mode 100644 tests/functional/u/unreachable_match_patterns.txt diff --git a/doc/data/messages/u/unreachable-match-patterns/bad.py b/doc/data/messages/u/unreachable-match-patterns/bad.py new file mode 100644 index 0000000000..deafc630a9 --- /dev/null +++ b/doc/data/messages/u/unreachable-match-patterns/bad.py @@ -0,0 +1,13 @@ +red = 0 +green = 1 +blue = 2 + + +color = blue +match color: + case red: # [unreachable-match-patterns] + print("I see red!") + case green: # [unreachable-match-patterns] + print("Grass is green") + case blue: + print("I'm feeling the blues :(") diff --git a/doc/data/messages/u/unreachable-match-patterns/good.py b/doc/data/messages/u/unreachable-match-patterns/good.py new file mode 100644 index 0000000000..0441b06b24 --- /dev/null +++ b/doc/data/messages/u/unreachable-match-patterns/good.py @@ -0,0 +1,15 @@ +from enum import Enum +class Color(Enum): + RED = 0 + GREEN = 1 + BLUE = 2 + + +color = Color.BLUE +match color: + case Color.RED: + print("I see red!") + case Color.GREEN: + print("Grass is green") + case Color.BLUE: + print("I'm feeling the blues :(") diff --git a/doc/data/messages/u/unreachable-match-patterns/related.rst b/doc/data/messages/u/unreachable-match-patterns/related.rst new file mode 100644 index 0000000000..b588ce80d5 --- /dev/null +++ b/doc/data/messages/u/unreachable-match-patterns/related.rst @@ -0,0 +1 @@ +- `PEP 636 `_ diff --git a/doc/user_guide/checkers/features.rst b/doc/user_guide/checkers/features.rst index 670b7d67ec..5260c3676c 100644 --- a/doc/user_guide/checkers/features.rst +++ b/doc/user_guide/checkers/features.rst @@ -673,6 +673,18 @@ Logging checker Messages format-interpolation is disabled then you can use str.format. +Match Statements checker +~~~~~~~~~~~~~~~~~~~~~~~~ + +Verbatim name of the checker is ``match_statements``. + +Match Statements checker Messages +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +:unreachable-match-patterns (E5000): *The name capture `case %s` makes the remaining patterns unreachable. Use a dotted name(for example an enum) to fix this* + Emitted when a name capture pattern in a match statement is used and there + are case statements below it. + + Method Args checker ~~~~~~~~~~~~~~~~~~~ diff --git a/doc/user_guide/messages/messages_overview.rst b/doc/user_guide/messages/messages_overview.rst index fc487fc25c..fb8c2f3aa7 100644 --- a/doc/user_guide/messages/messages_overview.rst +++ b/doc/user_guide/messages/messages_overview.rst @@ -165,6 +165,7 @@ All messages in the error category: error/unexpected-special-method-signature error/unhashable-member error/unpacking-non-sequence + error/unreachable-match-patterns error/unrecognized-inline-option error/unrecognized-option error/unsubscriptable-object diff --git a/doc/whatsnew/fragments/7128.new_check b/doc/whatsnew/fragments/7128.new_check new file mode 100644 index 0000000000..5d8df3c427 --- /dev/null +++ b/doc/whatsnew/fragments/7128.new_check @@ -0,0 +1,6 @@ +Add ``match-statements`` checker and the following message: +``unreachable-match-patterns``. +This will emit an error message when a name capture pattern is used in a match statement which would make the remaining patterns unreachable. +This code is a SyntaxError at runtime. + +Closes #7128 diff --git a/pylint/checkers/match_statements_checker.py b/pylint/checkers/match_statements_checker.py new file mode 100644 index 0000000000..06ec179125 --- /dev/null +++ b/pylint/checkers/match_statements_checker.py @@ -0,0 +1,59 @@ +# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt + +"""Match statement checker for Python code.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from astroid import nodes + +from pylint.checkers import BaseChecker +from pylint.checkers.utils import only_required_for_messages +from pylint.interfaces import HIGH + +if TYPE_CHECKING: + from pylint.lint import PyLinter + + +class MatchStatementChecker(BaseChecker): + name = "match_statements" + msgs = { + "E5000": ( + "The name capture `case %s` makes the remaining patterns unreachable. " + "Use a dotted name(for example an enum) to fix this", + "unreachable-match-patterns", + "Emitted when a name capture pattern in a match statement is used " + "and there are case statements below it.", + ) + } + + def open(self) -> None: + py_version = self.linter.config.py_version + self._py310_plus = py_version >= (3, 10) + + @only_required_for_messages("unreachable-match-patterns") + def visit_match(self, node: nodes.Match) -> None: + """Check if a name capture pattern prevents the other cases from being + reached + """ + for idx, case in enumerate(node.cases): + if ( + isinstance(case.pattern, nodes.MatchAs) + and case.pattern.pattern is None + and isinstance(case.pattern.name, nodes.AssignName) + and idx < len(node.cases) - 1 + and self._py310_plus + ): + self.add_message( + "unreachable-match-patterns", + node=case, + args=case.pattern.name.name, + confidence=HIGH, + ) + + +def register(linter: PyLinter) -> None: + linter.register_checker(MatchStatementChecker(linter)) diff --git a/tests/functional/u/unreachable_match_patterns.py b/tests/functional/u/unreachable_match_patterns.py new file mode 100644 index 0000000000..19986fbccd --- /dev/null +++ b/tests/functional/u/unreachable_match_patterns.py @@ -0,0 +1,15 @@ +"""Functional tests for the ``unreachable-match-patterns`` message""" + + +a = 'a' +b = 'b' +s = 'a' + + +match s: + case a: # [unreachable-match-patterns] + pass + case b: # [unreachable-match-patterns] + pass + case s: + pass diff --git a/tests/functional/u/unreachable_match_patterns.rc b/tests/functional/u/unreachable_match_patterns.rc new file mode 100644 index 0000000000..68a8c8ef15 --- /dev/null +++ b/tests/functional/u/unreachable_match_patterns.rc @@ -0,0 +1,2 @@ +[testoptions] +min_pyver=3.10 diff --git a/tests/functional/u/unreachable_match_patterns.txt b/tests/functional/u/unreachable_match_patterns.txt new file mode 100644 index 0000000000..c847ff79b4 --- /dev/null +++ b/tests/functional/u/unreachable_match_patterns.txt @@ -0,0 +1,2 @@ +unreachable-match-patterns:10:0:None:None::The name capture `case a` makes the remaining patterns unreachable. Use a dotted name(for example an enum) to fix this:HIGH +unreachable-match-patterns:12:0:None:None::The name capture `case b` makes the remaining patterns unreachable. Use a dotted name(for example an enum) to fix this:HIGH From a4c6cf8dbbcff34a53a543f3fc66db30b427dfc4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 12 Jun 2025 08:56:05 +0000 Subject: [PATCH 2/5] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- doc/data/messages/u/unreachable-match-patterns/good.py | 2 ++ pylint/checkers/match_statements_checker.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/data/messages/u/unreachable-match-patterns/good.py b/doc/data/messages/u/unreachable-match-patterns/good.py index 0441b06b24..0278641b89 100644 --- a/doc/data/messages/u/unreachable-match-patterns/good.py +++ b/doc/data/messages/u/unreachable-match-patterns/good.py @@ -1,4 +1,6 @@ from enum import Enum + + class Color(Enum): RED = 0 GREEN = 1 diff --git a/pylint/checkers/match_statements_checker.py b/pylint/checkers/match_statements_checker.py index 06ec179125..31ff11ff1c 100644 --- a/pylint/checkers/match_statements_checker.py +++ b/pylint/checkers/match_statements_checker.py @@ -37,7 +37,7 @@ def open(self) -> None: @only_required_for_messages("unreachable-match-patterns") def visit_match(self, node: nodes.Match) -> None: """Check if a name capture pattern prevents the other cases from being - reached + reached. """ for idx, case in enumerate(node.cases): if ( From e54d1ecd01296f218e429c91da915680c4e373a5 Mon Sep 17 00:00:00 2001 From: Mark Byrne Date: Thu, 12 Jun 2025 11:01:39 +0200 Subject: [PATCH 3/5] Remove the trailing `/` from the url. --- doc/data/messages/u/unreachable-match-patterns/related.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/data/messages/u/unreachable-match-patterns/related.rst b/doc/data/messages/u/unreachable-match-patterns/related.rst index b588ce80d5..bb0fe8f41e 100644 --- a/doc/data/messages/u/unreachable-match-patterns/related.rst +++ b/doc/data/messages/u/unreachable-match-patterns/related.rst @@ -1 +1 @@ -- `PEP 636 `_ +- `PEP 636 `_ From 4a742e827a782e1bc7843b21960592492f0c0381 Mon Sep 17 00:00:00 2001 From: Mark Byrne Date: Mon, 16 Jun 2025 23:05:08 +0200 Subject: [PATCH 4/5] Remove the unnecessary logic which checks if the version of Python is 3.10 or higher. --- pylint/checkers/match_statements_checker.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pylint/checkers/match_statements_checker.py b/pylint/checkers/match_statements_checker.py index 31ff11ff1c..dc7c7c41bd 100644 --- a/pylint/checkers/match_statements_checker.py +++ b/pylint/checkers/match_statements_checker.py @@ -30,10 +30,6 @@ class MatchStatementChecker(BaseChecker): ) } - def open(self) -> None: - py_version = self.linter.config.py_version - self._py310_plus = py_version >= (3, 10) - @only_required_for_messages("unreachable-match-patterns") def visit_match(self, node: nodes.Match) -> None: """Check if a name capture pattern prevents the other cases from being @@ -45,7 +41,6 @@ def visit_match(self, node: nodes.Match) -> None: and case.pattern.pattern is None and isinstance(case.pattern.name, nodes.AssignName) and idx < len(node.cases) - 1 - and self._py310_plus ): self.add_message( "unreachable-match-patterns", From 6488491b608d6ebcf412ae6fe226121e85c752ea Mon Sep 17 00:00:00 2001 From: Mark Byrne Date: Mon, 16 Jun 2025 23:07:17 +0200 Subject: [PATCH 5/5] Remove unnecessary `.rc` file from the functional test. --- tests/functional/u/unreachable_match_patterns.rc | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 tests/functional/u/unreachable_match_patterns.rc diff --git a/tests/functional/u/unreachable_match_patterns.rc b/tests/functional/u/unreachable_match_patterns.rc deleted file mode 100644 index 68a8c8ef15..0000000000 --- a/tests/functional/u/unreachable_match_patterns.rc +++ /dev/null @@ -1,2 +0,0 @@ -[testoptions] -min_pyver=3.10