mccabe.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  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, TypeVar, Union
  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 = Union[
  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 = Union[nodes.If, nodes.TryExcept, nodes.For, nodes.While]
  35. _AppendableNodeT = TypeVar(
  36. "_AppendableNodeT", bound=Union[_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 = (
  80. visitAssign
  81. ) = (
  82. visitAugAssign
  83. ) = (
  84. visitDelete
  85. ) = (
  86. visitRaise
  87. ) = (
  88. visitYield
  89. ) = (
  90. visitImport
  91. ) = (
  92. visitCall
  93. ) = (
  94. visitSubscript
  95. ) = (
  96. visitPass
  97. ) = (
  98. visitContinue
  99. ) = (
  100. visitBreak
  101. ) = visitGlobal = visitReturn = visitExpr = visitAwait = visitSimpleStatement
  102. def visitWith(self, node: nodes.With) -> None:
  103. self._append_node(node)
  104. self.dispatch_list(node.body)
  105. visitAsyncWith = visitWith
  106. def _append_node(self, node: _AppendableNodeT) -> _AppendableNodeT | None:
  107. if not self.tail or not self.graph:
  108. return None
  109. self.graph.connect(self.tail, node)
  110. self.tail = node
  111. return node
  112. def _subgraph(
  113. self,
  114. node: _SubGraphNodes,
  115. name: str,
  116. extra_blocks: Sequence[nodes.ExceptHandler] = (),
  117. ) -> None:
  118. """Create the subgraphs representing any `if` and `for` statements."""
  119. if self.graph is None:
  120. # global loop
  121. self.graph = PathGraph(node)
  122. self._subgraph_parse(node, node, extra_blocks)
  123. self.graphs[f"{self.classname}{name}"] = self.graph
  124. self.reset()
  125. else:
  126. self._append_node(node)
  127. self._subgraph_parse(node, node, extra_blocks)
  128. def _subgraph_parse(
  129. self,
  130. node: _SubGraphNodes,
  131. pathnode: _SubGraphNodes,
  132. extra_blocks: Sequence[nodes.ExceptHandler],
  133. ) -> None:
  134. """Parse the body and any `else` block of `if` and `for` statements."""
  135. loose_ends = []
  136. self.tail = node
  137. self.dispatch_list(node.body)
  138. loose_ends.append(self.tail)
  139. for extra in extra_blocks:
  140. self.tail = node
  141. self.dispatch_list(extra.body)
  142. loose_ends.append(self.tail)
  143. if node.orelse:
  144. self.tail = node
  145. self.dispatch_list(node.orelse)
  146. loose_ends.append(self.tail)
  147. else:
  148. loose_ends.append(node)
  149. if node and self.graph:
  150. bottom = f"{self._bottom_counter}"
  151. self._bottom_counter += 1
  152. for end in loose_ends:
  153. self.graph.connect(end, bottom)
  154. self.tail = bottom
  155. class McCabeMethodChecker(checkers.BaseChecker):
  156. """Checks McCabe complexity cyclomatic threshold in methods and functions
  157. to validate a too complex code.
  158. """
  159. name = "design"
  160. msgs = {
  161. "R1260": (
  162. "%s is too complex. The McCabe rating is %d",
  163. "too-complex",
  164. "Used when a method or function is too complex based on "
  165. "McCabe Complexity Cyclomatic",
  166. )
  167. }
  168. options = (
  169. (
  170. "max-complexity",
  171. {
  172. "default": 10,
  173. "type": "int",
  174. "metavar": "<int>",
  175. "help": "McCabe complexity cyclomatic threshold",
  176. },
  177. ),
  178. )
  179. @only_required_for_messages("too-complex")
  180. def visit_module(self, node: nodes.Module) -> None:
  181. """Visit an astroid.Module node to check too complex rating and
  182. add message if is greater than max_complexity stored from options.
  183. """
  184. visitor = PathGraphingAstVisitor()
  185. for child in node.body:
  186. visitor.preorder(child, visitor)
  187. for graph in visitor.graphs.values():
  188. complexity = graph.complexity()
  189. node = graph.root
  190. if hasattr(node, "name"):
  191. node_name = f"'{node.name}'"
  192. else:
  193. node_name = f"This '{node.__class__.__name__.lower()}'"
  194. if complexity <= self.linter.config.max_complexity:
  195. continue
  196. self.add_message(
  197. "too-complex", node=node, confidence=HIGH, args=(node_name, complexity)
  198. )
  199. def register(linter: PyLinter) -> None:
  200. linter.register_checker(McCabeMethodChecker(linter))