Mon, 12 Oct 2020 19:54:03 +0200
Started refactoring the code style checker.
--- a/eric6.e4p Mon Oct 12 19:28:42 2020 +0200 +++ b/eric6.e4p Mon Oct 12 19:54:03 2020 +0200 @@ -307,7 +307,9 @@ <Source>eric6/PluginManager/__init__.py</Source> <Source>eric6/Plugins/AboutPlugin/AboutDialog.py</Source> <Source>eric6/Plugins/AboutPlugin/__init__.py</Source> - <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/AnnotationsChecker.py</Source> + <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/AnnotationsChecker.py</Source> + <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/__init__.py</Source> + <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/translations.py</Source> <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/AstUtilities.py</Source> <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleAddBuiltinIgnoreDialog.py</Source> <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleChecker.py</Source> @@ -315,7 +317,9 @@ <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleCodeSelectionDialog.py</Source> <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleFixer.py</Source> <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleStatisticsDialog.py</Source> - <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/ComplexityChecker.py</Source> + <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Complexity/ComplexityChecker.py</Source> + <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Complexity/__init__.py</Source> + <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Complexity/translations.py</Source> <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/DocStyleChecker.py</Source> <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/MiscellaneousChecker.py</Source> <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/NamingStyleChecker.py</Source>
--- a/eric6/Documentation/Source/eric6.Plugins.CheckerPlugins.CodeStyleChecker.AnnotationsChecker.html Mon Oct 12 19:28:42 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,516 +0,0 @@ -<!DOCTYPE html> -<html><head> -<title>eric6.Plugins.CheckerPlugins.CodeStyleChecker.AnnotationsChecker</title> -<meta charset="UTF-8"> -<style> -body { - background: #EDECE6; - margin: 0em 1em 10em 1em; - color: black; -} - -h1 { color: white; background: #85774A; } -h2 { color: white; background: #85774A; } -h3 { color: white; background: #9D936E; } -h4 { color: white; background: #9D936E; } - -a { color: #BA6D36; } - -</style> -</head> -<body> -<a NAME="top" ID="top"></a> -<h1>eric6.Plugins.CheckerPlugins.CodeStyleChecker.AnnotationsChecker</h1> - -<p> -Module implementing a checker for function type annotations. -</p> -<h3>Global Attributes</h3> - -<table> -<tr><td>None</td></tr> -</table> -<h3>Classes</h3> - -<table> - -<tr> -<td><a href="#AnnotationsChecker">AnnotationsChecker</a></td> -<td>Class implementing a checker for function type annotations.</td> -</tr> -<tr> -<td><a href="#FunctionVisitor">FunctionVisitor</a></td> -<td>Class implementing a node visitor to check function annotations.</td> -</tr> -</table> -<h3>Functions</h3> - -<table> - -<tr> -<td><a href="#getAnnotationComplexity">getAnnotationComplexity</a></td> -<td>Function to determine the annotation complexity.</td> -</tr> -<tr> -<td><a href="#hasTypeAnnotations">hasTypeAnnotations</a></td> -<td>Function to check for type annotations.</td> -</tr> -</table> -<hr /> -<hr /> -<a NAME="AnnotationsChecker" ID="AnnotationsChecker"></a> -<h2>AnnotationsChecker</h2> - -<p> - Class implementing a checker for function type annotations. -</p> -<h3>Derived from</h3> -object -<h3>Class Attributes</h3> - -<table> -<tr><td>Codes</td></tr> -</table> -<h3>Class Methods</h3> - -<table> -<tr><td>None</td></tr> -</table> -<h3>Methods</h3> - -<table> - -<tr> -<td><a href="#AnnotationsChecker.__init__">AnnotationsChecker</a></td> -<td>Constructor</td> -</tr> -<tr> -<td><a href="#AnnotationsChecker.__checkAnnotationComplexity">__checkAnnotationComplexity</a></td> -<td>Private method to check the type annotation complexity.</td> -</tr> -<tr> -<td><a href="#AnnotationsChecker.__checkAnnotationsCoverage">__checkAnnotationsCoverage</a></td> -<td>Private method to check for function annotation coverage.</td> -</tr> -<tr> -<td><a href="#AnnotationsChecker.__checkFunctionAnnotations">__checkFunctionAnnotations</a></td> -<td>Private method to check for function annotation issues.</td> -</tr> -<tr> -<td><a href="#AnnotationsChecker.__error">__error</a></td> -<td>Private method to record an issue.</td> -</tr> -<tr> -<td><a href="#AnnotationsChecker.__generateTree">__generateTree</a></td> -<td>Private method to generate an AST for our source.</td> -</tr> -<tr> -<td><a href="#AnnotationsChecker.__ignoreCode">__ignoreCode</a></td> -<td>Private method to check if the message code should be ignored.</td> -</tr> -<tr> -<td><a href="#AnnotationsChecker.__reportInvalidSyntax">__reportInvalidSyntax</a></td> -<td>Private method to report a syntax error.</td> -</tr> -<tr> -<td><a href="#AnnotationsChecker.run">run</a></td> -<td>Public method to check the given source against annotation issues.</td> -</tr> -</table> -<h3>Static Methods</h3> - -<table> -<tr><td>None</td></tr> -</table> - -<a NAME="AnnotationsChecker.__init__" ID="AnnotationsChecker.__init__"></a> -<h4>AnnotationsChecker (Constructor)</h4> -<b>AnnotationsChecker</b>(<i>source, filename, select, ignore, expected, repeat, args</i>) - -<p> - Constructor -</p> -<dl> - -<dt><i>source</i> (list of str)</dt> -<dd> -source code to be checked -</dd> -<dt><i>filename</i> (str)</dt> -<dd> -name of the source file -</dd> -<dt><i>select</i> (list of str)</dt> -<dd> -list of selected codes -</dd> -<dt><i>ignore</i> (list of str)</dt> -<dd> -list of codes to be ignored -</dd> -<dt><i>expected</i> (list of str)</dt> -<dd> -list of expected codes -</dd> -<dt><i>repeat</i> (bool)</dt> -<dd> -flag indicating to report each occurrence of a code -</dd> -<dt><i>args</i> (dict)</dt> -<dd> -dictionary of arguments for the annotation checks -</dd> -</dl> -<a NAME="AnnotationsChecker.__checkAnnotationComplexity" ID="AnnotationsChecker.__checkAnnotationComplexity"></a> -<h4>AnnotationsChecker.__checkAnnotationComplexity</h4> -<b>__checkAnnotationComplexity</b>(<i></i>) - -<p> - Private method to check the type annotation complexity. -</p> -<a NAME="AnnotationsChecker.__checkAnnotationsCoverage" ID="AnnotationsChecker.__checkAnnotationsCoverage"></a> -<h4>AnnotationsChecker.__checkAnnotationsCoverage</h4> -<b>__checkAnnotationsCoverage</b>(<i></i>) - -<p> - Private method to check for function annotation coverage. -</p> -<a NAME="AnnotationsChecker.__checkFunctionAnnotations" ID="AnnotationsChecker.__checkFunctionAnnotations"></a> -<h4>AnnotationsChecker.__checkFunctionAnnotations</h4> -<b>__checkFunctionAnnotations</b>(<i></i>) - -<p> - Private method to check for function annotation issues. -</p> -<a NAME="AnnotationsChecker.__error" ID="AnnotationsChecker.__error"></a> -<h4>AnnotationsChecker.__error</h4> -<b>__error</b>(<i>lineNumber, offset, code, *args</i>) - -<p> - Private method to record an issue. -</p> -<dl> - -<dt><i>lineNumber</i> (int)</dt> -<dd> -line number of the issue -</dd> -<dt><i>offset</i> (int)</dt> -<dd> -position within line of the issue -</dd> -<dt><i>code</i> (str)</dt> -<dd> -message code -</dd> -<dt><i>args</i> (list)</dt> -<dd> -arguments for the message -</dd> -</dl> -<a NAME="AnnotationsChecker.__generateTree" ID="AnnotationsChecker.__generateTree"></a> -<h4>AnnotationsChecker.__generateTree</h4> -<b>__generateTree</b>(<i></i>) - -<p> - Private method to generate an AST for our source. -</p> -<dl> -<dt>Returns:</dt> -<dd> -generated AST -</dd> -</dl> -<dl> -<dt>Return Type:</dt> -<dd> -ast.Module -</dd> -</dl> -<a NAME="AnnotationsChecker.__ignoreCode" ID="AnnotationsChecker.__ignoreCode"></a> -<h4>AnnotationsChecker.__ignoreCode</h4> -<b>__ignoreCode</b>(<i>code</i>) - -<p> - Private method to check if the message code should be ignored. -</p> -<dl> - -<dt><i>code</i> (str)</dt> -<dd> -message code to check for -</dd> -</dl> -<dl> -<dt>Returns:</dt> -<dd> -flag indicating to ignore the given code -</dd> -</dl> -<dl> -<dt>Return Type:</dt> -<dd> -bool -</dd> -</dl> -<a NAME="AnnotationsChecker.__reportInvalidSyntax" ID="AnnotationsChecker.__reportInvalidSyntax"></a> -<h4>AnnotationsChecker.__reportInvalidSyntax</h4> -<b>__reportInvalidSyntax</b>(<i></i>) - -<p> - Private method to report a syntax error. -</p> -<a NAME="AnnotationsChecker.run" ID="AnnotationsChecker.run"></a> -<h4>AnnotationsChecker.run</h4> -<b>run</b>(<i></i>) - -<p> - Public method to check the given source against annotation issues. -</p> -<div align="right"><a href="#top">Up</a></div> -<hr /> -<hr /> -<a NAME="FunctionVisitor" ID="FunctionVisitor"></a> -<h2>FunctionVisitor</h2> - -<p> - Class implementing a node visitor to check function annotations. -</p> -<p> - Note: this class is modelled after flake8-annotations checker. -</p> -<h3>Derived from</h3> -ast.NodeVisitor -<h3>Class Attributes</h3> - -<table> -<tr><td>None</td></tr> -</table> -<h3>Class Methods</h3> - -<table> -<tr><td>None</td></tr> -</table> -<h3>Methods</h3> - -<table> - -<tr> -<td><a href="#FunctionVisitor.__init__">FunctionVisitor</a></td> -<td>Constructor</td> -</tr> -<tr> -<td><a href="#FunctionVisitor.__checkFunctionNode">__checkFunctionNode</a></td> -<td>Private method to check an individual function definition node.</td> -</tr> -<tr> -<td><a href="#FunctionVisitor.__classifyArgumentError">__classifyArgumentError</a></td> -<td>Private method to classify and record an argument annotation issue.</td> -</tr> -<tr> -<td><a href="#FunctionVisitor.__classifyReturnError">__classifyReturnError</a></td> -<td>Private method to classify and record a return annotation issue.</td> -</tr> -<tr> -<td><a href="#FunctionVisitor.visit_AsyncFunctionDef">visit_AsyncFunctionDef</a></td> -<td>Public method to handle an async function or method definition.</td> -</tr> -<tr> -<td><a href="#FunctionVisitor.visit_ClassDef">visit_ClassDef</a></td> -<td>Public method to handle class definitions.</td> -</tr> -<tr> -<td><a href="#FunctionVisitor.visit_FunctionDef">visit_FunctionDef</a></td> -<td>Public method to handle a function or method definition.</td> -</tr> -</table> -<h3>Static Methods</h3> - -<table> -<tr><td>None</td></tr> -</table> - -<a NAME="FunctionVisitor.__init__" ID="FunctionVisitor.__init__"></a> -<h4>FunctionVisitor (Constructor)</h4> -<b>FunctionVisitor</b>(<i>sourceLines</i>) - -<p> - Constructor -</p> -<dl> - -<dt><i>sourceLines</i> (list of str)</dt> -<dd> -lines of source code -</dd> -</dl> -<a NAME="FunctionVisitor.__checkFunctionNode" ID="FunctionVisitor.__checkFunctionNode"></a> -<h4>FunctionVisitor.__checkFunctionNode</h4> -<b>__checkFunctionNode</b>(<i>node, classMethod=False</i>) - -<p> - Private method to check an individual function definition node. -</p> -<dl> - -<dt><i>node</i> (ast.FunctionDef or ast.AsyncFunctionDef)</dt> -<dd> -reference to the node to be processed -</dd> -<dt><i>classMethod</i> (bool)</dt> -<dd> -flag indicating a class method -</dd> -</dl> -<a NAME="FunctionVisitor.__classifyArgumentError" ID="FunctionVisitor.__classifyArgumentError"></a> -<h4>FunctionVisitor.__classifyArgumentError</h4> -<b>__classifyArgumentError</b>(<i>argNode, argType, methodType</i>) - -<p> - Private method to classify and record an argument annotation issue. -</p> -<dl> - -<dt><i>argNode</i> (ast.arguments)</dt> -<dd> -reference to the argument node -</dd> -<dt><i>argType</i> (str)</dt> -<dd> -type of the argument node -</dd> -<dt><i>methodType</i> (str)</dt> -<dd> -type of method/function the argument belongs to -</dd> -</dl> -<a NAME="FunctionVisitor.__classifyReturnError" ID="FunctionVisitor.__classifyReturnError"></a> -<h4>FunctionVisitor.__classifyReturnError</h4> -<b>__classifyReturnError</b>(<i>methodType, visibilityType, lineno, colOffset</i>) - -<p> - Private method to classify and record a return annotation issue. -</p> -<dl> - -<dt><i>methodType</i> (str)</dt> -<dd> -type of method/function the argument belongs to -</dd> -<dt><i>visibilityType</i> (str)</dt> -<dd> -visibility of the function -</dd> -<dt><i>lineno</i> (int)</dt> -<dd> -line number -</dd> -<dt><i>colOffset</i> (int)</dt> -<dd> -column number -</dd> -</dl> -<a NAME="FunctionVisitor.visit_AsyncFunctionDef" ID="FunctionVisitor.visit_AsyncFunctionDef"></a> -<h4>FunctionVisitor.visit_AsyncFunctionDef</h4> -<b>visit_AsyncFunctionDef</b>(<i>node</i>) - -<p> - Public method to handle an async function or method definition. -</p> -<dl> - -<dt><i>node</i> (ast.AsyncFunctionDef)</dt> -<dd> -reference to the node to be processed -</dd> -</dl> -<a NAME="FunctionVisitor.visit_ClassDef" ID="FunctionVisitor.visit_ClassDef"></a> -<h4>FunctionVisitor.visit_ClassDef</h4> -<b>visit_ClassDef</b>(<i>node</i>) - -<p> - Public method to handle class definitions. -</p> -<dl> - -<dt><i>node</i> (ast.ClassDef)</dt> -<dd> -reference to the node to be processed -</dd> -</dl> -<a NAME="FunctionVisitor.visit_FunctionDef" ID="FunctionVisitor.visit_FunctionDef"></a> -<h4>FunctionVisitor.visit_FunctionDef</h4> -<b>visit_FunctionDef</b>(<i>node</i>) - -<p> - Public method to handle a function or method definition. -</p> -<dl> - -<dt><i>node</i> (ast.FunctionDef)</dt> -<dd> -reference to the node to be processed -</dd> -</dl> -<div align="right"><a href="#top">Up</a></div> -<hr /> -<hr /> -<a NAME="getAnnotationComplexity" ID="getAnnotationComplexity"></a> -<h2>getAnnotationComplexity</h2> -<b>getAnnotationComplexity</b>(<i>annotationNode</i>) - -<p> - Function to determine the annotation complexity. -</p> -<dl> - -<dt><i>annotationNode</i> (ast.AST)</dt> -<dd> -reference to the node to determine the annotation - complexity for -</dd> -</dl> -<dl> -<dt>Returns:</dt> -<dd> -annotation complexity -</dd> -</dl> -<dl> -<dt>Return Type:</dt> -<dd> -= int -</dd> -</dl> -<div align="right"><a href="#top">Up</a></div> -<hr /> -<hr /> -<a NAME="hasTypeAnnotations" ID="hasTypeAnnotations"></a> -<h2>hasTypeAnnotations</h2> -<b>hasTypeAnnotations</b>(<i>funcNode</i>) - -<p> - Function to check for type annotations. -</p> -<dl> - -<dt><i>funcNode</i> (ast.AsyncFunctionDef or ast.FunctionDef)</dt> -<dd> -reference to the function definition node to be checked -</dd> -</dl> -<dl> -<dt>Returns:</dt> -<dd> -flag indicating the presence of type annotations -</dd> -</dl> -<dl> -<dt>Return Type:</dt> -<dd> -bool -</dd> -</dl> -<div align="right"><a href="#top">Up</a></div> -<hr /> -</body></html> \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/AnnotationsChecker.py Mon Oct 12 19:54:03 2020 +0200 @@ -0,0 +1,465 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2019 - 2020 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a checker for function type annotations. +""" + +import sys +import ast + +import AstUtilities + + +class AnnotationsChecker(object): + """ + Class implementing a checker for function type annotations. + """ + Codes = [ + ## Function Annotations + "A001", "A002", "A003", + + ## Method Annotations + "A101", "A102", + + ## Return Annotations + "A201", "A202", "A203", "A204", "A205", "A206", + + ## Annotation Coverage + "A881", + + ## Annotation Complexity + "A891", + + ## Syntax Error + "A999", + ] + + def __init__(self, source, filename, select, ignore, expected, repeat, + 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 expected list of expected codes + @type list of str + @param repeat flag indicating to report each occurrence of a code + @type bool + @param args dictionary of arguments for the annotation checks + @type dict + """ + self.__select = tuple(select) + self.__ignore = ('',) if select else tuple(ignore) + self.__expected = expected[:] + self.__repeat = repeat + self.__filename = filename + self.__source = source[:] + self.__args = args + + # statistics counters + self.counters = {} + + # collection of detected errors + self.errors = [] + + checkersWithCodes = [ + ( + self.__checkFunctionAnnotations, + ("A001", "A002", "A003", "A101", "A102", + "A201", "A202", "A203", "A204", "A205", "A206",) + ), + (self.__checkAnnotationsCoverage, ("A881",)), + (self.__checkAnnotationComplexity, ("A891",)), + ] + + self.__defaultArgs = { + "MinimumCoverage": 75, # % of type annotation coverage + "MaximumComplexity": 3, + } + + 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 + + # Don't care about expected codes + if code in self.__expected: + return + + if code and (self.counters[code] == 1 or self.__repeat): + # record the issue with one based line number + self.errors.append( + { + "file": self.__filename, + "line": lineNumber + 1, + "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] + else: + offset = (1, 0) + self.__error(offset[0] - 1, offset[1] or 0, + 'A999', exc_type.__name__, exc.args[0]) + + def __generateTree(self): + """ + Private method to generate an AST for our source. + + @return generated AST + @rtype ast.Module + """ + source = "".join(self.__source) + return compile(source, self.__filename, 'exec', ast.PyCF_ONLY_AST) + + def run(self): + """ + Public method to check the given source against annotation issues. + """ + if not self.__filename: + # 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 = self.__generateTree() + except (SyntaxError, TypeError): + self.__reportInvalidSyntax() + return + + for check in self.__checkers: + check() + + def __checkFunctionAnnotations(self): + """ + Private method to check for function annotation issues. + """ + visitor = FunctionVisitor(self.__source) + visitor.visit(self.__tree) + for issue in visitor.issues: + node = issue[0] + reason = issue[1] + params = issue[2:] + self.__error(node.lineno - 1, node.col_offset, reason, *params) + + def __checkAnnotationsCoverage(self): + """ + Private method to check for function annotation coverage. + """ + minAnnotationsCoverage = self.__args.get( + "MinimumCoverage", self.__defaultArgs["MinimumCoverage"]) + if minAnnotationsCoverage == 0: + # 0 means it is switched off + return + + functionDefs = [ + f for f in ast.walk(self.__tree) + if isinstance(f, (ast.AsyncFunctionDef, ast.FunctionDef)) + ] + if not functionDefs: + # no functions/methods at all + return + + functionDefAnnotationsInfo = [ + hasTypeAnnotations(f) for f in functionDefs + ] + annotationsCoverage = int( + len(list(filter(None, functionDefAnnotationsInfo))) / + len(functionDefAnnotationsInfo) * 100 + ) + if annotationsCoverage < minAnnotationsCoverage: + self.__error(0, 0, "A881", annotationsCoverage) + + def __checkAnnotationComplexity(self): + """ + Private method to check the type annotation complexity. + """ + maxAnnotationComplexity = self.__args.get( + "MaximumComplexity", self.__defaultArgs["MaximumComplexity"]) + typeAnnotations = [] + + functionDefs = [ + f for f in ast.walk(self.__tree) + if isinstance(f, (ast.AsyncFunctionDef, ast.FunctionDef)) + ] + for functionDef in functionDefs: + typeAnnotations += list(filter( + None, [a.annotation for a in functionDef.args.args])) + if functionDef.returns: + typeAnnotations.append(functionDef.returns) + typeAnnotations += [a.annotation for a in ast.walk(self.__tree) + if isinstance(a, ast.AnnAssign) and a.annotation] + for annotation in typeAnnotations: + complexity = getAnnotationComplexity(annotation) + if complexity > maxAnnotationComplexity: + self.__error(annotation.lineno - 1, annotation.col_offset, + "A891", complexity, maxAnnotationComplexity) + + +class FunctionVisitor(ast.NodeVisitor): + """ + Class implementing a node visitor to check function annotations. + + Note: this class is modelled after flake8-annotations checker. + """ + def __init__(self, sourceLines): + """ + Constructor + + @param sourceLines lines of source code + @type list of str + """ + super(FunctionVisitor, self).__init__() + + self.__sourceLines = sourceLines + + self.issues = [] + + def visit_FunctionDef(self, node): + """ + Public method to handle a function or method definition. + + @param node reference to the node to be processed + @type ast.FunctionDef + """ + self.__checkFunctionNode(node) + self.generic_visit(node) + + def visit_AsyncFunctionDef(self, node): + """ + Public method to handle an async function or method definition. + + @param node reference to the node to be processed + @type ast.AsyncFunctionDef + """ + self.__checkFunctionNode(node) + self.generic_visit(node) + + def visit_ClassDef(self, node): + """ + Public method to handle class definitions. + + @param node reference to the node to be processed + @type ast.ClassDef + """ + methodNodes = [ + childNode for childNode in node.body + if isinstance(childNode, (ast.FunctionDef, ast.AsyncFunctionDef)) + ] + for methodNode in methodNodes: + self.__checkFunctionNode(methodNode, classMethod=True) + + def __checkFunctionNode(self, node, classMethod=False): + """ + Private method to check an individual function definition node. + + @param node reference to the node to be processed + @type ast.FunctionDef or ast.AsyncFunctionDef + @param classMethod flag indicating a class method + @type bool + """ + if node.name.startswith("__") and node.name.endswith("__"): + visibilityType = "special" + elif node.name.startswith("__"): + visibilityType = "private" + elif node.name.startswith("_"): + visibilityType = "protected" + else: + visibilityType = "public" + + if classMethod: + decorators = [ + decorator.id for decorator in node.decorator_list + if isinstance(decorator, ast.Name) + ] + if "classmethod" in decorators: + classMethodType = "decorator" + elif "staticmethod" in decorators: + classMethodType = "staticmethod" + else: + classMethodType = "" + else: + classMethodType = "function" + + # check argument annotations + for argType in ("args", "vararg", "kwonlyargs", "kwarg"): + args = node.args.__getattribute__(argType) + if args: + if not isinstance(args, list): + args = [args] + + for arg in args: + if not arg.annotation: + self.__classifyArgumentError( + arg, argType, classMethodType) + + # check function return annotation + if not node.returns: + lineno = node.lineno + colOffset = self.__sourceLines[lineno - 1].rfind(":") + 1 + self.__classifyReturnError(classMethodType, visibilityType, + lineno, colOffset) + + def __classifyReturnError(self, methodType, visibilityType, lineno, + colOffset): + """ + Private method to classify and record a return annotation issue. + + @param methodType type of method/function the argument belongs to + @type str + @param visibilityType visibility of the function + @type str + @param lineno line number + @type int + @param colOffset column number + @type int + """ + # create a dummy AST node to report line and column + node = ast.AST() + node.lineno = lineno + node.col_offset = colOffset + + # now classify the issue + if methodType == "classmethod": + self.issues.append((node, "A206")) + elif methodType == "staticmethod": + self.issues.append((node, "A205")) + elif visibilityType == "special": + self.issues.append((node, "A204")) + elif visibilityType == "private": + self.issues.append((node, "A203")) + elif visibilityType == "protected": + self.issues.append((node, "A202")) + else: + self.issues.append((node, "A201")) + + def __classifyArgumentError(self, argNode, argType, methodType): + """ + Private method to classify and record an argument annotation issue. + + @param argNode reference to the argument node + @type ast.arguments + @param argType type of the argument node + @type str + @param methodType type of method/function the argument belongs to + @type str + """ + # check class method issues + if methodType != "function": + if argNode.arg in ("cls", "self"): + if methodType == "classmethod": + self.issues.append((argNode, "A102")) + return + elif methodType != "staticmethod": + self.issues.append((argNode, "A101")) + return + + # check all other arguments + if argType == "kwarg": + self.issues.append((argNode, "A003", argNode.arg)) + elif argType == "vararg": + self.issues.append((argNode, "A002", argNode.arg)) + else: + # args and kwonlyargs + self.issues.append((argNode, "A001", argNode.arg)) + +###################################################################### +## some utility functions below +###################################################################### + + +def hasTypeAnnotations(funcNode): + """ + Function to check for type annotations. + + @param funcNode reference to the function definition node to be checked + @type ast.AsyncFunctionDef or ast.FunctionDef + @return flag indicating the presence of type annotations + @rtype bool + """ + hasReturnAnnotation = funcNode.returns is not None + hasArgsAnnotations = any(a for a in funcNode.args.args + if a.annotation is not None) + hasKwargsAnnotations = (funcNode.args and + funcNode.args.kwarg and + funcNode.args.kwarg.annotation is not None) + hasKwonlyargsAnnotations = any(a for a in funcNode.args.kwonlyargs + if a.annotation is not None) + + return any((hasReturnAnnotation, hasArgsAnnotations, hasKwargsAnnotations, + hasKwonlyargsAnnotations)) + + +def getAnnotationComplexity(annotationNode): + """ + Function to determine the annotation complexity. + + @param annotationNode reference to the node to determine the annotation + complexity for + @type ast.AST + @return annotation complexity + @rtype = int + """ + if AstUtilities.isString(annotationNode): + annotationNode = ast.parse(annotationNode.s).body[0].value + if isinstance(annotationNode, ast.Subscript): + return 1 + getAnnotationComplexity(annotationNode.slice.value) + if isinstance(annotationNode, ast.Tuple): + return max(getAnnotationComplexity(n) for n in annotationNode.elts) + return 1
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/__init__.py Mon Oct 12 19:54:03 2020 +0200 @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Package implementing the function type annotations checker. +"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/translations.py Mon Oct 12 19:54:03 2020 +0200 @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de> +# + + +""" +Module implementing message translations for the code style plugin messages +(code annotations part). +""" + +from PyQt5.QtCore import QCoreApplication + +_annotationsMessages = { + "A001": QCoreApplication.translate( + "AnnotationsChecker", + "missing type annotation for function argument '{0}'"), + "A002": QCoreApplication.translate( + "AnnotationsChecker", + "missing type annotation for '*{0}'"), + "A003": QCoreApplication.translate( + "AnnotationsChecker", + "missing type annotation for '**{0}'"), + "A101": QCoreApplication.translate( + "AnnotationsChecker", + "missing type annotation for 'self' in method"), + "A102": QCoreApplication.translate( + "AnnotationsChecker", + "missing type annotation for 'cls' in classmethod"), + "A201": QCoreApplication.translate( + "AnnotationsChecker", + "missing return type annotation for public function"), + "A202": QCoreApplication.translate( + "AnnotationsChecker", + "missing return type annotation for protected function"), + "A203": QCoreApplication.translate( + "AnnotationsChecker", + "missing return type annotation for private function"), + "A204": QCoreApplication.translate( + "AnnotationsChecker", + "missing return type annotation for special method"), + "A205": QCoreApplication.translate( + "AnnotationsChecker", + "missing return type annotation for staticmethod"), + "A206": QCoreApplication.translate( + "AnnotationsChecker", + "missing return type annotation for classmethod"), + + "A881": QCoreApplication.translate( + "AnnotationsChecker", + "type annotation coverage of {0}% is too low"), + + "A891": QCoreApplication.translate( + "AnnotationsChecker", + "type annotation is too complex ({0} > {1})"), + + "A999": QCoreApplication.translate( + "AnnotationsChecker", + "{0}: {1}"), +} + +_annotationsMessagesSampleArgs = { + "A001": ["arg1"], + "A002": ["args"], + "A003": ["kwargs"], + "A881": [60], + "A891": [5, 3], + "A999": ["SyntaxError", "Invalid Syntax"], +}
--- a/eric6/Plugins/CheckerPlugins/CodeStyleChecker/AnnotationsChecker.py Mon Oct 12 19:28:42 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,465 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (c) 2019 - 2020 Detlev Offenbach <detlev@die-offenbachs.de> -# - -""" -Module implementing a checker for function type annotations. -""" - -import sys -import ast - -import AstUtilities - - -class AnnotationsChecker(object): - """ - Class implementing a checker for function type annotations. - """ - Codes = [ - ## Function Annotations - "A001", "A002", "A003", - - ## Method Annotations - "A101", "A102", - - ## Return Annotations - "A201", "A202", "A203", "A204", "A205", "A206", - - ## Annotation Coverage - "A881", - - ## Annotation Complexity - "A891", - - ## Syntax Error - "A999", - ] - - def __init__(self, source, filename, select, ignore, expected, repeat, - 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 expected list of expected codes - @type list of str - @param repeat flag indicating to report each occurrence of a code - @type bool - @param args dictionary of arguments for the annotation checks - @type dict - """ - self.__select = tuple(select) - self.__ignore = ('',) if select else tuple(ignore) - self.__expected = expected[:] - self.__repeat = repeat - self.__filename = filename - self.__source = source[:] - self.__args = args - - # statistics counters - self.counters = {} - - # collection of detected errors - self.errors = [] - - checkersWithCodes = [ - ( - self.__checkFunctionAnnotations, - ("A001", "A002", "A003", "A101", "A102", - "A201", "A202", "A203", "A204", "A205", "A206",) - ), - (self.__checkAnnotationsCoverage, ("A881",)), - (self.__checkAnnotationComplexity, ("A891",)), - ] - - self.__defaultArgs = { - "MinimumCoverage": 75, # % of type annotation coverage - "MaximumComplexity": 3, - } - - 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 - - # Don't care about expected codes - if code in self.__expected: - return - - if code and (self.counters[code] == 1 or self.__repeat): - # record the issue with one based line number - self.errors.append( - { - "file": self.__filename, - "line": lineNumber + 1, - "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] - else: - offset = (1, 0) - self.__error(offset[0] - 1, offset[1] or 0, - 'A999', exc_type.__name__, exc.args[0]) - - def __generateTree(self): - """ - Private method to generate an AST for our source. - - @return generated AST - @rtype ast.Module - """ - source = "".join(self.__source) - return compile(source, self.__filename, 'exec', ast.PyCF_ONLY_AST) - - def run(self): - """ - Public method to check the given source against annotation issues. - """ - if not self.__filename: - # 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 = self.__generateTree() - except (SyntaxError, TypeError): - self.__reportInvalidSyntax() - return - - for check in self.__checkers: - check() - - def __checkFunctionAnnotations(self): - """ - Private method to check for function annotation issues. - """ - visitor = FunctionVisitor(self.__source) - visitor.visit(self.__tree) - for issue in visitor.issues: - node = issue[0] - reason = issue[1] - params = issue[2:] - self.__error(node.lineno - 1, node.col_offset, reason, *params) - - def __checkAnnotationsCoverage(self): - """ - Private method to check for function annotation coverage. - """ - minAnnotationsCoverage = self.__args.get( - "MinimumCoverage", self.__defaultArgs["MinimumCoverage"]) - if minAnnotationsCoverage == 0: - # 0 means it is switched off - return - - functionDefs = [ - f for f in ast.walk(self.__tree) - if isinstance(f, (ast.AsyncFunctionDef, ast.FunctionDef)) - ] - if not functionDefs: - # no functions/methods at all - return - - functionDefAnnotationsInfo = [ - hasTypeAnnotations(f) for f in functionDefs - ] - annotationsCoverage = int( - len(list(filter(None, functionDefAnnotationsInfo))) / - len(functionDefAnnotationsInfo) * 100 - ) - if annotationsCoverage < minAnnotationsCoverage: - self.__error(0, 0, "A881", annotationsCoverage) - - def __checkAnnotationComplexity(self): - """ - Private method to check the type annotation complexity. - """ - maxAnnotationComplexity = self.__args.get( - "MaximumComplexity", self.__defaultArgs["MaximumComplexity"]) - typeAnnotations = [] - - functionDefs = [ - f for f in ast.walk(self.__tree) - if isinstance(f, (ast.AsyncFunctionDef, ast.FunctionDef)) - ] - for functionDef in functionDefs: - typeAnnotations += list(filter( - None, [a.annotation for a in functionDef.args.args])) - if functionDef.returns: - typeAnnotations.append(functionDef.returns) - typeAnnotations += [a.annotation for a in ast.walk(self.__tree) - if isinstance(a, ast.AnnAssign) and a.annotation] - for annotation in typeAnnotations: - complexity = getAnnotationComplexity(annotation) - if complexity > maxAnnotationComplexity: - self.__error(annotation.lineno - 1, annotation.col_offset, - "A891", complexity, maxAnnotationComplexity) - - -class FunctionVisitor(ast.NodeVisitor): - """ - Class implementing a node visitor to check function annotations. - - Note: this class is modelled after flake8-annotations checker. - """ - def __init__(self, sourceLines): - """ - Constructor - - @param sourceLines lines of source code - @type list of str - """ - super(FunctionVisitor, self).__init__() - - self.__sourceLines = sourceLines - - self.issues = [] - - def visit_FunctionDef(self, node): - """ - Public method to handle a function or method definition. - - @param node reference to the node to be processed - @type ast.FunctionDef - """ - self.__checkFunctionNode(node) - self.generic_visit(node) - - def visit_AsyncFunctionDef(self, node): - """ - Public method to handle an async function or method definition. - - @param node reference to the node to be processed - @type ast.AsyncFunctionDef - """ - self.__checkFunctionNode(node) - self.generic_visit(node) - - def visit_ClassDef(self, node): - """ - Public method to handle class definitions. - - @param node reference to the node to be processed - @type ast.ClassDef - """ - methodNodes = [ - childNode for childNode in node.body - if isinstance(childNode, (ast.FunctionDef, ast.AsyncFunctionDef)) - ] - for methodNode in methodNodes: - self.__checkFunctionNode(methodNode, classMethod=True) - - def __checkFunctionNode(self, node, classMethod=False): - """ - Private method to check an individual function definition node. - - @param node reference to the node to be processed - @type ast.FunctionDef or ast.AsyncFunctionDef - @param classMethod flag indicating a class method - @type bool - """ - if node.name.startswith("__") and node.name.endswith("__"): - visibilityType = "special" - elif node.name.startswith("__"): - visibilityType = "private" - elif node.name.startswith("_"): - visibilityType = "protected" - else: - visibilityType = "public" - - if classMethod: - decorators = [ - decorator.id for decorator in node.decorator_list - if isinstance(decorator, ast.Name) - ] - if "classmethod" in decorators: - classMethodType = "decorator" - elif "staticmethod" in decorators: - classMethodType = "staticmethod" - else: - classMethodType = "" - else: - classMethodType = "function" - - # check argument annotations - for argType in ("args", "vararg", "kwonlyargs", "kwarg"): - args = node.args.__getattribute__(argType) - if args: - if not isinstance(args, list): - args = [args] - - for arg in args: - if not arg.annotation: - self.__classifyArgumentError( - arg, argType, classMethodType) - - # check function return annotation - if not node.returns: - lineno = node.lineno - colOffset = self.__sourceLines[lineno - 1].rfind(":") + 1 - self.__classifyReturnError(classMethodType, visibilityType, - lineno, colOffset) - - def __classifyReturnError(self, methodType, visibilityType, lineno, - colOffset): - """ - Private method to classify and record a return annotation issue. - - @param methodType type of method/function the argument belongs to - @type str - @param visibilityType visibility of the function - @type str - @param lineno line number - @type int - @param colOffset column number - @type int - """ - # create a dummy AST node to report line and column - node = ast.AST() - node.lineno = lineno - node.col_offset = colOffset - - # now classify the issue - if methodType == "classmethod": - self.issues.append((node, "A206")) - elif methodType == "staticmethod": - self.issues.append((node, "A205")) - elif visibilityType == "special": - self.issues.append((node, "A204")) - elif visibilityType == "private": - self.issues.append((node, "A203")) - elif visibilityType == "protected": - self.issues.append((node, "A202")) - else: - self.issues.append((node, "A201")) - - def __classifyArgumentError(self, argNode, argType, methodType): - """ - Private method to classify and record an argument annotation issue. - - @param argNode reference to the argument node - @type ast.arguments - @param argType type of the argument node - @type str - @param methodType type of method/function the argument belongs to - @type str - """ - # check class method issues - if methodType != "function": - if argNode.arg in ("cls", "self"): - if methodType == "classmethod": - self.issues.append((argNode, "A102")) - return - elif methodType != "staticmethod": - self.issues.append((argNode, "A101")) - return - - # check all other arguments - if argType == "kwarg": - self.issues.append((argNode, "A003", argNode.arg)) - elif argType == "vararg": - self.issues.append((argNode, "A002", argNode.arg)) - else: - # args and kwonlyargs - self.issues.append((argNode, "A001", argNode.arg)) - -###################################################################### -## some utility functions below -###################################################################### - - -def hasTypeAnnotations(funcNode): - """ - Function to check for type annotations. - - @param funcNode reference to the function definition node to be checked - @type ast.AsyncFunctionDef or ast.FunctionDef - @return flag indicating the presence of type annotations - @rtype bool - """ - hasReturnAnnotation = funcNode.returns is not None - hasArgsAnnotations = any(a for a in funcNode.args.args - if a.annotation is not None) - hasKwargsAnnotations = (funcNode.args and - funcNode.args.kwarg and - funcNode.args.kwarg.annotation is not None) - hasKwonlyargsAnnotations = any(a for a in funcNode.args.kwonlyargs - if a.annotation is not None) - - return any((hasReturnAnnotation, hasArgsAnnotations, hasKwargsAnnotations, - hasKwonlyargsAnnotations)) - - -def getAnnotationComplexity(annotationNode): - """ - Function to determine the annotation complexity. - - @param annotationNode reference to the node to determine the annotation - complexity for - @type ast.AST - @return annotation complexity - @rtype = int - """ - if AstUtilities.isString(annotationNode): - annotationNode = ast.parse(annotationNode.s).body[0].value - if isinstance(annotationNode, ast.Subscript): - return 1 + getAnnotationComplexity(annotationNode.slice.value) - if isinstance(annotationNode, ast.Tuple): - return max(getAnnotationComplexity(n) for n in annotationNode.elts) - return 1
--- a/eric6/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleChecker.py Mon Oct 12 19:28:42 2020 +0200 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleChecker.py Mon Oct 12 19:54:03 2020 +0200 @@ -20,7 +20,7 @@ from DocStyleChecker import DocStyleChecker from MiscellaneousChecker import MiscellaneousChecker -from ComplexityChecker import ComplexityChecker +from Complexity.ComplexityChecker import ComplexityChecker from Security.SecurityChecker import SecurityChecker @@ -418,7 +418,7 @@ # check function annotations if sys.version_info >= (3, 5, 0): # annotations are supported from Python 3.5 on - from AnnotationsChecker import AnnotationsChecker + from Annotations.AnnotationsChecker import AnnotationsChecker annotationsChecker = AnnotationsChecker( source, filename, select, ignore, [], repeatMessages, annotationArgs)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Complexity/ComplexityChecker.py Mon Oct 12 19:54:03 2020 +0200 @@ -0,0 +1,255 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2015 - 2020 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( + { + "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] + 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 + )
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Complexity/__init__.py Mon Oct 12 19:54:03 2020 +0200 @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Package implementing the code complexity checker. +"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Complexity/translations.py Mon Oct 12 19:54:03 2020 +0200 @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de> +# + +# -*- coding: utf-8 -*- + +# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de> +# + + +""" +Module implementing message translations for the code style plugin messages +(code complexity part). +""" + +from PyQt5.QtCore import QCoreApplication + +_complexityMessages = { + "C101": QCoreApplication.translate( + "ComplexityChecker", "'{0}' is too complex ({1})"), + "C111": QCoreApplication.translate( + "ComplexityChecker", "source code line is too complex ({0})"), + "C112": QCoreApplication.translate( + "ComplexityChecker", + "overall source code line complexity is too high ({0})"), + "C901": QCoreApplication.translate( + "ComplexityChecker", "{0}: {1}"), +} + +_complexityMessagesSampleArgs = { + "C101": ["foo.bar", "42"], + "C111": [42], + "C112": [12.0], + "C901": ["SyntaxError", "Invalid Syntax"], +}
--- a/eric6/Plugins/CheckerPlugins/CodeStyleChecker/ComplexityChecker.py Mon Oct 12 19:28:42 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,255 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (c) 2015 - 2020 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( - { - "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] - 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 - )
--- a/eric6/Plugins/CheckerPlugins/CodeStyleChecker/translations.py Mon Oct 12 19:28:42 2020 +0200 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/translations.py Mon Oct 12 19:54:03 2020 +0200 @@ -12,6 +12,12 @@ from .Security.translations import ( _securityMessages, _securityMessagesSampleArgs ) +from .Annotations.translations import ( + _annotationsMessages, _annotationsMessagesSampleArgs +) +from .Complexity.translations import ( + _complexityMessages, _complexityMessagesSampleArgs +) # TODO: separate this huge dict into separate translations per checker type _messages = { @@ -471,19 +477,6 @@ "names 'l', 'O' and 'I' should be avoided"), ################################################################## - ## Code complexity messages - ################################################################## - "C101": QCoreApplication.translate( - "ComplexityChecker", "'{0}' is too complex ({1})"), - "C111": QCoreApplication.translate( - "ComplexityChecker", "source code line is too complex ({0})"), - "C112": QCoreApplication.translate( - "ComplexityChecker", - "overall source code line complexity is too high ({0})"), - "C901": QCoreApplication.translate( - "ComplexityChecker", "{0}: {1}"), - - ################################################################## ## Messages of the Miscellaneous Checker ################################################################## "M101": QCoreApplication.translate( @@ -798,56 +791,6 @@ "MiscellaneousChecker", "{0}: {1}"), - - ################################################################## - ## Messages of the Annotations Checker - ################################################################## - "A001": QCoreApplication.translate( - "AnnotationsChecker", - "missing type annotation for function argument '{0}'"), - "A002": QCoreApplication.translate( - "AnnotationsChecker", - "missing type annotation for '*{0}'"), - "A003": QCoreApplication.translate( - "AnnotationsChecker", - "missing type annotation for '**{0}'"), - "A101": QCoreApplication.translate( - "AnnotationsChecker", - "missing type annotation for 'self' in method"), - "A102": QCoreApplication.translate( - "AnnotationsChecker", - "missing type annotation for 'cls' in classmethod"), - "A201": QCoreApplication.translate( - "AnnotationsChecker", - "missing return type annotation for public function"), - "A202": QCoreApplication.translate( - "AnnotationsChecker", - "missing return type annotation for protected function"), - "A203": QCoreApplication.translate( - "AnnotationsChecker", - "missing return type annotation for private function"), - "A204": QCoreApplication.translate( - "AnnotationsChecker", - "missing return type annotation for special method"), - "A205": QCoreApplication.translate( - "AnnotationsChecker", - "missing return type annotation for staticmethod"), - "A206": QCoreApplication.translate( - "AnnotationsChecker", - "missing return type annotation for classmethod"), - - "A881": QCoreApplication.translate( - "AnnotationsChecker", - "type annotation coverage of {0}% is too low"), - - "A891": QCoreApplication.translate( - "AnnotationsChecker", - "type annotation is too complex ({0} > {1})"), - - "A999": QCoreApplication.translate( - "AnnotationsChecker", - "{0}: {1}"), - ################################################################## ## CodeStyleFixer messages ################################################################## @@ -1034,11 +977,6 @@ "D263": ["buttonClicked"], "D901": ["SyntaxError", "Invalid Syntax"], - "C101": ["foo.bar", "42"], - "C111": [42], - "C112": [12.0], - "C901": ["SyntaxError", "Invalid Syntax"], - "M102": ["enc42"], "M131": ["list"], "M132": ["list"], @@ -1064,24 +1002,21 @@ "M823": ["dict"], "M901": ["SyntaxError", "Invalid Syntax"], - "A001": ["arg1"], - "A002": ["args"], - "A003": ["kwargs"], - "A881": [60], - "A891": [5, 3], - "A999": ["SyntaxError", "Invalid Syntax"], - "FIXWRITE_ERROR": ["IOError"], } messageCatalogs = ( _messages, _securityMessages, + _annotationsMessages, + _complexityMessages, ) messageSampleArgsCatalog = ( _messages_sample_args, _securityMessagesSampleArgs, + _annotationsMessagesSampleArgs, + _complexityMessagesSampleArgs, )