src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Simplify/SimplifyNodeVisitor.py

branch
eric7
changeset 9277
471c5a263d53
parent 9274
86fab0c74430
child 9278
36448ca469c2
--- a/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Simplify/SimplifyNodeVisitor.py	Thu Jul 28 14:19:57 2022 +0200
+++ b/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Simplify/SimplifyNodeVisitor.py	Thu Jul 28 19:44:54 2022 +0200
@@ -11,6 +11,7 @@
 import collections
 import copy
 import itertools
+import json
 
 try:
     from ast import unparse
@@ -18,8 +19,8 @@
     # Python < 3.9
     from .ast_unparse import unparse
 
-######################################################################
-## The following code is derived from the flake8-simplify package.
+###############################################################################
+## The following code is derived from the flake8-simplify package (v0.19.2).
 ##
 ## Original License:
 ##
@@ -44,8 +45,7 @@
 ## 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.
-######################################################################
-# TODO: update to bandit v0.19.2
+###############################################################################
 
 BOOL_CONST_TYPES = (ast.Constant, ast.NameConstant)
 AST_CONST_TYPES = (ast.Constant, ast.NameConstant, ast.Str, ast.Num)
@@ -90,6 +90,8 @@
         @type ast.Assign
         """
         self.__check181(node)
+        self.__check904(node)
+        self.__check909(node)
 
         self.generic_visit(node)
 
@@ -123,6 +125,7 @@
         self.__check114(node)
         self.__check116(node)
         self.__check122(node)
+        self.__check123(node)
 
         self.generic_visit(node)
 
@@ -176,6 +179,9 @@
         self.__check182(node)
         self.__check401(node)
         self.__check402(node)
+        self.__check901(node)
+        self.__check905(node)
+        self.__check906(node)
 
         self.generic_visit(node)
 
@@ -236,6 +242,17 @@
         self.__check208(node)
 
         self.generic_visit(node)
+    
+    def visit_Subscript(self, node):
+        """
+        Public method to process a Subscript node.
+
+        @param node reference to the Subscript node
+        @type ast.Subscript
+        """
+        self.__check907(node)
+
+        self.generic_visit(node)
 
     #############################################################
     ## Helper methods for the various checkers below
@@ -266,22 +283,21 @@
 
         return [name for name, count in counter.items() if count > 1]
 
-    def __isConstantIncrease(self, expression):
+    def __isConstantIncrease(self, expr):
         """
