Skip to content

Commit 7ddbd4b

Browse files
[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
1 parent c708b6a commit 7ddbd4b

File tree

4 files changed

+70
-16
lines changed

4 files changed

+70
-16
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Match cases are now counted as an edge in the McCabe graph, and will increase the complexity accordingly.
2+
3+
Refs #9667

pylint/extensions/mccabe.py

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
| nodes.Await
4040
)
4141

42-
_SubGraphNodes: TypeAlias = nodes.If | nodes.Try | nodes.For | nodes.While
42+
_SubGraphNodes: TypeAlias = nodes.If | nodes.Try | nodes.For | nodes.While | nodes.Match
4343
_AppendableNodeT = TypeVar(
4444
"_AppendableNodeT", bound=_StatementNodes | nodes.While | nodes.FunctionDef
4545
)
@@ -106,6 +106,9 @@ def visitWith(self, node: nodes.With) -> None:
106106

107107
visitAsyncWith = visitWith
108108

109+
def visitMatch(self, node: nodes.Match) -> None:
110+
self._subgraph(node, f"match_{id(node)}", node.cases)
111+
109112
def _append_node(self, node: _AppendableNodeT) -> _AppendableNodeT | None:
110113
if not self.tail or not self.graph:
111114
return None
@@ -117,9 +120,9 @@ def _subgraph(
117120
self,
118121
node: _SubGraphNodes,
119122
name: str,
120-
extra_blocks: Sequence[nodes.ExceptHandler] = (),
123+
extra_blocks: Sequence[nodes.ExceptHandler | nodes.MatchCase] = (),
121124
) -> None:
122-
"""Create the subgraphs representing any `if` and `for` statements."""
125+
"""Create the subgraphs representing any `if`, `for` or `match` statements."""
123126
if self.graph is None:
124127
# global loop
125128
self.graph = PathGraph(node)
@@ -134,23 +137,32 @@ def _subgraph_parse(
134137
self,
135138
node: _SubGraphNodes,
136139
pathnode: _SubGraphNodes,
137-
extra_blocks: Sequence[nodes.ExceptHandler],
140+
extra_blocks: Sequence[nodes.ExceptHandler | nodes.MatchCase],
138141
) -> None:
139-
"""Parse the body and any `else` block of `if` and `for` statements."""
142+
"""Parse the body and any `else` block of `if`, `for` or `match` statements."""
140143
loose_ends = []
141-
self.tail = node
142-
self.dispatch_list(node.body)
143-
loose_ends.append(self.tail)
144-
for extra in extra_blocks:
145-
self.tail = node
146-
self.dispatch_list(extra.body)
147-
loose_ends.append(self.tail)
148-
if node.orelse:
144+
if isinstance(node, nodes.Match):
145+
for case in extra_blocks:
146+
if isinstance(case, nodes.MatchCase):
147+
self.tail = node
148+
self.dispatch_list(case.body)
149+
loose_ends.append(self.tail)
150+
loose_ends.append(node)
151+
else:
149152
self.tail = node
150-
self.dispatch_list(node.orelse)
153+
self.dispatch_list(node.body)
151154
loose_ends.append(self.tail)
152-
else:
153-
loose_ends.append(node)
155+
for extra in extra_blocks:
156+
self.tail = node
157+
self.dispatch_list(extra.body)
158+
loose_ends.append(self.tail)
159+
if node.orelse:
160+
self.tail = node
161+
self.dispatch_list(node.orelse)
162+
loose_ends.append(self.tail)
163+
else:
164+
loose_ends.append(node)
165+
154166
if node and self.graph:
155167
bottom = f"{self._bottom_counter}"
156168
self._bottom_counter += 1

tests/functional/ext/mccabe/mccabe.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,3 +214,40 @@ def method3(self): # [too-complex]
214214
finally:
215215
pass
216216
return True
217+
218+
def match_case_complexity(avg): # [too-complex]
219+
"""McCabe rating: 4
220+
See https://github.com/astral-sh/ruff/issues/11421
221+
"""
222+
# pylint: disable=bare-name-capture-pattern
223+
match avg:
224+
case avg if avg < .3:
225+
avg_grade = "F"
226+
case avg if avg < .7:
227+
avg_grade = "E+"
228+
case _:
229+
raise ValueError(f"Unexpected average: {avg}")
230+
return avg_grade
231+
232+
233+
234+
def nested_match(data): # [too-complex]
235+
"""McCabe rating: 8
236+
237+
Nested match statements."""
238+
match data:
239+
case {"type": "user", "data": user_data}:
240+
match user_data: # Nested match adds complexity
241+
case {"name": str(name)}:
242+
return f"User: {name}"
243+
case {"id": int(user_id)}:
244+
return f"User ID: {user_id}"
245+
case _:
246+
return "Unknown user format"
247+
case {"type": "product", "data": product_data}:
248+
if "price" in product_data: # +1 for if
249+
return f"Product costs {product_data['price']}"
250+
else:
251+
return "Product with no price"
252+
case _:
253+
return "Unknown data type"

tests/functional/ext/mccabe/mccabe.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,5 @@ too-complex:142:4:142:15:MyClass1.method2:'method2' is too complex. The McCabe r
1313
too-many-branches:142:4:142:15:MyClass1.method2:Too many branches (19/12):UNDEFINED
1414
too-complex:198:0:204:15::This 'for' is too complex. The McCabe rating is 4:HIGH
1515
too-complex:207:0:207:11:method3:'method3' is too complex. The McCabe rating is 3:HIGH
16+
too-complex:218:0:218:25:match_case_complexity:'match_case_complexity' is too complex. The McCabe rating is 4:HIGH
17+
too-complex:234:0:234:16:nested_match:'nested_match' is too complex. The McCabe rating is 8:HIGH

0 commit comments

Comments
 (0)