3
3
# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt
4
4
5
5
# mypy: ignore-errors
6
- # pylint: disable=consider-using-f-string,inconsistent-return-statements,consider-using-generator,redefined-builtin
7
- # pylint: disable=super-with-arguments,too-many-function-args,bad-super-call
6
+ # pylint: disable=unused-argument,consider-using-generator
8
7
9
8
"""Module to add McCabe checker class for pylint.
10
9
16
15
17
16
from __future__ import annotations
18
17
19
- import ast
20
- from ast import iter_child_nodes
21
- from collections import defaultdict
22
18
from collections .abc import Sequence
23
19
from typing import TYPE_CHECKING , Any , TypeAlias , TypeVar
24
20
32
28
from pylint .lint import PyLinter
33
29
34
30
35
- class ASTVisitor :
36
- """Performs a depth-first walk of the AST."""
37
-
38
- def __init__ (self ):
39
- self .node = None
40
- self ._cache = {}
41
-
42
- def default (self , node , * args ):
43
- for child in iter_child_nodes (node ):
44
- self .dispatch (child , * args )
45
-
46
- def dispatch (self , node , * args ):
47
- self .node = node
48
- klass = node .__class__
49
- meth = self ._cache .get (klass )
50
- if meth is None :
51
- className = klass .__name__
52
- meth = getattr (self .visitor , "visit" + className , self .default )
53
- self ._cache [klass ] = meth
54
- return meth (node , * args )
55
-
56
- def preorder (self , tree , visitor , * args ):
57
- """Do preorder walk of tree using visitor."""
58
- self .visitor = visitor
59
- visitor .visit = self .dispatch
60
- self .dispatch (tree , * args ) # XXX *args make sense?
61
-
62
-
63
- class PathNode :
64
- def __init__ (self , name , look = "circle" ):
65
- self .name = name
66
- self .look = look
67
-
68
- def to_dot (self ):
69
- print ('node [shape=%s,label="%s"] %d;' % (self .look , self .name , self .dot_id ()))
70
-
71
- def dot_id (self ):
72
- return id (self )
73
-
74
-
75
- class Mccabe_PathGraph :
76
- def __init__ (self , name , entity , lineno , column = 0 ):
77
- self .name = name
78
- self .entity = entity
79
- self .lineno = lineno
80
- self .column = column
81
- self .nodes = defaultdict (list )
82
-
83
- def connect (self , n1 , n2 ):
84
- self .nodes [n1 ].append (n2 )
85
- # Ensure that the destination node is always counted.
86
- self .nodes [n2 ] = []
87
-
88
- def to_dot (self ):
89
- print ("subgraph {" )
90
- for node in self .nodes :
91
- node .to_dot ()
92
- for node , nexts in self .nodes .items ():
93
- for next in nexts :
94
- print ("%s -- %s;" % (node .dot_id (), next .dot_id ()))
95
- print ("}" )
96
-
97
- def complexity (self ):
98
- """Return the McCabe complexity for the graph.
99
-
100
- V-E+2
101
- """
102
- num_edges = sum ([len (n ) for n in self .nodes .values ()])
103
- num_nodes = len (self .nodes )
104
- return num_edges - num_nodes + 2
105
-
106
-
107
- class Mccabe_PathGraphingAstVisitor (ASTVisitor ):
108
- """A visitor for a parsed Abstract Syntax Tree which finds executable
109
- statements.
110
- """
111
-
112
- def __init__ (self ):
113
- super (Mccabe_PathGraphingAstVisitor , self ).__init__ ()
114
- self .classname = ""
115
- self .graphs = {}
116
- self .reset ()
117
-
118
- def reset (self ):
119
- self .graph = None
120
- self .tail = None
121
-
122
- def dispatch_list (self , node_list ):
123
- for node in node_list :
124
- self .dispatch (node )
125
-
126
- def visitFunctionDef (self , node ):
127
-
128
- if self .classname :
129
- entity = "%s%s" % (self .classname , node .name )
130
- else :
131
- entity = node .name
132
-
133
- name = "%d:%d: %r" % (node .lineno , node .col_offset , entity )
134
-
135
- if self .graph is not None :
136
- # closure
137
- pathnode = self .appendPathNode (name )
138
- self .tail = pathnode
139
- self .dispatch_list (node .body )
140
- bottom = PathNode ("" , look = "point" )
141
- self .graph .connect (self .tail , bottom )
142
- self .graph .connect (pathnode , bottom )
143
- self .tail = bottom
144
- else :
145
- self .graph = PathGraph (name , entity , node .lineno , node .col_offset )
146
- pathnode = PathNode (name )
147
- self .tail = pathnode
148
- self .dispatch_list (node .body )
149
- self .graphs ["%s%s" % (self .classname , node .name )] = self .graph
150
- self .reset ()
151
-
152
- visitAsyncFunctionDef = visitFunctionDef
153
-
154
- def visitClassDef (self , node ):
155
- old_classname = self .classname
156
- self .classname += node .name + "."
157
- self .dispatch_list (node .body )
158
- self .classname = old_classname
159
-
160
- def appendPathNode (self , name ):
161
- if not self .tail :
162
- return
163
- pathnode = PathNode (name )
164
- self .graph .connect (self .tail , pathnode )
165
- self .tail = pathnode
166
- return pathnode
167
-
168
- def visitSimpleStatement (self , node ):
169
- if node .lineno is None :
170
- lineno = 0
171
- else :
172
- lineno = node .lineno
173
- name = "Stmt %d" % lineno
174
- self .appendPathNode (name )
175
-
176
- def default (self , node , * args ):
177
- if isinstance (node , ast .stmt ):
178
- self .visitSimpleStatement (node )
179
- else :
180
- super (PathGraphingAstVisitor , self ).default (node , * args )
181
-
182
- def visitLoop (self , node ):
183
- name = "Loop %d" % node .lineno
184
- self ._subgraph (node , name )
185
-
186
- visitAsyncFor = visitFor = visitWhile = visitLoop
187
-
188
- def visitIf (self , node ):
189
- name = "If %d" % node .lineno
190
- self ._subgraph (node , name )
191
-
192
- def _subgraph (self , node , name , extra_blocks = ()):
193
- """Create the subgraphs representing any `if` and `for` statements."""
194
- if self .graph is None :
195
- # global loop
196
- self .graph = PathGraph (name , name , node .lineno , node .col_offset )
197
- pathnode = PathNode (name )
198
- self ._subgraph_parse (node , pathnode , extra_blocks )
199
- self .graphs ["%s%s" % (self .classname , name )] = self .graph
200
- self .reset ()
201
- else :
202
- pathnode = self .appendPathNode (name )
203
- self ._subgraph_parse (node , pathnode , extra_blocks )
204
-
205
- def _subgraph_parse (self , node , pathnode , extra_blocks ):
206
- """Parse the body and any `else` block of `if` and `for` statements."""
207
- loose_ends = []
208
- self .tail = pathnode
209
- self .dispatch_list (node .body )
210
- loose_ends .append (self .tail )
211
- for extra in extra_blocks :
212
- self .tail = pathnode
213
- self .dispatch_list (extra .body )
214
- loose_ends .append (self .tail )
215
- if node .orelse :
216
- self .tail = pathnode
217
- self .dispatch_list (node .orelse )
218
- loose_ends .append (self .tail )
219
- else :
220
- loose_ends .append (pathnode )
221
- if pathnode :
222
- bottom = PathNode ("" , look = "point" )
223
- for le in loose_ends :
224
- self .graph .connect (le , bottom )
225
- self .tail = bottom
226
-
227
- def visitTryExcept (self , node ):
228
- name = "TryExcept %d" % node .lineno
229
- self ._subgraph (node , name , extra_blocks = node .handlers )
230
-
231
- visitTry = visitTryExcept
232
-
233
- def visitWith (self , node ):
234
- name = "With %d" % node .lineno
235
- self .appendPathNode (name )
236
- self .dispatch_list (node .body )
237
-
238
- visitAsyncWith = visitWith
239
-
240
-
241
31
_StatementNodes : TypeAlias = (
242
32
nodes .Assert
243
33
| nodes .Assign
@@ -263,32 +53,68 @@ def visitWith(self, node):
263
53
)
264
54
265
55
266
- class PathGraph ( Mccabe_PathGraph ): # type: ignore[misc]
56
+ class PathGraph :
267
57
def __init__ (self , node : _SubGraphNodes | nodes .FunctionDef ):
268
- super (). __init__ ( name = "" , entity = "" , lineno = 1 )
58
+ self . name = ""
269
59
self .root = node
60
+ self .nodes = {}
270
61
62
+ def connect (self , n1 , n2 ):
63
+ if n1 not in self .nodes :
64
+ self .nodes [n1 ] = []
65
+ self .nodes [n1 ].append (n2 )
66
+ # Ensure that the destination node is always counted.
67
+ if n2 not in self .nodes :
68
+ self .nodes [n2 ] = []
69
+
70
+ def complexity (self ):
71
+ """Return the McCabe complexity for the graph.
72
+
73
+ V-E+2
74
+ """
75
+ num_edges = sum ([len (n ) for n in self .nodes .values ()])
76
+ num_nodes = len (self .nodes )
77
+ return num_edges - num_nodes + 2
78
+
79
+
80
+ class PathGraphingAstVisitor :
81
+ """A visitor for a parsed Abstract Syntax Tree which finds executable
82
+ statements.
83
+ """
271
84
272
- class PathGraphingAstVisitor (Mccabe_PathGraphingAstVisitor ): # type: ignore[misc]
273
85
def __init__ (self ) -> None :
274
- super ().__init__ ()
86
+ self .classname = ""
87
+ self .graphs = {}
88
+ self ._cache = {}
275
89
self ._bottom_counter = 0
276
90
self .graph : PathGraph | None = None
91
+ self .tail = None
92
+
93
+ def reset (self ):
94
+ self .graph = None
95
+ self .tail = None
277
96
278
97
def default (self , node : nodes .NodeNG , * args : Any ) -> None :
279
98
for child in node .get_children ():
280
99
self .dispatch (child , * args )
281
100
282
101
def dispatch (self , node : nodes .NodeNG , * args : Any ) -> Any :
283
- self .node = node
284
102
klass = node .__class__
285
103
meth = self ._cache .get (klass )
286
104
if meth is None :
287
105
class_name = klass .__name__
288
- meth = getattr (self . visitor , "visit" + class_name , self .default )
106
+ meth = getattr (self , "visit" + class_name , self .default )
289
107
self ._cache [klass ] = meth
290
108
return meth (node , * args )
291
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 )
117
+
292
118
def visitFunctionDef (self , node : nodes .FunctionDef ) -> None :
293
119
if self .graph is not None :
294
120
# closure
@@ -309,6 +135,12 @@ def visitFunctionDef(self, node: nodes.FunctionDef) -> None:
309
135
310
136
visitAsyncFunctionDef = visitFunctionDef
311
137
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
+
312
144
def visitSimpleStatement (self , node : _StatementNodes ) -> None :
313
145
self ._append_node (node )
314
146
@@ -324,6 +156,22 @@ def visitWith(self, node: nodes.With) -> None:
324
156
325
157
visitAsyncWith = visitWith
326
158
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
164
+
165
+ def visitIf (self , node : nodes .If ) -> None :
166
+ name = f"if_{ id (node )} "
167
+ self ._subgraph (node , name )
168
+
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
174
+
327
175
def visitMatch (self , node : nodes .Match ) -> None :
328
176
self ._subgraph (node , f"match_{ id (node )} " , node .cases )
329
177
0 commit comments