|
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=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 | + |
5 | 9 | """Module to add McCabe checker class for pylint."""
|
6 | 10 |
|
7 | 11 | from __future__ import annotations
|
8 | 12 |
|
| 13 | +import ast |
| 14 | +from ast import iter_child_nodes |
| 15 | +from collections import defaultdict |
9 | 16 | from collections.abc import Sequence
|
10 | 17 | from typing import TYPE_CHECKING, Any, TypeAlias, TypeVar
|
11 | 18 |
|
12 | 19 | from astroid import nodes
|
13 |
| -from mccabe import PathGraph as Mccabe_PathGraph |
14 |
| -from mccabe import PathGraphingAstVisitor as Mccabe_PathGraphingAstVisitor |
15 | 20 |
|
16 | 21 | from pylint import checkers
|
17 | 22 | from pylint.checkers.utils import only_required_for_messages
|
|
20 | 25 | if TYPE_CHECKING:
|
21 | 26 | from pylint.lint import PyLinter
|
22 | 27 |
|
| 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 | + |
23 | 235 | _StatementNodes: TypeAlias = (
|
24 | 236 | nodes.Assert
|
25 | 237 | | nodes.Assign
|
|
0 commit comments