Added checks for implicit string concatenation (based on flake8-implicit-str-concat 0.4.0) to the Miscellaneous checker. eric7

Thu, 30 Nov 2023 11:59:40 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Thu, 30 Nov 2023 11:59:40 +0100
branch
eric7
changeset 10361
e6ff9a4f6ee5
parent 10360
9ffdb1490bd2
child 10362
cfa7034cccf6

Added checks for implicit string concatenation (based on flake8-implicit-str-concat 0.4.0) to the Miscellaneous checker.

docs/ThirdParty.md file | annotate | diff | comparison | revisions
eric7.epj file | annotate | diff | comparison | revisions
src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Miscellaneous/MiscellaneousChecker.py file | annotate | diff | comparison | revisions
src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Miscellaneous/translations.py file | annotate | diff | comparison | revisions
--- a/docs/ThirdParty.md	Wed Nov 29 18:07:53 2023 +0100
+++ b/docs/ThirdParty.md	Thu Nov 30 11:59:40 2023 +0100
@@ -26,6 +26,7 @@
 | flake8-bugbear                |  23.11.26 | MIT License (MIT)                  |
 | flake8-comprehensions         |   3.14.0  | MIT License (MIT)                  |
 | flake8-future-annotations     |   1.1.0   | MIT License (MIT)                  |
+| flake8-implicit-str-concat    |   0.4.0   | MIT License (MIT)                  |
 | flake8-local-import           |   1.0.6   | MIT License (MIT)                  |
 | flake8-pep585                 |   0.1.7   | Mozilla Public License Version 2.0 |
 | flake8-pep604                 |   1.1.0   | MIT License (MIT)                  |
--- a/eric7.epj	Wed Nov 29 18:07:53 2023 +0100
+++ b/eric7.epj	Thu Nov 30 11:59:40 2023 +0100
@@ -68,7 +68,7 @@
         "DocstringType": "eric_black",
         "EnabledCheckerCategories": "C, D, E, I, M, NO, N, Y, U, W",
         "ExcludeFiles": "*/ThirdParty/*, */coverage/*, */Ui_*.py, */Examples/*, */pycodestyle.py,*/pyflakes/checker.py,*/mccabe.py,*/eradicate.py,*/ast_unparse.py,*/piplicenses.py,*/pipdeptree.py,*/MCUScripts/*,*/MicroPython/Tools/*",
-        "ExcludeMessages": "C101,E203,E265,E266,E305,E402,M201,M701,M702,M811,M834,N802,N803,N807,N808,N821,W293,W503,Y119,Y401,Y402",
+        "ExcludeMessages": "C101,E203,E265,E266,E305,E402,M201,M701,M702,M811,M834,M852,N802,N803,N807,N808,N821,W293,W503,Y119,Y401,Y402",
         "FixCodes": "",
         "FixIssues": false,
         "FutureChecker": "",
--- 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):
     """
--- a/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Miscellaneous/translations.py	Wed Nov 29 18:07:53 2023 +0100
+++ b/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Miscellaneous/translations.py	Thu Nov 30 11:59:40 2023 +0100
@@ -132,10 +132,6 @@
         "Unnecessary list comprehension passed to {0}() prevents short-circuiting"
         " - rewrite as a generator"
     ),
-
-
-
-
     ## Dictionaries with sorted keys
     "M201": QCoreApplication.translate(
         "MiscellaneousChecker",
@@ -599,6 +595,19 @@
         "prefer implied line continuation inside parentheses, "
         "brackets and braces as opposed to a backslash",
     ),
+    ## implicitly concatenated strings
+    "M851": QCoreApplication.translate(
+        "MiscellaneousChecker",
+        "implicitly concatenated string or bytes literals on one line",
+    ),
+    "M852": QCoreApplication.translate(
+        "MiscellaneousChecker",
+        "implicitly concatenated string or bytes literals over continuation line",
+    ),
+    "M853": QCoreApplication.translate(
+        "MiscellaneousChecker",
+        "explicitly concatenated string or bytes should be implicitly concatenated",
+    ),
     ## commented code
     "M891": QCoreApplication.translate(
         "MiscellaneousChecker",
@@ -607,9 +616,12 @@
 }
 
 _miscellaneousMessagesSampleArgs = {
+    ## Coding line
     "M102": ["enc42"],
+    ## Shadowed Builtins
     "M131": ["list"],
     "M132": ["list"],
+    ## Comprehensions
     "M185": ["list", "set"],
     "M186": ["list", "dict"],
     "M188": ["list"],
@@ -626,7 +638,9 @@
     "M197": ["list"],
     "M198": ["dict comprehension"],
     "M199": ["any"],
+    ## Dictionaries with sorted keys
     "M201": ["bar", "foo"],
+    ## Property
     "M210": [2],
     "M211": [1],
     "M212": [2],
@@ -635,6 +649,7 @@
     "M215": ["foo", "bar"],
     "M216": ["foo", "bar"],
     "M217": ["foo"],
+    ## Bugbear
     "M507": ["x"],
     "M513": ["Exception"],
     "M514": ["OSError, IOError", " as err", "OSError"],
@@ -645,14 +660,18 @@
     "M533": ["foo"],
     "M534": ["split", "maxsplit"],
     "M535": ["foo"],
+    ## Format Strings
     "M601": ["%s"],
     "M621": [5],
     "M622": ["foo"],
     "M631": [5],
     "M632": ["foo"],
+    ## Future statements
     "M701": ["print_function, unicode_literals", "print_function"],
     "M702": ["print_function, unicode_literals"],
+    ## Gettext
     "M711": ["lgettext"],
+    ## Mutable Defaults
     "M821": ["Dict"],
     "M822": ["Call"],
     "M823": ["dict"],

eric ide

mercurial