--- 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): """