33# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt
44
55# 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
87
98"""Module to add McCabe checker class for pylint.
109
1615
1716from __future__ import annotations
1817
19- import ast
20- from ast import iter_child_nodes
21- from collections import defaultdict
2218from collections .abc import Sequence
2319from typing import TYPE_CHECKING , Any , TypeAlias , TypeVar
2420
3228 from pylint .lint import PyLinter
3329
3430
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-
24131_StatementNodes : TypeAlias = (
24232 nodes .Assert
24333 | nodes .Assign
@@ -263,32 +53,68 @@ def visitWith(self, node):
26353)
26454
26555
266- class PathGraph ( Mccabe_PathGraph ): # type: ignore[misc]
56+ class PathGraph :
26757 def __init__ (self , node : _SubGraphNodes | nodes .FunctionDef ):
268- super (). __init__ ( name = "" , entity = "" , lineno = 1 )
58+ self . name = ""
26959 self .root = node
60+ self .nodes = {}
27061
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+ """
27184
272- class PathGraphingAstVisitor (Mccabe_PathGraphingAstVisitor ): # type: ignore[misc]
27385 def __init__ (self ) -> None :
274- super ().__init__ ()
86+ self .classname = ""
87+ self .graphs = {}
88+ self ._cache = {}
27589 self ._bottom_counter = 0
27690 self .graph : PathGraph | None = None
91+ self .tail = None
92+
93+ def reset (self ):
94+ self .graph = None
95+ self .tail = None
27796
27897 def default (self , node : nodes .NodeNG , * args : Any ) -> None :
27998 for child in node .get_children ():
28099 self .dispatch (child , * args )
281100
282101 def dispatch (self , node : nodes .NodeNG , * args : Any ) -> Any :
283- self .node = node
284102 klass = node .__class__
285103 meth = self ._cache .get (klass )
286104 if meth is None :
287105 class_name = klass .__name__
288- meth = getattr (self . visitor , "visit" + class_name , self .default )
106+ meth = getattr (self , "visit" + class_name , self .default )
289107 self ._cache [klass ] = meth
290108 return meth (node , * args )
291109
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+
292118 def visitFunctionDef (self , node : nodes .FunctionDef ) -> None :
293119 if self .graph is not None :
294120 # closure
@@ -309,6 +135,12 @@ def visitFunctionDef(self, node: nodes.FunctionDef) -> None:
309135
310136 visitAsyncFunctionDef = visitFunctionDef
311137
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+
312144 def visitSimpleStatement (self , node : _StatementNodes ) -> None :
313145 self ._append_node (node )
314146
@@ -324,6 +156,22 @@ def visitWith(self, node: nodes.With) -> None:
324156
325157 visitAsyncWith = visitWith
326158
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+
327175 def visitMatch (self , node : nodes .Match ) -> None :
328176 self ._subgraph (node , f"match_{ id (node )} " , node .cases )
329177
0 commit comments