diff --git a/doc/whatsnew/fragments/9667.false_negative b/doc/whatsnew/fragments/9667.false_negative new file mode 100644 index 0000000000..3bccd0fe5e --- /dev/null +++ b/doc/whatsnew/fragments/9667.false_negative @@ -0,0 +1,3 @@ +Match cases are now counted as edges 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..3cdbbc162d 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,34 @@ 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 `match`/`case` blocks, or the body and `else` block of `if`/`for` + 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