Skip to content

Commit 8710bbe

Browse files
[vendor mccabe] Vendor mccabe to reduce supply chain risks and optimize analysis
1 parent f0547a2 commit 8710bbe

File tree

2 files changed

+222
-4
lines changed

2 files changed

+222
-4
lines changed

pylint/extensions/mccabe.py

Lines changed: 221 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,27 @@
22
# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE
33
# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt
44

5-
"""Module to add McCabe checker class for pylint."""
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
8+
9+
"""Module to add McCabe checker class for pylint.
10+
11+
Based on:
12+
http://nedbatchelder.com/blog/200803/python_code_complexity_microtool.html
13+
Later integrated in pycqa/mccabe under the MIT License then vendored in pylint
14+
under the GPL License.
15+
"""
616

717
from __future__ import annotations
818

19+
import ast
20+
from ast import iter_child_nodes
21+
from collections import defaultdict
922
from collections.abc import Sequence
1023
from typing import TYPE_CHECKING, Any, TypeAlias, TypeVar
1124

1225
from astroid import nodes
13-
from mccabe import PathGraph as Mccabe_PathGraph
14-
from mccabe import PathGraphingAstVisitor as Mccabe_PathGraphingAstVisitor
1526

1627
from pylint import checkers
1728
from pylint.checkers.utils import only_required_for_messages
@@ -20,6 +31,213 @@
2031
if TYPE_CHECKING:
2132
from pylint.lint import PyLinter
2233

34+
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+
23241
_StatementNodes: TypeAlias = (
24242
nodes.Assert
25243
| nodes.Assign

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ dependencies = [
4545
"dill>=0.3.6; python_version>='3.11'",
4646
"dill>=0.3.7; python_version>='3.12'",
4747
"isort>=4.2.5,!=5.13,<7",
48-
"mccabe>=0.6,<0.8",
4948
"platformdirs>=2.2",
5049
"tomli>=1.1; python_version<'3.11'",
5150
"tomlkit>=0.10.1",
@@ -178,6 +177,7 @@ lint.ignore = [
178177
"RUF012", # mutable default values in class attributes
179178
"UP038", # Use `X | Y` in `isinstance` call instead of `(X, Y)`
180179
]
180+
lint.per-file-ignores."pylint/extensions/mccabe.py" = [ "UP008", "UP031" ]
181181
lint.pydocstyle.convention = "pep257"
182182

183183
[tool.isort]

0 commit comments

Comments
 (0)