Skip to content

Commit 37cdacf

Browse files
[vendor mccabe] Vendor mccabe to reduce supply chain risks and optimize analysis
1 parent c708b6a commit 37cdacf

File tree

2 files changed

+215
-3
lines changed

2 files changed

+215
-3
lines changed

pylint/extensions/mccabe.py

Lines changed: 214 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,21 @@
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+
# 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+
59
"""Module to add McCabe checker class for pylint."""
610

711
from __future__ import annotations
812

13+
import ast
14+
from ast import iter_child_nodes
15+
from collections import defaultdict
916
from collections.abc import Sequence
1017
from typing import TYPE_CHECKING, Any, TypeAlias, TypeVar
1118

1219
from astroid import nodes
13-
from mccabe import PathGraph as Mccabe_PathGraph
14-
from mccabe import PathGraphingAstVisitor as Mccabe_PathGraphingAstVisitor
1520

1621
from pylint import checkers
1722
from pylint.checkers.utils import only_required_for_messages
@@ -20,6 +25,213 @@
2025
if TYPE_CHECKING:
2126
from pylint.lint import PyLinter
2227

28+
29+
class ASTVisitor:
30+
"""Performs a depth-first walk of the AST."""
31+
32+
def __init__(self):
33+
self.node = None
34+
self._cache = {}
35+
36+
def default(self, node, *args):
37+
for child in iter_child_nodes(node):
38+
self.dispatch(child, *args)
39+
40+
def dispatch(self, node, *args):
41+
self.node = node
42+
klass = node.__class__
43+
meth = self._cache.get(klass)
44+
if meth is None:
45+
className = klass.__name__
46+
meth = getattr(self.visitor, "visit" + className, self.default)
47+
self._cache[klass] = meth
48+
return meth(node, *args)
49+
50+
def preorder(self, tree, visitor, *args):
51+
"""Do preorder walk of tree using visitor."""
52+
self.visitor = visitor
53+
visitor.visit = self.dispatch
54+
self.dispatch(tree, *args) # XXX *args make sense?
55+
56+
57+
class PathNode:
58+
def __init__(self, name, look="circle"):
59+
self.name = name
60+
self.look = look
61+
62+
def to_dot(self):
63+
print('node [shape=%s,label="%s"] %d;' % (self.look, self.name, self.dot_id()))
64+
65+
def dot_id(self):
66+
return id(self)
67+
68+
69+
class Mccabe_PathGraph:
70+
def __init__(self, name, entity, lineno, column=0):
71+
self.name = name
72+
self.entity = entity
73+
self.lineno = lineno
74+
self.column = column
75+
self.nodes = defaultdict(list)
76+
77+
def connect(self, n1, n2):
78+
self.nodes[n1].append(n2)
79+
# Ensure that the destination node is always counted.
80+
self.nodes[n2] = []
81+
82+
def to_dot(self):
83+
print("subgraph {")
84+
for node in self.nodes:
85+
node.to_dot()
86+
for node, nexts in self.nodes.items():
87+
for next in nexts:
88+
print("%s -- %s;" % (node.dot_id(), next.dot_id()))
89+
print("}")
90+
91+
def complexity(self):
92+
"""Return the McCabe complexity for the graph.
93+
94+
V-E+2
95+
"""
96+
num_edges = sum([len(n) for n in self.nodes.values()])
97+
num_nodes = len(self.nodes)
98+
return num_edges - num_nodes + 2
99+
100+
101+
class Mccabe_PathGraphingAstVisitor(ASTVisitor):
102+
"""A visitor for a parsed Abstract Syntax Tree which finds executable
103+
statements.
104+
"""
105+
106+
def __init__(self):
107+
super(Mccabe_PathGraphingAstVisitor, self).__init__()
108+
self.classname = ""
109+
self.graphs = {}
110+
self.reset()
111+
112+
def reset(self):
113+
self.graph = None
114+
self.tail = None
115+
116+
def dispatch_list(self, node_list):
117+
for node in node_list:
118+
self.dispatch(node)
119+
120+
def visitFunctionDef(self, node):
121+
122+
if self.classname:
123+
entity = "%s%s" % (self.classname, node.name)
124+
else:
125+
entity = node.name
126+
127+
name = "%d:%d: %r" % (node.lineno, node.col_offset, entity)
128+
129+
if self.graph is not None:
130+
# closure
131+
pathnode = self.appendPathNode(name)
132+
self.tail = pathnode
133+
self.dispatch_list(node.body)
134+
bottom = PathNode("", look="point")
135+
self.graph.connect(self.tail, bottom)
136+
self.graph.connect(pathnode, bottom)
137+
self.tail = bottom
138+
else:
139+
self.graph = PathGraph(name, entity, node.lineno, node.col_offset)
140+
pathnode = PathNode(name)
141+
self.tail = pathnode
142+
self.dispatch_list(node.body)
143+
self.graphs["%s%s" % (self.classname, node.name)] = self.graph
144+
self.reset()
145+
146+
visitAsyncFunctionDef = visitFunctionDef
147+
148+
def visitClassDef(self, node):
149+
old_classname = self.classname
150+
self.classname += node.name + "."
151+
self.dispatch_list(node.body)
152+
self.classname = old_classname
153+
154+
def appendPathNode(self, name):
155+
if not self.tail:
156+
return
157+
pathnode = PathNode(name)
158+
self.graph.connect(self.tail, pathnode)
159+
self.tail = pathnode
160+
return pathnode
161+
162+
def visitSimpleStatement(self, node):
163+
if node.lineno is None:
164+
lineno = 0
165+
else:
166+
lineno = node.lineno
167+
name = "Stmt %d" % lineno
168+
self.appendPathNode(name)
169+
170+
def default(self, node, *args):
171+
if isinstance(node, ast.stmt):
172+
self.visitSimpleStatement(node)
173+
else:
174+
super(PathGraphingAstVisitor, self).default(node, *args)
175+
176+
def visitLoop(self, node):
177+
name = "Loop %d" % node.lineno
178+
self._subgraph(node, name)
179+
180+
visitAsyncFor = visitFor = visitWhile = visitLoop
181+
182+
def visitIf(self, node):
183+
name = "If %d" % node.lineno
184+
self._subgraph(node, name)
185+
186+
def _subgraph(self, node, name, extra_blocks=()):
187+
"""Create the subgraphs representing any `if` and `for` statements."""
188+
if self.graph is None:
189+
# global loop
190+
self.graph = PathGraph(name, name, node.lineno, node.col_offset)
191+
pathnode = PathNode(name)
192+
self._subgraph_parse(node, pathnode, extra_blocks)
193+
self.graphs["%s%s" % (self.classname, name)] = self.graph
194+
self.reset()
195+
else:
196+
pathnode = self.appendPathNode(name)
197+
self._subgraph_parse(node, pathnode, extra_blocks)
198+
199+
def _subgraph_parse(self, node, pathnode, extra_blocks):
200+
"""Parse the body and any `else` block of `if` and `for` statements."""
201+
loose_ends = []
202+
self.tail = pathnode
203+
self.dispatch_list(node.body)
204+
loose_ends.append(self.tail)
205+
for extra in extra_blocks:
206+
self.tail = pathnode
207+
self.dispatch_list(extra.body)
208+
loose_ends.append(self.tail)
209+
if node.orelse:
210+
self.tail = pathnode
211+
self.dispatch_list(node.orelse)
212+
loose_ends.append(self.tail)
213+
else:
214+
loose_ends.append(pathnode)
215+
if pathnode:
216+
bottom = PathNode("", look="point")
217+
for le in loose_ends:
218+
self.graph.connect(le, bottom)
219+
self.tail = bottom
220+
221+
def visitTryExcept(self, node):
222+
name = "TryExcept %d" % node.lineno
223+
self._subgraph(node, name, extra_blocks=node.handlers)
224+
225+
visitTry = visitTryExcept
226+
227+
def visitWith(self, node):
228+
name = "With %d" % node.lineno
229+
self.appendPathNode(name)
230+
self.dispatch_list(node.body)
231+
232+
visitAsyncWith = visitWith
233+
234+
23235
_StatementNodes: TypeAlias = (
24236
nodes.Assert
25237
| 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)