-        Private method check the given expression for being a constant
-        increase.
-
-        @param expression reference to the expression node
+        Private method to check an expression for being a constant increase.
+        
+        @param expr reference to the node to be checked
         @type ast.AugAssign
         @return flag indicating a constant increase
         @rtype bool
         """
-        return isinstance(expression.op, ast.Add) and (
+        return isinstance(expr.op, ast.Add) and (
             (
-                isinstance(expression.value, ast.Constant)
-                and isinstance(expression.value.value, int)
+                isinstance(expr.value, ast.Constant)
+                and expr.value.value == 1
             )
-            or isinstance(expression.value, ast.Num)
+            or (isinstance(expr.value, ast.Num) and expr.value.n == 1)
         )
 
     def __getIfBodyPairs(self, node):
@@ -415,6 +431,36 @@
         newNode.ops = [op]
         return newNode
 
+    def __expressionUsesVariable(self, expr, var):
+        """
+        Private method to check, if a variable is used by an expression.
+        
+        @param expr expression node to be checked
+        @type ast.expr
+        @param var variable name to be checked for
+        @type str
+        @return flag indicating the expression uses the variable
+        @rtype bool
+        """
+        return var in unparse(expr)
+        # This is WAY too broad, but it's better to have false-negatives than
+        # false-positives.
+
+    def __bodyContainsContinue(self, stmts):
+        """
+        Private method to check, if a list of statements contain a 'continue' statement.
+        
+        @param stmts list of statements
+        @type list of ast.stmt
+        @return flag indicating a continue statement
+        @rtype bool
+        """
+        return any(
+            isinstance(stmt, ast.Continue)
+            or (isinstance(stmt, ast.If) and self.__bodyContainsContinue(stmt.body))
+            for stmt in stmts
+        )
+
     #############################################################
     ## Methods to check for possible code simplifications below
     #############################################################
@@ -514,6 +560,7 @@
             or not isinstance(node.body[0].value.value, ast.Name)
             or node.target.id != node.body[0].value.value.id
             or node.orelse != []
+            or isinstance(node.parent, ast.AsyncFunctionDef)
         ):
             iterable = unparse(node.iter)
             self.__error(node.lineno - 1, node.col_offset, "Y104", iterable)
@@ -609,7 +656,6 @@
         if (
             (tryHasReturn or exceptHasReturn)
             and finallyHasReturn
-            and finallyReturn is not None
         ):
             self.__error(finallyReturn.lineno - 1, finallyReturn.col_offset, "Y107")
 
@@ -645,7 +691,19 @@
             and node.body[0].targets[0].id == node.orelse[0].targets[0].id
             and not isinstance(node.parent, ast.If)
         ):
-            assign = unparse(node.body[0].targets[0])
+            targetVar = node.body[0].targets[0]
+            assign = unparse(targetVar)
+            
+            # It's part of a bigger if-elseif block:
+            if isinstance(node.parent, ast.If):
+                for n in node.parent.body:
+                    if (
+                        isinstance(n, ast.Assign)
+                        and isinstance(n.targets[0], ast.Name)
+                        and n.targets[0].id == targetVar.id
+                    ):
+                        return
+            
             body = unparse(node.body[0].value)
             cond = unparse(node.test)
             orelse = unparse(node.orelse[0].value)
@@ -720,6 +778,7 @@
             and isinstance(node.body[0].body[0], ast.Return)
             and isinstance(node.body[0].body[0].value, BOOL_CONST_TYPES)
             and hasattr(node.body[0].body[0].value, "value")
+            and isinstance(node.next_sibling, ast.Return)
         ):
             check = unparse(node.body[0].test)
             target = unparse(node.target)
@@ -729,9 +788,15 @@
                     node.lineno - 1, node.col_offset, "Y110", check, target, iterable
                 )
             elif node.body[0].body[0].value.value is False:
-                check = "not " + check
-                if check.startswith("not not "):
-                    check = check[len("not not ") :]
+                isCompoundExpression = " and " in check or " or " in check
+
+                if isCompoundExpression:
+                    check = f"not ({check})"
+                else:
+                    if check.startswith("not "):
+                        check = check[len("not "):]
+                    else:
+                        check = f"not {check}"
                 self.__error(
                     node.lineno - 1, node.col_offset, "Y111", check, target, iterable
                 )
@@ -826,19 +891,44 @@
         # for el in iterable:
         #     ...
         #     idx += 1
-        variableCandidates = []
-        for expression in node.body:
-            if (
-                isinstance(expression, ast.AugAssign)
-                and self.__isConstantIncrease(expression)
-                and isinstance(expression.target, ast.Name)
-            ):
-                variableCandidates.append(expression.target)
+        if not self.__bodyContainsContinue(node.body):
+            # Find variables that might just count the iteration of the current loop
+            variableCandidates = []
+            for expression in node.body:
+                if (
+                    isinstance(expression, ast.AugAssign)
+                    and self.__isConstantIncrease(expression)
+                    and isinstance(expression.target, ast.Name)
+                ):
+                    variableCandidates.append(expression.target)
+            strCandidates = [unparse(x) for x in variableCandidates]
+
+            olderSiblings = []
+            for olderSibling in node.parent.body:
+                if olderSibling is node:
+                    break
+                olderSiblings.append(olderSibling)
 
-        for candidate in variableCandidates:
-            self.__error(
-                candidate.lineno - 1, candidate.col_offset, "Y113", unparse(candidate)
-            )
+            matches = [
+                n.targets[0]
+                for n in olderSiblings
+                if isinstance(n, ast.Assign)
+                and len(n.targets) == 1
+                and isinstance(n.targets[0], ast.Name)
+                and unparse(n.targets[0]) in strCandidates
+            ]
+            if len(matches) == 0:
+                return
+
+            sibling = node.previous_sibling
+            while sibling is not None:
+                sibling = sibling.previous_sibling
+
+            for match in matches:
+                variable = unparse(match)
+                self.__error(
+                    match.lineno - 1, match.col_offset, "Y113", variable
+                )
 
     def __check114(self, node):
         """
