Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions doc/whatsnew/fragments/9667.false_negative
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Match cases are now counted as an edge in the McCabe graph, and will increase the complexity accordingly.

Refs #9667
44 changes: 28 additions & 16 deletions pylint/extensions/mccabe.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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
Expand Down
37 changes: 37 additions & 0 deletions tests/functional/ext/mccabe/mccabe.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
2 changes: 2 additions & 0 deletions tests/functional/ext/mccabe/mccabe.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading