eric6/Plugins/CheckerPlugins/CodeStyleChecker/PathLib/PathlibChecker.py

changeset 8166
bd5cd5858503
child 8198
1c765dc90c21
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/PathLib/PathlibChecker.py	Sun Mar 21 14:17:16 2021 +0100
@@ -0,0 +1,317 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2021 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing the checker for functions that can be replaced by use of
+the pathlib module.
+"""
+
+import ast
+import sys
+
+
+class PathlibChecker(object):
+    """
+    Class implementing a checker for functions that can be replaced by use of
+    the pathlib module.
+    """
+    Codes = [
+        ## Replacements for the os module functions
+        "P101", "P102", "P103", "P104", "P105", "P106", "P107",
+        "P108", "P109", "P110", "P111",
+        
+        ## Replacements for the os.path module functions
+        "P201", "P202", "P203", "P204", "P205", "P206", "P207",
+        "P208", "P209", "P210", "P211", "P212",
+        
+        ## Replacements for some Python standrd library functions
+        "P301",
+        
+        ## Replacements for py.path.local
+        "P401",
+    ]
+    
+    # map functions to be replaced to error codes
+    Function2Code = {
+        "os.chmod": "P101",
+        "os.mkdir": "P102",
+        "os.makedirs": "P103",
+        "os.rename": "P104",
+        "os.replace": "P105",
+        "os.rmdir": "P106",
+        "os.remove": "P107",
+        "os.unlink": "P108",
+        "os.getcwd": "P109",
+        "os.readlink": "P110",
+        "os.stat": "P111",
+        
+        "os.path.abspath": "P201",
+        "os.path.exists": "P202",
+        "os.path.expanduser": "P203",
+        "os.path.isdir": "P204",
+        "os.path.isfile": "P205",
+        "os.path.islink": "P206",
+        "os.path.isabs": "P207",
+        "os.path.join": "P208",
+        "os.path.basename": "P209",
+        "os.path.dirname": "P210",
+        "os.path.samefile": "P211",
+        "os.path.splitext": "P212",
+        
+        "open": "P301",
+        
+        "py.path.local": "P401",
+    }
+    
+    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 = PathlibVisitor(self.__checkForReplacement)
+        visitor.visit(self.__tree)
+    
+    def __checkForReplacement(self, node, name):
+        """
+        Private method to check the given node for the need for a
+        replacement.
+        
+        @param node reference to the AST node to check
+        @type ast.AST
+        @param name resolved name of the node
+        @type str
+        """
+        try:
+            errorCode = self.Function2Code[name]
+            self.__error(node.lineno - 1, node.col_offset, errorCode)
+        except KeyError:
+            # name is not in our list of replacements
+            pass
+
+
+class PathlibVisitor(ast.NodeVisitor):
+    """
+    Class to traverse the AST node tree and check for potential issues.
+    """
+    def __init__(self, checkCallback):
+        """
+        Constructor
+        
+        @param checkCallback callback function taking a reference to the
+            AST node and the resolved name
+        @type func
+        """
+        super(PathlibVisitor, self).__init__()
+        
+        self.__checkCallback = checkCallback
+        self.__importAlias = {}
+    
+    def visit_ImportFrom(self, node):
+        """
+        Public method handle the ImportFrom AST node.
+        
+        @param node reference to the ImportFrom AST node
+        @type ast.ImportFrom
+        """
+        for imp in node.names:
+            if imp.asname:
+                self.__importAlias[imp.asname] = f"{node.module}.{imp.name}"
+            else:
+                self.__importAlias[imp.name] = f"{node.module}.{imp.name}"
+
+    def visit_Import(self, node):
+        """
+        Public method to handle the Import AST node.
+        
+        @param node reference to the Import AST node
+        @type ast.Import
+        """
+        for imp in node.names:
+            if imp.asname:
+                self.__importAlias[imp.asname] = imp.name
+
+    def visit_Call(self, node):
+        """
+        Public method to handle the Call AST node.
+        
+        @param node reference to the Call AST node
+        @type ast.Call
+        """
+        nameResolver = NameResolver(self.__importAlias)
+        nameResolver.visit(node.func)
+        
+        self.__checkCallback(node, nameResolver.name())
+
+
+class NameResolver(ast.NodeVisitor):
+    """
+    Class to resolve a Name or Attribute node.
+    """
+    def __init__(self, importAlias):
+        """
+        Constructor
+        
+        @param importAlias reference to the import aliases dictionary
+        @type dict
+        """
+        self.__importAlias = importAlias
+        self.__names = []
+    
+    def name(self):
+        """
+        Public method to resolve the name.
+        
+        @return resolved name
+        @rtype str
+        """
+        try:
+            attr = self.__importAlias[self.__names[-1]]
+            self.__names[-1] = attr
+        except (KeyError, IndexError):
+            # do nothing if there is no such name or the names list is empty
+            pass
+        
+        return ".".join(reversed(self.__names))
+    
+    def visit_Name(self, node):
+        """
+        Public method to handle the Name AST node.
+        
+        @param node reference to the Name AST node
+        @type ast.Name
+        """
+        self.__names.append(node.id)
+    
+    def visit_Attribute(self, node):
+        """
+        Public method to handle the Attribute AST node.
+        
+        @param node reference to the Attribute AST node
+        @type ast.Attribute
+        """
+        try:
+            self.__names.append(node.attr)
+            self.__names.append(node.value.id)
+        except AttributeError:
+            self.generic_visit(node)

eric ide

mercurial