From 7ddbd4bd62011fd1bb6565d7807a4b3aab09ca3e Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Wed, 22 May 2024 09:11:50 +0200 Subject: [PATCH 1/4] [mccabe - too-complex] Add each match case as an edge in the graph We decided against an option to permit to not count them. Refs #9667 --- doc/whatsnew/fragments/9667.false_negative | 3 ++ pylint/extensions/mccabe.py | 44 ++++++++++++++-------- tests/functional/ext/mccabe/mccabe.py | 37 ++++++++++++++++++ tests/functional/ext/mccabe/mccabe.txt | 2 + 4 files changed, 70 insertions(+), 16 deletions(-) create mode 100644 doc/whatsnew/fragments/9667.false_negative diff --git a/doc/whatsnew/fragments/9667.false_negative b/doc/whatsnew/fragments/9667.false_negative new file mode 100644 index 0000000000..92dfa97031 --- /dev/null +++ b/doc/whatsnew/fragments/9667.false_negative @@ -0,0 +1,3 @@ +Match cases are now counted as an edge in the McCabe graph, and will increase the complexity accordingly. + +Refs #9667 diff --git a/pylint/extensions/mccabe.py b/pylint/extensions/mccabe.py index 45bd35cee6..f494f95ea9 100644 --- a/pylint/extensions/mccabe.py +++ b/pylint/extensions/mccabe.py @@ -39,7 +39,7 @@ | nodes.Await ) -_SubGraphNodes: TypeAlias = nodes.If | nodes.Try | nodes.For | nodes.While +_SubGraphNodes: TypeAlias = nodes.If | nodes.Try | nodes.For | nodes.While | nodes.Match _AppendableNodeT = TypeVar( "_AppendableNodeT", bound=_StatementNodes | nodes.While | nodes.FunctionDef ) @@ -106,6 +106,9 @@ def visitWith(self, node: nodes.With) -> None: visitAsyncWith = visitWith + 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: return None @@ -117,9 +120,9 @@ def _subgraph( self, node: _SubGraphNodes, name: str, - extra_blocks: Sequence[nodes.ExceptHandler] = (), + extra_blocks: Sequence[nodes.ExceptHandler | nodes.MatchCase] = (), ) -> None: - """Create the subgraphs representing any `if` and `for` statements.""" + """Create the subgraphs representing any `if`, `for` or `match` statements.""" if self.graph is None: # global loop self.graph = PathGraph(node) @@ -134,23 +137,32 @@ def _subgraph_parse( self, node: _SubGraphNodes, pathnode: _SubGraphNodes, - extra_blocks: Sequence[nodes.ExceptHandler], + extra_blocks: Sequence[nodes.ExceptHandler | nodes.MatchCase], ) -> None: - """Parse the body and any `else` block of `if` and `for` statements.""" + """Parse the body and any `else` block of `if`, `for` or `match` statements.""" loose_ends = [] - self.tail = node - self.dispatch_list(node.body) - loose_ends.append(self.tail) - for extra in extra_blocks: - self.tail = node - self.dispatch_list(extra.body) - loose_ends.append(self.tail) - if node.orelse: + if isinstance(node, nodes.Match): + for case in extra_blocks: + if isinstance(case, nodes.MatchCase): + self.tail = node + self.dispatch_list(case.body) + loose_ends.append(self.tail) + loose_ends.append(node) + else: self.tail = node - self.dispatch_list(node.orelse) + self.dispatch_list(node.body) loose_ends.append(self.tail) - else: - loose_ends.append(node) + for extra in extra_blocks: + self.tail = node + self.dispatch_list(extra.body) + loose_ends.append(self.tail) + if node.orelse: + self.tail = node + self.dispatch_list(node.orelse) + loose_ends.append(self.tail) + else: + loose_ends.append(node) + if node and self.graph: bottom = f"{self._bottom_counter}" self._bottom_counter += 1 diff --git a/tests/functional/ext/mccabe/mccabe.py b/tests/functional/ext/mccabe/mccabe.py index 2c6d57a70b..b670b4a40f 100644 --- a/tests/functional/ext/mccabe/mccabe.py +++ b/tests/functional/ext/mccabe/mccabe.py @@ -214,3 +214,40 @@ def method3(self): # [too-complex] finally: pass return True + +def match_case_complexity(avg): # [too-complex] + """McCabe rating: 4 + See https://github.com/astral-sh/ruff/issues/11421 + """ + # pylint: disable=bare-name-capture-pattern + match avg: + case avg if avg < .3: + avg_grade = "F" + case avg if avg < .7: + avg_grade = "E+" + case _: + raise ValueError(f"Unexpected average: {avg}") + return avg_grade + + + +def nested_match(data): # [too-complex] + """McCabe rating: 8 + + Nested match statements.""" + match data: + case {"type": "user", "data": user_data}: + match user_data: # Nested match adds complexity + case {"name": str(name)}: + return f"User: {name}" + case {"id": int(user_id)}: + return f"User ID: {user_id}" + case _: + return "Unknown user format" + case {"type": "product", "data": product_data}: + if "price" in product_data: # +1 for if + return f"Product costs {product_data['price']}" + else: + return "Product with no price" + case _: + return "Unknown data type" diff --git a/tests/functional/ext/mccabe/mccabe.txt b/tests/functional/ext/mccabe/mccabe.txt index 3ff80e86cd..e38dfe94a2 100644 --- a/tests/functional/ext/mccabe/mccabe.txt +++ b/tests/functional/ext/mccabe/mccabe.txt @@ -13,3 +13,5 @@ too-complex:142:4:142:15:MyClass1.method2:'method2' is too complex. The McCabe r too-many-branches:142:4:142:15:MyClass1.method2:Too many branches (19/12):UNDEFINED too-complex:198:0:204:15::This 'for' is too complex. The McCabe rating is 4:HIGH too-complex:207:0:207:11:method3:'method3' is too complex. The McCabe rating is 3:HIGH +too-complex:218:0:218:25:match_case_complexity:'match_case_complexity' is too complex. The McCabe rating is 4:HIGH +too-complex:234:0:234:16:nested_match:'nested_match' is too complex. The McCabe rating is 8:HIGH From a6203c060c66b53fc39e22d283edf85b64b2dce5 Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Sun, 14 Sep 2025 18:30:48 +0200 Subject: [PATCH 2/4] Update doc/whatsnew/fragments/9667.false_negative Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- doc/whatsnew/fragments/9667.false_negative | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/whatsnew/fragments/9667.false_negative b/doc/whatsnew/fragments/9667.false_negative index 92dfa97031..3bccd0fe5e 100644 --- a/doc/whatsnew/fragments/9667.false_negative +++ b/doc/whatsnew/fragments/9667.false_negative @@ -1,3 +1,3 @@ -Match cases are now counted as an edge in the McCabe graph, and will increase the complexity accordingly. +Match cases are now counted as edges in the McCabe graph and will increase the complexity accordingly. Refs #9667 From 8ca139f417daacfdfe961007fdcb3632c8022930 Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Mon, 15 Sep 2025 15:57:18 +0200 Subject: [PATCH 3/4] Update pylint/extensions/mccabe.py Co-authored-by: Jaap Roes --- pylint/extensions/mccabe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylint/extensions/mccabe.py b/pylint/extensions/mccabe.py index f494f95ea9..343a356297 100644 --- a/pylint/extensions/mccabe.py +++ b/pylint/extensions/mccabe.py @@ -139,7 +139,7 @@ def _subgraph_parse( pathnode: _SubGraphNodes, extra_blocks: Sequence[nodes.ExceptHandler | nodes.MatchCase], ) -> None: - """Parse the body and any `else` block of `if`, `for` or `match` statements.""" + """Parse `match`/`case` blocks, or the body and `else` block of `if`/`for` statements.""" loose_ends = [] if isinstance(node, nodes.Match): for case in extra_blocks: From a43ce55a5430e5a73b837042343b3314cdcf020d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 15 Sep 2025 13:58:09 +0000 Subject: [PATCH 4/4] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- pylint/extensions/mccabe.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pylint/extensions/mccabe.py b/pylint/extensions/mccabe.py index 343a356297..3cdbbc162d 100644 --- a/pylint/extensions/mccabe.py +++ b/pylint/extensions/mccabe.py @@ -139,7 +139,9 @@ def _subgraph_parse( pathnode: _SubGraphNodes, extra_blocks: Sequence[nodes.ExceptHandler | nodes.MatchCase], ) -> None: - """Parse `match`/`case` blocks, or the body and `else` block of `if`/`for` statements.""" + """Parse `match`/`case` blocks, or the body and `else` block of `if`/`for` + statements. + """ loose_ends = [] if isinstance(node, nodes.Match): for case in extra_blocks: