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)