Sun, 04 Oct 2020 15:25:17 +0200

Detlev Offenbach <>
Sun, 04 Oct 2020 15:25:17 +0200
changeset 7749
parent 7639

Configuration Dialog: optimized the display of the various configuration pages.

# -*- coding: utf-8 -*-

# Copyright (c) 2015 - 2020 Detlev Offenbach <>

Module implementing a checker for code complexity.

import sys
import ast

from mccabe import PathGraphingAstVisitor

class ComplexityChecker(object):
    Class implementing a checker for code complexity.
    Codes = [
        "C111", "C112",

    def __init__(self, source, filename, select, ignore, args):
        @param source source code to be checked
        @type list of str
        @param filename name of the source file
        @type str
        @param select list of selected codes
        @type list of str
        @param ignore list of codes to be ignored
        @type list of str
        @param args dictionary of arguments for the miscellaneous checks
        @type dict
        self.__filename = filename
        self.__source = source[:]
        self.__select = tuple(select)
        self.__ignore = ('',) if select else tuple(ignore)
        self.__args = args
        self.__defaultArgs = {
            "McCabeComplexity": 10,
            "LineComplexity": 15,
            "LineComplexityScore": 10,
        # statistics counters
        self.counters = {}
        # collection of detected errors
        self.errors = []
        checkersWithCodes = [
            (self.__checkMcCabeComplexity, ("C101",)),
            (self.__checkLineComplexity, ("C111", "C112")),
        self.__checkers = []
        for checker, codes in checkersWithCodes:
            if any(not (code and self.__ignoreCode(code))
                    for code in codes):
    def __ignoreCode(self, code):
        Private method to check if the message code should be ignored.

        @param code message code to check for
        @type str
        @return flag indicating to ignore the given code
        @rtype bool
        return (code.startswith(self.__ignore) and
                not code.startswith(self.__select))
    def __error(self, lineNumber, offset, code, *args):
        Private method to record an issue.
        @param lineNumber line number of the issue
        @type int
        @param offset position within line of the issue
        @type int
        @param code message code
        @type str
        @param args arguments for the message
        @type list
        if self.__ignoreCode(code):
        if code in self.counters:
            self.counters[code] += 1
            self.counters[code] = 1
        if code:
            # record the issue with one based line number
                    "file": self.__filename,
                    "line": lineNumber,
                    "offset": offset,
                    "code": code,
                    "args": args,
    def __reportInvalidSyntax(self):
        Private method to report a syntax error.
        exc_type, exc = sys.exc_info()[:2]
        if len(exc.args) > 1:
            offset = exc.args[1]
            if len(offset) > 2:
                offset = offset[1:3]
            offset = (1, 0)
        self.__error(offset[0] - 1, offset[1] or 0,
                     'C901', exc_type.__name__, exc.args[0])
    def run(self):
        Public method to check the given source for code complexity.
        if not self.__filename or not self.__source:
            # don't do anything, if essential data is missing
        if not self.__checkers:
            # don't do anything, if no codes were selected
            self.__tree = compile(''.join(self.__source), self.__filename,
                                  'exec', ast.PyCF_ONLY_AST)
        except (SyntaxError, TypeError):
        for check in self.__checkers:
    def __checkMcCabeComplexity(self):
        Private method to check the McCabe code complexity.
            # create the AST again because it is modified by the checker
            tree = compile(''.join(self.__source), self.__filename, 'exec',
        except (SyntaxError, TypeError):
            # compile errors are already reported by the run() method
        maxComplexity = self.__args.get("McCabeComplexity",
        visitor = PathGraphingAstVisitor()
        visitor.preorder(tree, visitor)
        for graph in visitor.graphs.values():
            if graph.complexity() > maxComplexity:
                self.__error(graph.lineno, 0, "C101",
                             graph.entity, graph.complexity())
    def __checkLineComplexity(self):
        Private method to check the complexity of a single line of code and
        the median line complexity of the source code.
        Complexity is defined as the number of AST nodes produced by a line
        of code.
        maxLineComplexity = self.__args.get(
            "LineComplexity", self.__defaultArgs["LineComplexity"])
        maxLineComplexityScore = self.__args.get(
            "LineComplexityScore", self.__defaultArgs["LineComplexityScore"])
        visitor = LineComplexityVisitor()
        sortedItems = visitor.sortedList()
        score = visitor.score()
        for line, complexity in sortedItems:
            if complexity > maxLineComplexity:
                self.__error(line, 0, "C111", complexity)
        if score > maxLineComplexityScore:
            self.__error(0, 0, "C112", score)

class LineComplexityVisitor(ast.NodeVisitor):
    Class calculating the number of AST nodes per line of code
    and the median nodes/line score.
    def __init__(self):
        super(LineComplexityVisitor, self).__init__()
        self.__count = {}
    def visit(self, node):
        Public method to recursively visit all the nodes and add up the
        @param node reference to the node
        @type ast.AST
        if hasattr(node, 'lineno'):
            self.__count[node.lineno] = self.__count.get(node.lineno, 0) + 1
    def sortedList(self):
        Public method to get a sorted list of (line, nodes) tuples.
        @return sorted list of (line, nodes) tuples
        @rtype list of tuple of (int,int)
        lst = [(line, self.__count[line])
               for line in sorted(self.__count.keys())]
        return lst

    def score(self):
        Public method to calculate the median.
        @return median line complexity value
        @rtype float
        lst = self.__count.values()
        sortedList = sorted(lst)
        listLength = len(lst)
        medianIndex = (listLength - 1) // 2
        if listLength == 0:
            return 0.0
        elif (listLength % 2):
            return float(sortedList[medianIndex])
            return (
                (sortedList[medianIndex] + sortedList[medianIndex + 1]) / 2.0

eric ide