mccabe.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. # Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
  2. # For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE
  3. # Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt
  4. """Module to add McCabe checker class for pylint."""
  5. from __future__ import annotations
  6. from collections.abc import Sequence
  7. from typing import TYPE_CHECKING, Any, TypeAlias, TypeVar
  8. from astroid import nodes
  9. from mccabe import PathGraph as Mccabe_PathGraph
  10. from mccabe import PathGraphingAstVisitor as Mccabe_PathGraphingAstVisitor
  11. from pylint import checkers
  12. from pylint.checkers.utils import only_required_for_messages
  13. from pylint.interfaces import HIGH
  14. if TYPE_CHECKING:
  15. from pylint.lint import PyLinter
  16. _StatementNodes: TypeAlias = (
  17. nodes.Assert
  18. | nodes.Assign
  19. | nodes.AugAssign
  20. | nodes.Delete
  21. | nodes.Raise
  22. | nodes.Yield
  23. | nodes.Import
  24. | nodes.Call
  25. | nodes.Subscript
  26. | nodes.Pass
  27. | nodes.Continue
  28. | nodes.Break
  29. | nodes.Global
  30. | nodes.Return
  31. | nodes.Expr
  32. | nodes.Await
  33. )
  34. _SubGraphNodes: TypeAlias = nodes.If | nodes.Try | nodes.For | nodes.While | nodes.Match
  35. _AppendableNodeT = TypeVar(
  36. "_AppendableNodeT", bound=_StatementNodes | nodes.While | nodes.FunctionDef
  37. )
  38. class PathGraph(Mccabe_PathGraph): # type: ignore[misc]
  39. def __init__(self, node: _SubGraphNodes | nodes.FunctionDef):
  40. super().__init__(name="", entity="", lineno=1)
  41. self.root = node
  42. class PathGraphingAstVisitor(Mccabe_PathGraphingAstVisitor): # type: ignore[misc]
  43. def __init__(self) -> None:
  44. super().__init__()
  45. self._bottom_counter = 0
  46. self.graph: PathGraph | None = None
  47. def default(self, node: nodes.NodeNG, *args: Any) -> None:
  48. for child in node.get_children():
  49. self.dispatch(child, *args)
  50. def dispatch(self, node: nodes.NodeNG, *args: Any) -> Any:
  51. self.node = node
  52. klass = node.__class__
  53. meth = self._cache.get(klass)
  54. if meth is None:
  55. class_name = klass.__name__
  56. meth = getattr(self.visitor, "visit" + class_name, self.default)
  57. self._cache[klass] = meth
  58. return meth(node, *args)
  59. def visitFunctionDef(self, node: nodes.FunctionDef) -> None:
  60. if self.graph is not None:
  61. # closure
  62. pathnode = self._append_node(node)
  63. self.tail = pathnode
  64. self.dispatch_list(node.body)
  65. bottom = f"{self._bottom_counter}"
  66. self._bottom_counter += 1
  67. self.graph.connect(self.tail, bottom)
  68. self.graph.connect(node, bottom)
  69. self.tail = bottom
  70. else:
  71. self.graph = PathGraph(node)
  72. self.tail = node
  73. self.dispatch_list(node.body)
  74. self.graphs[f"{self.classname}{node.name}"] = self.graph
  75. self.reset()
  76. visitAsyncFunctionDef = visitFunctionDef
  77. def visitSimpleStatement(self, node: _StatementNodes) -> None:
  78. self._append_node(node)
  79. visitAssert = visitAssign = visitAugAssign = visitDelete = visitRaise = (
  80. visitYield
  81. ) = visitImport = visitCall = visitSubscript = visitPass = visitContinue = (
  82. visitBreak
  83. ) = visitGlobal = visitReturn = visitExpr = visitAwait = visitSimpleStatement
  84. def visitWith(self, node: nodes.With) -> None:
  85. self._append_node(node)
  86. self.dispatch_list(node.body)
  87. visitAsyncWith = visitWith
  88. def visitMatch(self, node: nodes.Match) -> None:
  89. self._subgraph(node, f"match_{id(node)}", node.cases)
  90. def _append_node(self, node: _AppendableNodeT) -> _AppendableNodeT | None:
  91. if not (self.tail and self.graph):
  92. return None
  93. self.graph.connect(self.tail, node)
  94. self.tail = node
  95. return node
  96. def _subgraph(
  97. self,
  98. node: _SubGraphNodes,
  99. name: str,
  100. extra_blocks: Sequence[nodes.ExceptHandler | nodes.MatchCase] = (),
  101. ) -> None:
  102. """Create the subgraphs representing any `if`, `for` or `match` statements."""
  103. if self.graph is None:
  104. # global loop
  105. self.graph = PathGraph(node)
  106. self._subgraph_parse(node, node, extra_blocks)
  107. self.graphs[f"{self.classname}{name}"] = self.graph
  108. self.reset()
  109. else:
  110. self._append_node(node)
  111. self._subgraph_parse(node, node, extra_blocks)
  112. def _subgraph_parse(
  113. self,
  114. node: _SubGraphNodes,
  115. pathnode: _SubGraphNodes,
  116. extra_blocks: Sequence[nodes.ExceptHandler | nodes.MatchCase],
  117. ) -> None:
  118. """Parse `match`/`case` blocks, or the body and `else` block of `if`/`for`
  119. statements.
  120. """
  121. loose_ends = []
  122. if isinstance(node, nodes.Match):
  123. for case in extra_blocks:
  124. if isinstance(case, nodes.MatchCase):
  125. self.tail = node
  126. self.dispatch_list(case.body)
  127. loose_ends.append(self.tail)
  128. loose_ends.append(node)
  129. else:
  130. self.tail = node
  131. self.dispatch_list(node.body)
  132. loose_ends.append(self.tail)
  133. for extra in extra_blocks:
  134. self.tail = node
  135. self.dispatch_list(extra.body)
  136. loose_ends.append(self.tail)
  137. if node.orelse:
  138. self.tail = node
  139. self.dispatch_list(node.orelse)
  140. loose_ends.append(self.tail)
  141. else:
  142. loose_ends.append(node)
  143. if node and self.graph:
  144. bottom = f"{self._bottom_counter}"
  145. self._bottom_counter += 1
  146. for end in loose_ends:
  147. self.graph.connect(end, bottom)
  148. self.tail = bottom
  149. class McCabeMethodChecker(checkers.BaseChecker):
  150. """Checks McCabe complexity cyclomatic threshold in methods and functions
  151. to validate a too complex code.
  152. """
  153. name = "design"
  154. msgs = {
  155. "R1260": (
  156. "%s is too complex. The McCabe rating is %d",
  157. "too-complex",
  158. "Used when a method or function is too complex based on "
  159. "McCabe Complexity Cyclomatic",
  160. )
  161. }
  162. options = (
  163. (
  164. "max-complexity",
  165. {
  166. "default": 10,
  167. "type": "int",
  168. "metavar": "<int>",
  169. "help": "McCabe complexity cyclomatic threshold",
  170. },
  171. ),
  172. )
  173. @only_required_for_messages("too-complex")
  174. def visit_module(self, node: nodes.Module) -> None:
  175. """Visit an nodes.Module node to check too complex rating and
  176. add message if is greater than max_complexity stored from options.
  177. """
  178. visitor = PathGraphingAstVisitor()
  179. for child in node.body:
  180. visitor.preorder(child, visitor)
  181. for graph in visitor.graphs.values():
  182. complexity = graph.complexity()
  183. node = graph.root
  184. if hasattr(node, "name"):
  185. node_name = f"'{node.name}'"
  186. else:
  187. node_name = f"This '{node.__class__.__name__.lower()}'"
  188. if complexity <= self.linter.config.max_complexity:
  189. continue
  190. self.add_message(
  191. "too-complex", node=node, confidence=HIGH, args=(node_name, complexity)
  192. )
  193. def register(linter: PyLinter) -> None:
  194. linter.register_checker(McCabeMethodChecker(linter))