@@ -854,7 +944,10 @@
         #     b
         ifBodyPairs = self.__getIfBodyPairs(node)
         errorPairs = []
-        for ifbody1, ifbody2 in itertools.combinations(ifBodyPairs, 2):
+        for i in range(len(ifBodyPairs) - 1):
+            # It's not all combinations because of this:
+            ifbody1 = ifBodyPairs[i]
+            ifbody2 = ifBodyPairs[i + 1]
             if self.__isSameBody(ifbody1[1], ifbody2[1]):
                 errorPairs.append((ifbody1, ifbody2))
         for ifbody1, ifbody2 in errorPairs:
@@ -925,11 +1018,14 @@
             else:
                 bodyValueStr = "None"
             if isinstance(node.test.comparators[0], ast.Str):
-                keyValuePairs = {node.test.comparators[0].s: bodyValueStr}
+                value = (
+                    bodyValueStr
+                    if bodyValueStr[0] == '"' and bodyValueStr[-1] == '"' else
+                    bodyValueStr[1:-1]
+                )
+                keyValuePairs = {node.test.comparators[0].s: value}
             elif isinstance(node.test.comparators[0], ast.Num):
-                keyValuePairs = {
-                    node.test.comparators[0].n: bodyValueStr,
-                }
+                keyValuePairs = {node.test.comparators[0].n: bodyValueStr}
             else:
                 keyValuePairs = {node.test.comparators[0].value: bodyValueStr}
             while child:
@@ -947,13 +1043,22 @@
                 ):
                     return
 
+                returnCall = child.body[0]
+                if isinstance(returnCall.value, ast.Call):
+                    return
+
                 if isinstance(child.test.comparators[0], ast.Str):
                     key = child.test.comparators[0].s
                 elif isinstance(child.test.comparators[0], ast.Num):
                     key = child.test.comparators[0].n
                 else:
                     key = child.test.comparators[0].value
-                keyValuePairs[key] = unparse(child.body[0].value).strip("'")
+
+                value = unparse(child.body[0].value)
+                if value[0] == '"' and value[-1] == '"':
+                    value = value[1:-1]
+                keyValuePairs[key] = value
+
                 if len(child.orelse) == 1:
                     if isinstance(child.orelse[0], ast.If):
                         child = child.orelse[0]
@@ -1125,6 +1230,69 @@
             dictname = unparse(node.test.comparators[0])
             self.__error(node.lineno - 1, node.col_offset, "Y122", dictname, key)
 
