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

Thu, 01 Apr 2021 17:23:35 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Thu, 01 Apr 2021 17:23:35 +0200
changeset 8188
bfa6c0969acf
parent 8186
655b658aa7ee
child 8189
17df5c8df8c1
permissions
-rw-r--r--

Code Style Checker
- added icons for the new simplification checks

# -*- 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