Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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 edges in the McCabe graph and will increase the complexity accordingly.

Refs #9667
46 changes: 30 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,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
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