2
2
# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE
3
3
# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt
4
4
5
- # mypy: ignore-errors
6
- # pylint: disable=unused-argument,consider-using-generator
7
-
8
5
"""Module to add McCabe checker class for pylint.
9
6
10
7
Based on:
15
12
16
13
from __future__ import annotations
17
14
18
- from collections .abc import Sequence
19
- from typing import TYPE_CHECKING , Any , TypeAlias , TypeVar
15
+ from typing import TYPE_CHECKING , Any
20
16
21
17
from astroid import nodes
22
18
28
24
from pylint .lint import PyLinter
29
25
30
26
31
- _StatementNodes : TypeAlias = (
32
- nodes .Assert
33
- | nodes .Assign
34
- | nodes .AugAssign
35
- | nodes .Delete
36
- | nodes .Raise
37
- | nodes .Yield
38
- | nodes .Import
39
- | nodes .Call
40
- | nodes .Subscript
41
- | nodes .Pass
42
- | nodes .Continue
43
- | nodes .Break
44
- | nodes .Global
45
- | nodes .Return
46
- | nodes .Expr
47
- | nodes .Await
48
- )
49
-
50
- _SubGraphNodes : TypeAlias = nodes .If | nodes .Try | nodes .For | nodes .While | nodes .Match
51
- _AppendableNodeT = TypeVar (
52
- "_AppendableNodeT" , bound = _StatementNodes | nodes .While | nodes .FunctionDef
53
- )
54
-
55
-
56
27
class PathGraph :
57
- def __init__ (self , node : _SubGraphNodes | nodes .FunctionDef ):
58
- self .name = ""
59
- self .root = node
60
- self .nodes = {}
28
+ def __init__ (self ) -> None :
29
+ self .nodes : dict [Any , list [Any ]] = {}
61
30
62
- def connect (self , n1 , n2 ) :
31
+ def connect (self , n1 : Any , n2 : Any ) -> None :
63
32
if n1 not in self .nodes :
64
33
self .nodes [n1 ] = []
65
34
self .nodes [n1 ].append (n2 )
66
35
# Ensure that the destination node is always counted.
67
36
if n2 not in self .nodes :
68
37
self .nodes [n2 ] = []
69
38
70
- def complexity (self ):
39
+ def complexity (self ) -> int :
71
40
"""Return the McCabe complexity for the graph.
72
41
73
42
V-E+2
74
43
"""
75
- num_edges = sum ([ len (n ) for n in self .nodes .values ()] )
44
+ num_edges = sum (len (n ) for n in self .nodes .values ())
76
45
num_nodes = len (self .nodes )
77
46
return num_edges - num_nodes + 2
78
47
@@ -83,155 +52,119 @@ class PathGraphingAstVisitor:
83
52
"""
84
53
85
54
def __init__ (self ) -> None :
86
- self .classname = ""
87
- self .graphs = {}
88
- self ._cache = {}
55
+ self .graphs : dict [str , tuple [PathGraph , nodes .NodeNG ]] = {}
89
56
self ._bottom_counter = 0
90
57
self .graph : PathGraph | None = None
91
- self .tail = None
58
+ self .tail : Any = None
92
59
93
- def reset (self ) :
94
- self . graph = None
95
- self . tail = None
60
+ def dispatch (self , node : nodes . NodeNG ) -> None :
61
+ meth = getattr ( self , "visit" + node . __class__ . __name__ , self . default )
62
+ meth ( node )
96
63
97
- def default (self , node : nodes .NodeNG , * args : Any ) -> None :
64
+ def default (self , node : nodes .NodeNG ) -> None :
98
65
for child in node .get_children ():
99
- self .dispatch (child , * args )
100
-
101
- def dispatch (self , node : nodes .NodeNG , * args : Any ) -> Any :
102
- klass = node .__class__
103
- meth = self ._cache .get (klass )
104
- if meth is None :
105
- class_name = klass .__name__
106
- meth = getattr (self , "visit" + class_name , self .default )
107
- self ._cache [klass ] = meth
108
- return meth (node , * args )
109
-
110
- def preorder (self , tree , visitor ):
111
- """Do preorder walk of tree using visitor."""
112
- self .dispatch (tree )
113
-
114
- def dispatch_list (self , node_list ):
115
- for node in node_list :
116
- self .dispatch (node )
66
+ self .dispatch (child )
117
67
118
68
def visitFunctionDef (self , node : nodes .FunctionDef ) -> None :
119
69
if self .graph is not None :
120
70
# closure
121
- pathnode = self ._append_node (node )
122
- self .tail = pathnode
123
- self .dispatch_list (node .body )
71
+ self .graph .connect (self .tail , node )
72
+ self .tail = node
73
+ for child in node .body :
74
+ self .dispatch (child )
124
75
bottom = f"{ self ._bottom_counter } "
125
76
self ._bottom_counter += 1
126
77
self .graph .connect (self .tail , bottom )
127
78
self .graph .connect (node , bottom )
128
79
self .tail = bottom
129
80
else :
130
- self .graph = PathGraph (node )
81
+ self .graph = PathGraph ()
131
82
self .tail = node
132
- self .dispatch_list (node .body )
133
- self .graphs [f"{ self .classname } { node .name } " ] = self .graph
134
- self .reset ()
83
+ for child in node .body :
84
+ self .dispatch (child )
85
+ self .graphs [node .name ] = (self .graph , node )
86
+ self .graph = None
87
+ self .tail = None
135
88
136
89
visitAsyncFunctionDef = visitFunctionDef
137
90
138
- def visitClassDef (self , node : nodes .ClassDef ) -> None :
139
- old_classname = self .classname
140
- self .classname += node .name + "."
141
- self .dispatch_list (node .body )
142
- self .classname = old_classname
143
-
144
- def visitSimpleStatement (self , node : _StatementNodes ) -> None :
145
- self ._append_node (node )
91
+ def visitAssert (self , node : nodes .NodeNG ) -> None :
92
+ if self .tail and self .graph :
93
+ self .graph .connect (self .tail , node )
94
+ self .tail = node
146
95
147
- visitAssert = visitAssign = visitAugAssign = visitDelete = visitRaise = (
148
- visitYield
149
- ) = visitImport = visitCall = visitSubscript = visitPass = visitContinue = (
150
- visitBreak
151
- ) = visitGlobal = visitReturn = visitExpr = visitAwait = visitSimpleStatement
96
+ visitAssign = visitAugAssign = visitDelete = visitRaise = visitYield = (
97
+ visitImport
98
+ ) = visitCall = visitSubscript = visitPass = visitContinue = visitBreak = (
99
+ visitGlobal
100
+ ) = visitReturn = visitExpr = visitAwait = visitAssert
152
101
153
102
def visitWith (self , node : nodes .With ) -> None :
154
- self ._append_node (node )
155
- self .dispatch_list (node .body )
103
+ if self .tail and self .graph :
104
+ self .graph .connect (self .tail , node )
105
+ self .tail = node
106
+ for child in node .body :
107
+ self .dispatch (child )
156
108
157
109
visitAsyncWith = visitWith
158
110
159
- def visitLoop (self , node : nodes .For | nodes .While ) -> None :
160
- name = f"loop_{ id (node )} "
161
- self ._subgraph (node , name )
162
-
163
- visitAsyncFor = visitFor = visitWhile = visitLoop
111
+ def visitFor (self , node : nodes .For | nodes .While ) -> None :
112
+ self ._subgraph (node , node .handlers if isinstance (node , nodes .Try ) else [])
164
113
165
- def visitIf (self , node : nodes .If ) -> None :
166
- name = f"if_{ id (node )} "
167
- self ._subgraph (node , name )
114
+ visitAsyncFor = visitWhile = visitIf = visitFor
168
115
169
- def visitTryExcept (self , node : nodes .Try ) -> None :
170
- name = f"try_{ id (node )} "
171
- self ._subgraph (node , name , extra_blocks = node .handlers )
172
-
173
- visitTry = visitTryExcept
116
+ def visitTry (self , node : nodes .Try ) -> None :
117
+ self ._subgraph (node , node .handlers )
174
118
175
119
def visitMatch (self , node : nodes .Match ) -> None :
176
- self ._subgraph (node , f"match_{ id (node )} " , node .cases )
177
-
178
- def _append_node (self , node : _AppendableNodeT ) -> _AppendableNodeT | None :
179
- if not self .tail or not self .graph :
180
- return None
181
- self .graph .connect (self .tail , node )
182
- self .tail = node
183
- return node
120
+ self ._subgraph (node , node .cases )
184
121
185
122
def _subgraph (
186
- self ,
187
- node : _SubGraphNodes ,
188
- name : str ,
189
- extra_blocks : Sequence [nodes .ExceptHandler | nodes .MatchCase ] = (),
123
+ self , node : nodes .NodeNG , extra_blocks : list [nodes .NodeNG ] | None = None
190
124
) -> None :
191
- """Create the subgraphs representing any `if`, `for` or `match` statements."""
125
+ if extra_blocks is None :
126
+ extra_blocks = []
192
127
if self .graph is None :
193
- # global loop
194
- self .graph = PathGraph (node )
195
- self ._subgraph_parse (node , node , extra_blocks )
196
- self .graphs [ f" { self . classname } { name } " ] = self . graph
197
- self .reset ()
128
+ self . graph = PathGraph ()
129
+ self ._parse (node , extra_blocks )
130
+ self .graphs [ f"loop_ { id (node ) } " ] = ( self . graph , node )
131
+ self .graph = None
132
+ self .tail = None
198
133
else :
199
- self ._append_node (node )
200
- self ._subgraph_parse (node , node , extra_blocks )
201
-
202
- def _subgraph_parse (
203
- self ,
204
- node : _SubGraphNodes ,
205
- pathnode : _SubGraphNodes ,
206
- extra_blocks : Sequence [nodes .ExceptHandler | nodes .MatchCase ],
207
- ) -> None :
208
- """Parse `match`/`case` blocks, or the body and `else` block of `if`/`for`
209
- statements.
210
- """
134
+ if self .tail :
135
+ self .graph .connect (self .tail , node )
136
+ self .tail = node
137
+ self ._parse (node , extra_blocks )
138
+
139
+ def _parse (self , node : nodes .NodeNG , extra_blocks : list [nodes .NodeNG ]) -> None :
211
140
loose_ends = []
212
141
if isinstance (node , nodes .Match ):
213
142
for case in extra_blocks :
214
143
if isinstance (case , nodes .MatchCase ):
215
144
self .tail = node
216
- self .dispatch_list (case .body )
145
+ for child in case .body :
146
+ self .dispatch (child )
217
147
loose_ends .append (self .tail )
218
148
loose_ends .append (node )
219
149
else :
220
150
self .tail = node
221
- self .dispatch_list (node .body )
151
+ for child in node .body :
152
+ self .dispatch (child )
222
153
loose_ends .append (self .tail )
223
154
for extra in extra_blocks :
224
155
self .tail = node
225
- self .dispatch_list (extra .body )
156
+ for child in extra .body :
157
+ self .dispatch (child )
226
158
loose_ends .append (self .tail )
227
- if node .orelse :
159
+ if hasattr ( node , "orelse" ) and node .orelse :
228
160
self .tail = node
229
- self .dispatch_list (node .orelse )
161
+ for child in node .orelse :
162
+ self .dispatch (child )
230
163
loose_ends .append (self .tail )
231
164
else :
232
165
loose_ends .append (node )
233
166
234
- if node and self .graph :
167
+ if self .graph :
235
168
bottom = f"{ self ._bottom_counter } "
236
169
self ._bottom_counter += 1
237
170
for end in loose_ends :
@@ -267,16 +200,15 @@ class McCabeMethodChecker(checkers.BaseChecker):
267
200
)
268
201
269
202
@only_required_for_messages ("too-complex" )
270
- def visit_module (self , node : nodes .Module ) -> None :
203
+ def visit_module (self , module : nodes .Module ) -> None :
271
204
"""Visit an astroid.Module node to check too complex rating and
272
205
add message if is greater than max_complexity stored from options.
273
206
"""
274
207
visitor = PathGraphingAstVisitor ()
275
- for child in node .body :
276
- visitor .preorder (child , visitor )
277
- for graph in visitor .graphs .values ():
208
+ for child in module .body :
209
+ visitor .dispatch (child )
210
+ for graph , node in visitor .graphs .values ():
278
211
complexity = graph .complexity ()
279
- node = graph .root
280
212
if hasattr (node , "name" ):
281
213
node_name = f"'{ node .name } '"
282
214
else :
0 commit comments