| # Copyright (c) 2016-2020 Claudiu Popa <[email protected]> |
| # Copyright (c) 2016 Moises Lopez <[email protected]> |
| # Copyright (c) 2017, 2020 hippo91 <[email protected]> |
| # Copyright (c) 2019, 2021 Pierre Sassoulas <[email protected]> |
| # Copyright (c) 2019 Hugo van Kemenade <[email protected]> |
| # Copyright (c) 2020 Anthony Sottile <[email protected]> |
| |
| # Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html |
| # For details: https://github.com/PyCQA/pylint/blob/master/COPYING |
| |
| """Module to add McCabe checker class for pylint. """ |
| |
| from mccabe import PathGraph as Mccabe_PathGraph |
| from mccabe import PathGraphingAstVisitor as Mccabe_PathGraphingAstVisitor |
| |
| from pylint import checkers |
| from pylint.checkers.utils import check_messages |
| from pylint.interfaces import HIGH, IAstroidChecker |
| |
| |
| class PathGraph(Mccabe_PathGraph): |
| def __init__(self, node): |
| super().__init__(name="", entity="", lineno=1) |
| self.root = node |
| |
| |
| class PathGraphingAstVisitor(Mccabe_PathGraphingAstVisitor): |
| def __init__(self): |
| super().__init__() |
| self._bottom_counter = 0 |
| |
| def default(self, node, *args): |
| for child in node.get_children(): |
| self.dispatch(child, *args) |
| |
| def dispatch(self, node, *args): |
| 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): |
| if self.graph is not None: |
| # closure |
| pathnode = self._append_node(node) |
| self.tail = pathnode |
| self.dispatch_list(node.body) |
| bottom = "%s" % 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.tail = node |
| self.dispatch_list(node.body) |
| self.graphs[f"{self.classname}{node.name}"] = self.graph |
| self.reset() |
| |
| visitAsyncFunctionDef = visitFunctionDef |
| |
| def visitSimpleStatement(self, node): |
| self._append_node(node) |
| |
| visitAssert = ( |
| visitAssign |
| ) = ( |
| visitAugAssign |
| ) = ( |
| visitDelete |
| ) = ( |
| visitPrint |
| ) = ( |
| visitRaise |
| ) = ( |
| visitYield |
| ) = ( |
| visitImport |
| ) = ( |
| visitCall |
| ) = ( |
| visitSubscript |
| ) = ( |
| visitPass |
| ) = ( |
| visitContinue |
| ) = ( |
| visitBreak |
| ) = visitGlobal = visitReturn = visitExpr = visitAwait = visitSimpleStatement |
| |
| def visitWith(self, node): |
| self._append_node(node) |
| self.dispatch_list(node.body) |
| |
| visitAsyncWith = visitWith |
| |
| def _append_node(self, node): |
| if not self.tail: |
| return None |
| self.graph.connect(self.tail, node) |
| self.tail = node |
| return node |
| |
| def _subgraph(self, node, name, extra_blocks=()): |
| """create the subgraphs representing any `if` and `for` statements""" |
| 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() |
| else: |
| self._append_node(node) |
| self._subgraph_parse(node, node, extra_blocks) |
| |
| def _subgraph_parse(self, node, pathnode, extra_blocks): |
| """parse the body and any `else` block of `if` and `for` statements""" |
| loose_ends = [] |
| self.tail = node |
| self.dispatch_list(node.body) |
| loose_ends.append(self.tail) |
| for extra in extra_blocks: |
| self.tail = node |
| self.dispatch_list(extra.body) |
| loose_ends.append(self.tail) |
| if node.orelse: |
| self.tail = node |
| self.dispatch_list(node.orelse) |
| loose_ends.append(self.tail) |
| else: |
| loose_ends.append(node) |
| if node: |
| bottom = "%s" % self._bottom_counter |
| self._bottom_counter += 1 |
| for end in loose_ends: |
| self.graph.connect(end, bottom) |
| self.tail = bottom |
| |
| |
| class McCabeMethodChecker(checkers.BaseChecker): |
| """Checks McCabe complexity cyclomatic threshold in methods and functions |
| to validate a too complex code. |
| """ |
| |
| __implements__ = IAstroidChecker |
| name = "design" |
| |
| msgs = { |
| "R1260": ( |
| "%s is too complex. The McCabe rating is %d", |
| "too-complex", |
| "Used when a method or function is too complex based on " |
| "McCabe Complexity Cyclomatic", |
| ) |
| } |
| options = ( |
| ( |
| "max-complexity", |
| { |
| "default": 10, |
| "type": "int", |
| "metavar": "<int>", |
| "help": "McCabe complexity cyclomatic threshold", |
| }, |
| ), |
| ) |
| |
| @check_messages("too-complex") |
| def visit_module(self, node): |
| """visit an astroid.Module node to check too complex rating and |
| add message if is greather than max_complexity stored from options""" |
| visitor = PathGraphingAstVisitor() |
| for child in node.body: |
| visitor.preorder(child, visitor) |
| for graph in visitor.graphs.values(): |
| complexity = graph.complexity() |
| node = graph.root |
| if hasattr(node, "name"): |
| node_name = "'%s'" % node.name |
| else: |
| node_name = "This '%s'" % node.__class__.__name__.lower() |
| if complexity <= self.config.max_complexity: |
| continue |
| self.add_message( |
| "too-complex", node=node, confidence=HIGH, args=(node_name, complexity) |
| ) |
| |
| |
| def register(linter): |
| """Required method to auto register this checker. |
| |
| :param linter: Main interface object for Pylint plugins |
| :type linter: Pylint object |
| """ |
| linter.register_checker(McCabeMethodChecker(linter)) |