src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Miscellaneous/MiscellaneousChecker.py

branch
eric7
changeset 10361
e6ff9a4f6ee5
parent 10360
9ffdb1490bd2
child 10362
cfa7034cccf6
--- a/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Miscellaneous/MiscellaneousChecker.py	Wed Nov 29 18:07:53 2023 +0100
+++ b/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Miscellaneous/MiscellaneousChecker.py	Thu Nov 30 11:59:40 2023 +0100
@@ -13,12 +13,26 @@
 import copy
 import itertools
 import re
+import sys
 import tokenize
 
 from collections import defaultdict, namedtuple
 from keyword import iskeyword
 from string import Formatter
 
+try:
+    # Python 3.10+
+    from itertools import pairwise
+except ImportError:
+    # replacement for Python < 3.10
+    from itertools import tee
+
+    def pairwise(iterable):
+        a, b = tee(iterable)
+        next(b, None)
+        return zip(a, b)
+
+
 import AstUtilities
 
 from .eradicate import Eradicator
@@ -64,15 +78,24 @@
         "M184",
         "M185",
         "M186",
-        "M187",
         "M188",
         "M189",
+        "M189a",
+        "M189b",
         "M190",
+        "M190a",
+        "M190b",
         "M191",
-        "M192",
         "M193",
+        "M193a",
+        "M193b",
+        "M193c",
         "M194",
         "M195",
+        "M196",
+        "M197",
+        "M198",
+        "M199",
         ## Dictionaries with sorted keys
         "M201",
         ## Property
@@ -184,6 +207,10 @@
         "M834",
         ## line continuation
         "M841",
+        ## implicitly concatenated strings
+        "M851",
+        "M852",
+        "M853",
         ## commented code
         "M891",
     ]
@@ -227,6 +254,9 @@
         self.__tree = copy.deepcopy(tree)
         self.__args = args
 
+        linesIterator = iter(self.__source)
+        self.__tokens = list(tokenize.generate_tokens(lambda: next(linesIterator)))
+
         self.__pep3101FormatRegex = re.compile(
             r'^(?:[^\'"]*[\'"][^\'"]*[\'"])*\s*%|^\s*%'
         )
@@ -255,15 +285,24 @@
                     "M184",
                     "M185",
                     "M186",
-                    "M187",
                     "M188",
                     "M189",
+                    "M189a",
+                    "M189b",
                     "M190",
+                    "M190a",
+                    "M190b",
                     "M191",
-                    "M192",
                     "M193",
+                    "M193a",
+                    "M193b",
+                    "M193c",
                     "M194",
                     "M195",
+                    "M196",
+                    "M197",
+                    "M198",
+                    "M199",
                 ),
             ),
             (self.__checkDictWithSortedKeys, ("M201",)),
@@ -369,6 +408,8 @@
             (self.__checkMutableDefault, ("M821", "M822")),
             (self.__checkReturn, ("M831", "M832", "M833", "M834")),
             (self.__checkLineContinuation, ("M841",)),
+            (self.__checkImplicitStringConcat, ("M851", "M852")),
+            (self.__checkExplicitStringConcat, ("M853",)),
             (self.__checkCommentedCode, ("M891",)),
         ]
 