+    def __check123(self, node):
+        """
+        Private method to check for complicated dictionary access with default value.
+        
+        @param node reference to the AST node to be checked
+        @type ast.If
+        """
+        isPattern1 = (
+            len(node.body) == 1
+            and isinstance(node.body[0], ast.Assign)
+            and len(node.body[0].targets) == 1
+            and isinstance(node.body[0].value, ast.Subscript)
+            and len(node.orelse) == 1
+            and isinstance(node.orelse[0], ast.Assign)
+            and len(node.orelse[0].targets) == 1
+            and isinstance(node.test, ast.Compare)
+            and len(node.test.ops) == 1
+            and isinstance(node.test.ops[0], ast.In)
+        )
+
+        # just like pattern_1, but using NotIn and reversing if/else
+        isPattern2 = (
+            len(node.body) == 1
+            and isinstance(node.body[0], ast.Assign)
+            and len(node.orelse) == 1
+            and isinstance(node.orelse[0], ast.Assign)
+            and isinstance(node.orelse[0].value, ast.Subscript)
+            and isinstance(node.test, ast.Compare)
+            and len(node.test.ops) == 1
+            and isinstance(node.test.ops[0], ast.NotIn)
+        )
+
+        if isPattern1:
+            key = node.test.left
+            if unparse(key) != unparse(node.body[0].value.slice):
+                return
+            assignToIfBody = node.body[0].targets[0]
+            assignToElse = node.orelse[0].targets[0]
+            if unparse(assignToIfBody) != unparse(assignToElse):
+                return
+            dictName = node.test.comparators[0]
+            defaultValue = node.orelse[0].value
+            valueNode = node.body[0].targets[0]
+            keyStr = unparse(key)
+            dictStr = unparse(dictName)
+            defaultStr = unparse(defaultValue)
+            valueStr = unparse(valueNode)
+        elif isPattern2:
+            key = node.test.left
+            if unparse(key) != unparse(node.orelse[0].value.slice):
+                return
+            dictName = node.test.comparators[0]
+            defaultValue = node.body[0].value
+            valueNode = node.body[0].targets[0]
+            keyStr = unparse(key)
+            dictStr = unparse(dictName)
+            defaultStr = unparse(defaultValue)
+            valueStr = unparse(valueNode)
+        else:
+            return
+        self.__error(node.lineno - 1, node.col_offset, "Y123", valueStr, dictStr,
+                     keyStr, defaultStr)
+
     def __check181(self, node):
         """
         Private method to check for assignments that could be converted into
@@ -1550,6 +1718,171 @@
         if hasBareNumeric and not isException:
             self.__error(node.lineno - 1, node.col_offset, "Y402")
 
+    def __check901(self, node):
+        """
+        Private method to check for unnecessary bool conversion.
+        
+        @param node reference to the AST node to be checked
+        @type ast.Call
+        """
+        if (
+            isinstance(node.func, ast.Name)
+            and node.func.id == "bool"
+            and len(node.args) == 1
+            and isinstance(node.args[0], ast.Compare)
+        ):
+            actual = unparse(node)
+            expected = unparse(node.args[0])
+            self.__error(node.lineno - 1, node.col_offset, "Y901", expected, actual)
+
+    def __check904(self, node):
+        """
+        Private method to check for dictionary initialization.
+        
+        @param node reference to the AST node to be checked
+        @type ast.Assign
+        """
+        # a = {}; a['b'] = 'c'
+        n2 = node.next_sibling
+        if (
+            isinstance(node.value, ast.Dict)
+            and isinstance(n2, ast.Assign)
+            and len(n2.targets) == 1
+            and len(node.targets) == 1
+            and isinstance(n2.targets[0], ast.Subscript)
+            and isinstance(n2.targets[0].value, ast.Name)
+            and isinstance(node.targets[0], ast.Name)
+            and n2.targets[0].value.id == node.targets[0].id
+        ):
+            dictName = unparse(node.targets[0])
+            if not self.__expressionUsesVariable(n2.value, dictName):
+                self.__error(node.lineno - 1, node.col_offset, "Y904", dictName)
+
+    def __check905(self, node):
+        """
+        Private method to check for list initialization by splitting a string.
+        
+        @param node reference to the AST node to be checked
+        @type ast.Call
+        """
+        if (
+            isinstance(node.func, ast.Attribute)
+            and node.func.attr == "split"
+            and isinstance(node.func.value, (ast.Str, ast.Constant))
+        ):
+            if isinstance(node.func.value, ast.Constant):
+                value = node.func.value.value
+            else:
+                value = node.func.value.s
+
+            expected = json.dumps(value.split())
+            actual = unparse(node.func.value) + ".split()"
+            self.__error(node.lineno - 1, node.col_offset, "Y905", expected, actual)
+
+    def __check906(self, node):
+        """
+        Private method to check for unnecessary nesting of os.path.join().
+        
+        @param node reference to the AST node to be checked
+        @type ast.Call
+        """
+        # __IGNORE_WARNING_D234r__
+        def getOsPathJoinArgs(node):
+            names = []
+            for arg in node.args:
+                if (
+                    isinstance(arg, ast.Call)
+                    and isinstance(arg.func, ast.Attribute)
+                    and isinstance(arg.func.value, ast.Attribute)
+                    and isinstance(arg.func.value.value, ast.Name)
+                    and arg.func.value.value.id == "os"
+                    and arg.func.value.attr == "path"
+                    and arg.func.attr == "join"
+                ):
+                    names += getOsPathJoinArgs(arg)
+                elif isinstance(arg, ast.Name):
+                    names.append(arg.id)
+                elif isinstance(arg, ast.Str):
+                    names.append(f"'{arg.s}'")
+            return names
+
+        if (
+            isinstance(node.func, ast.Attribute)
+            and isinstance(node.func.value, ast.Attribute)
+            and isinstance(node.func.value.value, ast.Name)
+            and node.func.value.value.id == "os"
+            and node.func.value.attr == "path"
+            and node.func.attr == "join"
+            and len(node.args) == 2
+            and any(
+                (
+                    isinstance(arg, ast.Call)
+                    and isinstance(arg.func, ast.Attribute)
+                    and isinstance(arg.func.value, ast.Attribute)
+                    and isinstance(arg.func.value.value, ast.Name)
+                    and arg.func.value.value.id == "os"
+                    and arg.func.value.attr == "path"
+                    and arg.func.attr == "join"
+                )
+                for arg in node.args
+            )
+        ):
+            names = getOsPathJoinArgs(node)
+
+            actual = unparse(node)
+            expected = "os.path.join({0})".format(', '.join(names))
+            self.__error(node.lineno - 1, node.col_offset, "Y906", expected, actual)
+
+    def __check907(self, node):
+        """
+        Private method to check for Union type annotation with None.
+        
+        @param node reference to the AST node to be checked
+        @type ast.Subscript
+        """
+        if (isinstance(node.value, ast.Name) and node.value.id == "Union"):
+            if (
+                isinstance(node.slice, ast.Index)
+                and isinstance(node.slice.value, ast.Tuple)
+            ):
+                # Python 3.8
+                tupleVar = node.slice.value
+            elif isinstance(node.slice, ast.Tuple):
+                # Python 3.9+
+                tupleVar = node.slice
+            else:
+                return
+
+            hasNone = False
+            others = []
+            for elt in tupleVar.elts:
+                if isinstance(elt, BOOL_CONST_TYPES) and elt.value is None:
+                    hasNone = True
+                else:
+                    others.append(elt)
+
+            if len(others) == 1 and hasNone:
+                type_ = unparse(others[0])
+                self.__error(
+                    node.lineno - 1, node.col_offset, "Y907", type_, unparse(node)
+                )
+
+    def __check909(self, node):
+        """
+        Private method to check for reflexive assignments.
+        
+        @param node reference to the AST node to be checked
+        @type ast.Assign
+        """
+        names = []
+        if isinstance(node.value, (ast.Name, ast.Subscript, ast.Tuple)):
+            names.append(unparse(node.value))
+        for target in node.targets:
+            names.append(unparse(target))
+
+        if len(names) != len(set(names)) and not isinstance(node.parent, ast.ClassDef):
+            srccode = unparse(node)
+            self.__error(node.lineno - 1, node.col_offset, "Y909", srccode)
 
 #
 # eflag: noqa = M891

eric ide

mercurial