eric6/Plugins/CheckerPlugins/CodeStyleChecker/ComplexityChecker.py

changeset 6942
2602857055c5
parent 6645
ad476851d7e0
child 7360
9190402e4505
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/ComplexityChecker.py	Sun Apr 14 15:09:21 2019 +0200
@@ -0,0 +1,251 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2015 - 2019 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+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 = [
+        "C101",
+        "C111", "C112",
+        
+        "C901",
+    ]
+
+    def __init__(self, source, filename, select, ignore, args):
+        """
+        Constructor
+        
+        @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):
+                self.__checkers.append(checker)
+    
+    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):
+            return
+        
+        if code in self.counters:
+            self.counters[code] += 1
+        else:
+            self.counters[code] = 1
+        
+        if code:
+            # record the issue with one based line number
+            self.errors.append(
+                (self.__filename, lineNumber, offset, (code, 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]
+        else:
+            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
+            return
+        
+        if not self.__checkers:
+            # don't do anything, if no codes were selected
+            return
+        
+        try:
+            self.__tree = compile(''.join(self.__source), self.__filename,
+                                  'exec', ast.PyCF_ONLY_AST)
+        except (SyntaxError, TypeError):
+            self.__reportInvalidSyntax()
+            return
+        
+        for check in self.__checkers:
+            check()
+    
+    def __checkMcCabeComplexity(self):
+        """
+        Private method to check the McCabe code complexity.
+        """
+        try:
+            # create the AST again because it is modified by the checker
+            tree = compile(''.join(self.__source), self.__filename, 'exec',
+                           ast.PyCF_ONLY_AST)
+        except (SyntaxError, TypeError):
+            # compile errors are already reported by the run() method
+            return
+        
+        maxComplexity = self.__args.get("McCabeComplexity",
+                                        self.__defaultArgs["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()
+        visitor.visit(self.__tree)
+        
+        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):
+        """
+        Constructor
+        """
+        super(LineComplexityVisitor, self).__init__()
+        self.__count = {}
+    
+    def visit(self, node):
+        """
+        Public method to recursively visit all the nodes and add up the
+        instructions.
+        
+        @param node reference to the node
+        @type ast.AST
+        """
+        if hasattr(node, 'lineno'):
+            self.__count[node.lineno] = self.__count.get(node.lineno, 0) + 1
+        self.generic_visit(node)
+    
+    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])
+        else:
+            return (
+                (sortedList[medianIndex] + sortedList[medianIndex + 1]) / 2.0
+            )
+
+#
+# eflag: noqa = M702

eric ide

mercurial