@@ -548,9 +589,7 @@
         Private method to check line continuation using backslash.
         """
         # generate source lines without comments
-        linesIterator = iter(self.__source)
-        tokens = tokenize.generate_tokens(lambda: next(linesIterator))
-        comments = [token for token in tokens if token[0] == tokenize.COMMENT]
+        comments = [tok for tok in self.__tokens if tok[0] == tokenize.COMMENT]
         stripped = self.__source[:]
         for comment in comments:
             lineno = comment[3][0]
@@ -908,7 +947,6 @@
                         "tuple": "M189a",
                         "list": "M190a",
                     }[node.func.id]
-                    ##suffix = "remove the outer call to {func}()."
                     self.__error(
                         node.lineno - 1,
                         node.col_offset,
@@ -948,7 +986,6 @@
                         )
                     )
                 ):
-                    ##suffix = "rewrite as a {func} literal."
                     errorCode = {
                         "tuple": "M189b",
                         "list": "M190b",
@@ -1010,12 +1047,6 @@
                                 node.args[0].func.id,
                                 not reverseFlagValue,
                             )
-                        ##if reverse_flag_value is None:
-                            ##remediation = " - toggle reverse argument to sorted()"
-                        ##else:
-                            ##remediation = " - use sorted(..., reverse={!r})".format(
-                                ##not reverse_flag_value
-                            ##)
 
                     else:
                         self.__error(
@@ -1071,7 +1102,9 @@
                     and len(node.args) == 2
                     and isinstance(node.args[0], ast.Lambda)
                 ):
-                    self.__error(node.lineno - 1, node.col_offset, "M197", "generator expression")
+                    self.__error(
+                        node.lineno - 1, node.col_offset, "M197", "generator expression"
+                    )
 
                 elif (
                     node.func.id in ("list", "set", "dict")
@@ -1099,7 +1132,9 @@
 
                     if rewriteable:
                         comprehensionType = f"{node.func.id} comprehension"
-                        self.__error(node.lineno - 1, node.col_offset, "M197", comprehensionType)
+                        self.__error(
+                            node.lineno - 1, node.col_offset, "M197", comprehensionType
+                        )
 
                 elif isinstance(node, (ast.DictComp, ast.ListComp, ast.SetComp)) and (
                     len(node.generators) == 1
@@ -1401,6 +1436,94 @@
                 if propertyCount > 1:
                     self.__error(node.lineno - 1, node.col_offset, "M217", node.name)
 
+    #######################################################################
+    ## The following method check for implicitly concatenated strings
+    ##
+    ## These methods are adapted from: flake8-implicit-str-concat v0.4.0
+    ## Original: Copyright (c) 2023 Dylan Turner
+    #######################################################################
+
+    if sys.version_info < (3, 12):
+        def __isImplicitStringConcat(self, first, second):
+            """
+            Private method to check, if the given strings indicate an implicit string
+            concatenation.
+
+            @param first first token
+            @type tuple
+            @param second second token
+            @type tuple
+            @return flag indicating an implicit string concatenation
+            """
+            return first.type == second.type == tokenize.STRING
+
+    else:
+
+        def __isImplicitStringConcat(self, first, second):
+            """
+            Private method to check, if the given strings indicate an implicit string
+            concatenation.
+
+            @param first first token
+            @type tuple
+            @param second second token
+            @type tuple
+            @return flag indicating an implicit string concatenation
+            """
+            return (
+                (first.type == second.type == tokenize.STRING)
+                or (
+                    first.type == tokenize.STRING
+                    and second.type == tokenize.FSTRING_START
+                )
+                or (
+                    first.type == tokenize.FSTRING_END
+                    and second.type == tokenize.STRING
+                )
+                or (
+                    first.type == tokenize.FSTRING_END
+                    and second.type == tokenize.FSTRING_START
+                )
+            )
+
+    def __checkImplicitStringConcat(self):
+        """
+        Private method to check for implicitly concatenated strings.
+        """
+        tokensWithoutWhitespace = (
+            tok
+            for tok in self.__tokens
+            if tok.type
+            not in (
+                tokenize.NL,
+                tokenize.NEWLINE,
+                tokenize.INDENT,
+                tokenize.DEDENT,
+                tokenize.COMMENT,
+            )
+        )
+        for a, b in pairwise(tokensWithoutWhitespace):
+            if self.__isImplicitStringConcat(a, b):
+                self.__error(
+                    a.end[0] - 1, a.end[1], "M851" if a.end[0] == b.start[0] else "M852"
+                )
+
+    def __checkExplicitStringConcat(self):
+        """
+        Private method to check for explicitly concatenated strings.
+        """
+        for node in ast.walk(self.__tree):
+            if (
+                isinstance(node, ast.BinOp)
+                and isinstance(node.op, ast.Add)
+                and all(
+                    AstUtilities.isBaseString(operand)
+                    or isinstance(operand, ast.JoinedStr)
+                    for operand in (node.left, node.right)
+                )
+            ):
+                self.__error(node.lineno - 1, node.col_offset, "M853")
+
 
 class TextVisitor(ast.NodeVisitor):
     """

eric ide

mercurial