diff --git a/doc/whatsnew/fragments/10551.internal b/doc/whatsnew/fragments/10551.internal new file mode 100644 index 0000000000..1c78cdbdac --- /dev/null +++ b/doc/whatsnew/fragments/10551.internal @@ -0,0 +1,4 @@ +The dependency to mccabe was removed and its content is now vendored in pylint. Optimization were done as a result +because mccabe was a code to dot graph generator and pylint only need to calculate the mccabe score not draw graphics. + +Refs #10551 diff --git a/pylint/extensions/mccabe.py b/pylint/extensions/mccabe.py index 3cdbbc162d..fb3a009022 100644 --- a/pylint/extensions/mccabe.py +++ b/pylint/extensions/mccabe.py @@ -2,16 +2,19 @@ # For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE # Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt -"""Module to add McCabe checker class for pylint.""" +"""Module to add McCabe checker class for pylint. + +Based on: +http://nedbatchelder.com/blog/200803/python_code_complexity_microtool.html +Later integrated in pycqa/mccabe under the MIT License then vendored in pylint +under the GPL License. +""" from __future__ import annotations -from collections.abc import Sequence -from typing import TYPE_CHECKING, Any, TypeAlias, TypeVar +from typing import TYPE_CHECKING, Any, TypeAlias from astroid import nodes -from mccabe import PathGraph as Mccabe_PathGraph -from mccabe import PathGraphingAstVisitor as Mccabe_PathGraphingAstVisitor from pylint import checkers from pylint.checkers.utils import only_required_for_messages @@ -20,7 +23,7 @@ if TYPE_CHECKING: from pylint.lint import PyLinter -_StatementNodes: TypeAlias = ( +SimpleNode: TypeAlias = ( nodes.Assert | nodes.Assign | nodes.AugAssign @@ -39,133 +42,166 @@ | nodes.Await ) -_SubGraphNodes: TypeAlias = nodes.If | nodes.Try | nodes.For | nodes.While | nodes.Match -_AppendableNodeT = TypeVar( - "_AppendableNodeT", bound=_StatementNodes | nodes.While | nodes.FunctionDef -) +class PathGraph: + def __init__(self) -> None: + self.nodes: dict[Any, list[Any]] = {} + + def connect(self, n1: Any, n2: Any) -> None: + if n1 not in self.nodes: + self.nodes[n1] = [] + self.nodes[n1].append(n2) + # Ensure that the destination node is always counted. + if n2 not in self.nodes: + self.nodes[n2] = [] + + def complexity(self) -> int: + """Return the McCabe complexity for the graph. + + V-E+2 + """ + num_edges = sum(len(n) for n in self.nodes.values()) + num_nodes = len(self.nodes) + return num_edges - num_nodes + 2 -class PathGraph(Mccabe_PathGraph): # type: ignore[misc] - def __init__(self, node: _SubGraphNodes | nodes.FunctionDef): - super().__init__(name="", entity="", lineno=1) - self.root = node +class PathGraphingAstVisitor: + """A visitor for a parsed Abstract Syntax Tree which finds executable + statements. + """ -class PathGraphingAstVisitor(Mccabe_PathGraphingAstVisitor): # type: ignore[misc] def __init__(self) -> None: - super().__init__() + self.graphs: dict[str, tuple[PathGraph, nodes.NodeNG]] = {} self._bottom_counter = 0 self.graph: PathGraph | None = None - - def default(self, node: nodes.NodeNG, *args: Any) -> None: + self.tail: Any = None + + def dispatch(self, node: nodes.NodeNG) -> None: + { + "FunctionDef": self.visitFunctionDef, + "AsyncFunctionDef": self.visitFunctionDef, + "With": self.visitWith, + "AsyncWith": self.visitWith, + "For": self.visitFor, + "AsyncFor": self.visitFor, + "While": self.visitFor, + "If": self.visitFor, + "Try": self.visitTry, + "Match": self.visitMatch, + "Assert": self.visitSimpleNode, + "Assign": self.visitSimpleNode, + "AugAssign": self.visitSimpleNode, + "Delete": self.visitSimpleNode, + "Raise": self.visitSimpleNode, + "Yield": self.visitSimpleNode, + "Import": self.visitSimpleNode, + "Call": self.visitSimpleNode, + "Subscript": self.visitSimpleNode, + "Pass": self.visitSimpleNode, + "Continue": self.visitSimpleNode, + "Break": self.visitSimpleNode, + "Global": self.visitSimpleNode, + "Return": self.visitSimpleNode, + "Expr": self.visitSimpleNode, + "Await": self.visitSimpleNode, + }.get(node.__class__.__name__, self.default)(node) + + def default(self, node: nodes.NodeNG) -> None: for child in node.get_children(): - self.dispatch(child, *args) - - def dispatch(self, node: nodes.NodeNG, *args: Any) -> Any: - self.node = node - klass = node.__class__ - meth = self._cache.get(klass) - if meth is None: - class_name = klass.__name__ - meth = getattr(self.visitor, "visit" + class_name, self.default) - self._cache[klass] = meth - return meth(node, *args) - - def visitFunctionDef(self, node: nodes.FunctionDef) -> None: + self.dispatch(child) + + def visitFunctionDef( + self, node: nodes.FunctionDef | nodes.AsyncFunctionDef + ) -> None: if self.graph is not None: # closure - pathnode = self._append_node(node) - self.tail = pathnode - self.dispatch_list(node.body) + self.graph.connect(self.tail, node) + self.tail = node + for child in node.body: + self.dispatch(child) bottom = f"{self._bottom_counter}" self._bottom_counter += 1 self.graph.connect(self.tail, bottom) self.graph.connect(node, bottom) self.tail = bottom else: - self.graph = PathGraph(node) + self.graph = PathGraph() + self.tail = node + for child in node.body: + self.dispatch(child) + self.graphs[node.name] = (self.graph, node) + self.graph = None + self.tail = None + + def visitSimpleNode(self, node: SimpleNode) -> None: + if self.tail and self.graph: + self.graph.connect(self.tail, node) self.tail = node - self.dispatch_list(node.body) - self.graphs[f"{self.classname}{node.name}"] = self.graph - self.reset() - - visitAsyncFunctionDef = visitFunctionDef - - def visitSimpleStatement(self, node: _StatementNodes) -> None: - self._append_node(node) - visitAssert = visitAssign = visitAugAssign = visitDelete = visitRaise = ( - visitYield - ) = visitImport = visitCall = visitSubscript = visitPass = visitContinue = ( - visitBreak - ) = visitGlobal = visitReturn = visitExpr = visitAwait = visitSimpleStatement + def visitWith(self, node: nodes.With | nodes.AsyncWith) -> None: + if self.tail and self.graph: + self.graph.connect(self.tail, node) + self.tail = node + for child in node.body: + self.dispatch(child) - def visitWith(self, node: nodes.With) -> None: - self._append_node(node) - self.dispatch_list(node.body) + def visitFor( + self, node: nodes.For | nodes.AsyncFor | nodes.While | nodes.If + ) -> None: + self._subgraph(node, node.handlers if isinstance(node, nodes.Try) else []) - visitAsyncWith = visitWith + def visitTry(self, node: nodes.Try) -> None: + self._subgraph(node, node.handlers) def visitMatch(self, node: nodes.Match) -> None: - self._subgraph(node, f"match_{id(node)}", node.cases) - - def _append_node(self, node: _AppendableNodeT) -> _AppendableNodeT | None: - if not self.tail or not self.graph: - return None - self.graph.connect(self.tail, node) - self.tail = node - return node + self._subgraph(node, node.cases) def _subgraph( - self, - node: _SubGraphNodes, - name: str, - extra_blocks: Sequence[nodes.ExceptHandler | nodes.MatchCase] = (), + self, node: nodes.NodeNG, extra_blocks: list[nodes.NodeNG] | None = None ) -> None: - """Create the subgraphs representing any `if`, `for` or `match` statements.""" + if extra_blocks is None: + extra_blocks = [] if self.graph is None: - # global loop - self.graph = PathGraph(node) - self._subgraph_parse(node, node, extra_blocks) - self.graphs[f"{self.classname}{name}"] = self.graph - self.reset() + self.graph = PathGraph() + self._parse(node, extra_blocks) + self.graphs[f"loop_{id(node)}"] = (self.graph, node) + self.graph = None + self.tail = None else: - self._append_node(node) - self._subgraph_parse(node, node, extra_blocks) - - def _subgraph_parse( - self, - node: _SubGraphNodes, - pathnode: _SubGraphNodes, - extra_blocks: Sequence[nodes.ExceptHandler | nodes.MatchCase], - ) -> None: - """Parse `match`/`case` blocks, or the body and `else` block of `if`/`for` - statements. - """ + if self.tail: + self.graph.connect(self.tail, node) + self.tail = node + self._parse(node, extra_blocks) + + def _parse(self, node: nodes.NodeNG, extra_blocks: list[nodes.NodeNG]) -> None: loose_ends = [] if isinstance(node, nodes.Match): for case in extra_blocks: if isinstance(case, nodes.MatchCase): self.tail = node - self.dispatch_list(case.body) + for child in case.body: + self.dispatch(child) loose_ends.append(self.tail) loose_ends.append(node) else: self.tail = node - self.dispatch_list(node.body) + for child in node.body: + self.dispatch(child) loose_ends.append(self.tail) for extra in extra_blocks: self.tail = node - self.dispatch_list(extra.body) + for child in extra.body: + self.dispatch(child) loose_ends.append(self.tail) - if node.orelse: + if hasattr(node, "orelse") and node.orelse: self.tail = node - self.dispatch_list(node.orelse) + for child in node.orelse: + self.dispatch(child) loose_ends.append(self.tail) else: loose_ends.append(node) - if node and self.graph: + if self.graph: bottom = f"{self._bottom_counter}" self._bottom_counter += 1 for end in loose_ends: @@ -201,16 +237,15 @@ class McCabeMethodChecker(checkers.BaseChecker): ) @only_required_for_messages("too-complex") - def visit_module(self, node: nodes.Module) -> None: + def visit_module(self, module: nodes.Module) -> None: """Visit an astroid.Module node to check too complex rating and add message if is greater than max_complexity stored from options. """ visitor = PathGraphingAstVisitor() - for child in node.body: - visitor.preorder(child, visitor) - for graph in visitor.graphs.values(): + for child in module.body: + visitor.dispatch(child) + for graph, node in visitor.graphs.values(): complexity = graph.complexity() - node = graph.root if hasattr(node, "name"): node_name = f"'{node.name}'" else: diff --git a/pyproject.toml b/pyproject.toml index e7215271d8..4510f7b4df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,6 @@ dependencies = [ "dill>=0.3.6; python_version>='3.11'", "dill>=0.3.7; python_version>='3.12'", "isort>=4.2.5,!=5.13,<7", - "mccabe>=0.6,<0.8", "platformdirs>=2.2", "tomli>=1.1; python_version<'3.11'", "tomlkit>=0.10.1",