Skip to content

Commit f0547a2

Browse files
Pierre-Sassoulascdce8pjaap3
authored
[mccabe - too-complex] Add each match case as an edge in the graph (#9667)
We decided against an option to permit to not count them. Refs #9667 Co-authored-by: Marc Mueller <[email protected]> Co-authored-by: Jaap Roes <[email protected]>
1 parent 3d2ffd6 commit f0547a2

File tree

4 files changed

+72
-16
lines changed

4 files changed

+72
-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 edges in the McCabe graph and will increase the complexity accordingly.
2+
3+
Refs #9667

pylint/extensions/mccabe.py

Lines changed: 30 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,34 @@ 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 `match`/`case` blocks, or the body and `else` block of `if`/`for`
143+
statements.
144+
"""
140145
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:
146+
if isinstance(node, nodes.Match):
147+
for case in extra_blocks:
148+
if isinstance(case, nodes.MatchCase):
149+
self.tail = node
150+
self.dispatch_list(case.body)
151+
loose_ends.append(self.tail)
152+
loose_ends.append(node)
153+
else:
149154
self.tail = node
150-
self.dispatch_list(node.orelse)
155+
self.dispatch_list(node.body)
151156
loose_ends.append(self.tail)
152-
else:
153-
loose_ends.append(node)
157+
for extra in extra_blocks:
158+
self.tail = node
159+
self.dispatch_list(extra.body)
160+
loose_ends.append(self.tail)
161+
if node.orelse:
162+
self.tail = node
163+
self.dispatch_list(node.orelse)
164+
loose_ends.append(self.tail)
165+
else:
166+
loose_ends.append(node)
167+
154168
if node and self.graph:
155169
bottom = f"{self._bottom_counter}"
156170
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)