eric6/Plugins/CheckerPlugins/CodeStyleChecker/Simplify/SimplifyChecker.py

changeset 8186
655b658aa7ee
child 8189
17df5c8df8c1
diff -r c9acf46b54ce -r 655b658aa7ee eric6/Plugins/CheckerPlugins/CodeStyleChecker/Simplify/SimplifyChecker.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Simplify/SimplifyChecker.py	Wed Mar 31 19:51:41 2021 +0200
@@ -0,0 +1,249 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2021 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing the checker for simplifying Python code.
+"""
+
+import ast
+import collections
+import sys
+
+try:
+    from ast import unparse
+except AttributeError:
+    # Python < 3.9
+    from .ast_unparse import unparse
+
+
+class SimplifyChecker(object):
+    """
+    Class implementing a checker for to help simplifying Python code.
+    """
+    Codes = [
+        "Y101", 
+    ]
+    
+    def __init__(self, source, filename, selected, ignored, expected, repeat):
+        """
+        Constructor
+        
+        @param source source code to be checked
+        @type list of str
+        @param filename name of the source file
+        @type str
+        @param selected list of selected codes
+        @type list of str
+        @param ignored 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
+        """
+        self.__select = tuple(selected)
+        self.__ignore = ('',) if selected else tuple(ignored)
+        self.__expected = expected[:]
+        self.__repeat = repeat
+        self.__filename = filename
+        self.__source = source[:]
+        
+        # statistics counters
+        self.counters = {}
+        
+        # collection of detected errors
+        self.errors = []
+        
+        self.__checkCodes = (code for code in self.Codes
+                             if not self.__ignoreCode(code))
+    
+    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,
+                     'M901', exc_type.__name__, exc.args[0])
+    
+    def __generateTree(self):
+        """
+        Private method to generate an AST for our source.
+        
+        @return generated AST
+        @rtype ast.AST
+        """
+        return ast.parse("".join(self.__source), self.__filename)
+    
+    def run(self):
+        """
+        Public method to check the given source against functions
+        to be replaced by 'pathlib' equivalents.
+        """
+        if not self.__filename:
+            # don't do anything, if essential data is missing
+            return
+        
+        if not self.__checkCodes:
+            # don't do anything, if no codes were selected
+            return
+        
+        try:
+            self.__tree = self.__generateTree()
+        except (SyntaxError, TypeError):
+            self.__reportInvalidSyntax()
+            return
+        
+        visitor = SimplifyVisitor(self.__error)
+        visitor.visit(self.__tree)
+
+######################################################################
+## The following code is derived from the flake8-simplify package.
+##
+## Original License:
+##
+## MIT License
+##
+## Copyright (c) 2020 Martin Thoma
+##
+## Permission is hereby granted, free of charge, to any person obtaining a copy
+## of this software and associated documentation files (the "Software"), to
+## deal in the Software without restriction, including without limitation the
+## rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+## sell copies of the Software, and to permit persons to whom the Software is
+## furnished to do so, subject to the following conditions:
+##
+## The above copyright notice and this permission notice shall be included in
+## all copies or substantial portions of the Software.
+##
+## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+## IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+## FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+## AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+## LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+## FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+## IN THE SOFTWARE.
+######################################################################
+
+
+class SimplifyVisitor(ast.NodeVisitor):
+    """
+    Class to traverse the AST node tree and check for code that can be
+    simplified.
+    """
+    def __init__(self, errorCallback):
+        """
+        Constructor
+        
+        @param checkCallback callback function taking a reference to the
+            AST node and the resolved name
+        @type func
+        """
+        super(SimplifyVisitor, self).__init__()
+        
+        self.__error = errorCallback
+    
+    def visit_BoolOp(self, node):
+        """
+        Public method to process a BoolOp node.
+        
+        @param node reference to the BoolOp node
+        @type ast.BoolOp
+        """
+        self.__check101(node)
+    
+    #############################################################
+    ## Methods to check for possible code simplifications below
+    #############################################################
+    
+    def __getDuplicatedIsinstanceCall(self, node):
+        """
+        Private method to get a list of isinstance arguments which could
+        be combined.
+        
+        @param node reference to the AST node to be inspected
+        @type ast.BoolOp
+        """
+        counter = collections.defaultdict(int)
+        
+        for call in node.values:
+            # Ensure this is a call of the built-in isinstance() function.
+            if not isinstance(call,  ast.Call) or len(call.args) != 2:
+                continue
+            functionName = call.func.id
+            if functionName != "isinstance":
+                continue
+            
+            arg0Name = unparse(call.args[0])
+            counter[arg0Name] += 1
+        
+        return [name for name, count in counter.items() if count > 1]
+    
+    def __check101(self, node):
+        """
+        Private method to check for duplicate isinstance() calls.
+        
+        @param node reference to the AST node to be checked
+        @type ast.BoolOp
+        """
+        if not isinstance(node.op, ast.Or):
+            return
+        
+        for variable in self.__getDuplicatedIsinstanceCall(node):
+            self.__error(node.lineno - 1, node.col_offset, "Y101", variable)

eric ide

mercurial