diff options
author | Lucas Cimon <lucas.cimon@gmail.com> | 2018-10-10 19:56:25 +0200 |
---|---|---|
committer | Claudiu Popa <pcmanticore@gmail.com> | 2018-10-10 20:56:25 +0300 |
commit | fcc01516ae176ad3fdedc4497328105f3314e376 (patch) | |
tree | ff912ba3ca5640e5f782a62d3a3765754ae5ced4 | |
parent | 04cf3f8298fa1c718f950c90c34ee99a17f3c503 (diff) | |
download | pylint-git-fcc01516ae176ad3fdedc4497328105f3314e376.tar.gz |
Adding implicit-str-concat-in-sequence check (#1655)
``implicit-str-concat-in-sequence`` detects string concatenation inside lists, sets & tuples.
It would warn on code such as `('a', 'b' 'c')`.
-rw-r--r-- | CONTRIBUTORS.txt | 2 | ||||
-rw-r--r-- | ChangeLog | 2 | ||||
-rw-r--r-- | doc/whatsnew/2.2.rst | 9 | ||||
-rw-r--r-- | pylint/checkers/base.py | 18 | ||||
-rw-r--r-- | pylint/checkers/classes.py | 18 | ||||
-rw-r--r-- | pylint/checkers/design_analysis.py | 2 | ||||
-rw-r--r-- | pylint/checkers/exceptions.py | 6 | ||||
-rw-r--r-- | pylint/checkers/format.py | 2 | ||||
-rw-r--r-- | pylint/checkers/imports.py | 8 | ||||
-rw-r--r-- | pylint/checkers/misc.py | 2 | ||||
-rw-r--r-- | pylint/checkers/newstyle.py | 2 | ||||
-rw-r--r-- | pylint/checkers/python3.py | 10 | ||||
-rw-r--r-- | pylint/checkers/refactoring.py | 8 | ||||
-rw-r--r-- | pylint/checkers/stdlib.py | 4 | ||||
-rw-r--r-- | pylint/checkers/strings.py | 77 | ||||
-rw-r--r-- | pylint/checkers/typecheck.py | 2 | ||||
-rw-r--r-- | pylint/checkers/variables.py | 6 | ||||
-rw-r--r-- | pylint/lint.py | 2 | ||||
-rw-r--r-- | pylint/test/functional/bad_continuation.py | 2 | ||||
-rw-r--r-- | pylint/test/functional/implicit_str_concat_in_sequence.py | 26 | ||||
-rw-r--r-- | pylint/test/functional/implicit_str_concat_in_sequence.txt | 4 | ||||
-rw-r--r-- | pylint/test/unittest_checker_strings.py | 26 |
22 files changed, 186 insertions, 52 deletions
diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index eea7b2c6d..5d08fd307 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -243,3 +243,5 @@ contributors: * Scott Worley: contributor * Michael Hudson-Doyle + +* Lucas Cimon: contributor @@ -167,6 +167,8 @@ Release date: TBA Close #1422 Close #2019 + * Add a new check 'implicit-str-concat-in-sequence' to spot string concatenation inside lists, sets & tuples. + What's New in Pylint 2.1.1? =========================== diff --git a/doc/whatsnew/2.2.rst b/doc/whatsnew/2.2.rst index e001dd3d4..8812fc20c 100644 --- a/doc/whatsnew/2.2.rst +++ b/doc/whatsnew/2.2.rst @@ -20,6 +20,15 @@ New checkers * ``logging-format-style`` is a new option for the logging checker for usage of str.format() style format strings in calls to loggers. +* ``implicit-str-concat-in-sequence`` detects string concatenation inside lists, sets & tuples. + + Example of code that would generate such warning: + + .. code-block:: python + + woops = ('a', 'b' 'c') + + Other Changes ============= diff --git a/pylint/checkers/base.py b/pylint/checkers/base.py index 52be3c3f8..4a08f9325 100644 --- a/pylint/checkers/base.py +++ b/pylint/checkers/base.py @@ -448,12 +448,12 @@ class BasicErrorChecker(_BasicChecker): "E0104": ( "Return outside function", "return-outside-function", - 'Used when a "return" statement is found outside a function or ' "method.", + 'Used when a "return" statement is found outside a function or method.', ), "E0105": ( "Yield outside function", "yield-outside-function", - 'Used when a "yield" statement is found outside a function or ' "method.", + 'Used when a "yield" statement is found outside a function or method.', ), "E0106": ( "Return with argument inside generator", @@ -472,7 +472,7 @@ class BasicErrorChecker(_BasicChecker): "E0108": ( "Duplicate argument name %s in function definition", "duplicate-argument-name", - "Duplicate argument names in function definitions are syntax" " errors.", + "Duplicate argument names in function definitions are syntax errors.", ), "E0110": ( "Abstract class %r with abstract methods instantiated", @@ -496,12 +496,12 @@ class BasicErrorChecker(_BasicChecker): "E0113": ( "Starred assignment target must be in a list or tuple", "invalid-star-assignment-target", - "Emitted when a star expression is used as a starred " "assignment target.", + "Emitted when a star expression is used as a starred assignment target.", ), "E0114": ( "Can use starred expression only in assignment target", "star-needs-assignment-target", - "Emitted when a star expression is not used in an " "assignment target.", + "Emitted when a star expression is not used in an assignment target.", ), "E0115": ( "Name %r is nonlocal and global", @@ -896,7 +896,7 @@ class BasicChecker(_BasicChecker): "W0104": ( "Statement seems to have no effect", "pointless-statement", - "Used when a statement doesn't have (or at least seems to) " "any effect.", + "Used when a statement doesn't have (or at least seems to) any effect.", ), "W0105": ( "String statement has no effect", @@ -923,7 +923,7 @@ class BasicChecker(_BasicChecker): "W0109": ( "Duplicate key %r in dictionary", "duplicate-key", - "Used when a dictionary expression binds the same key multiple " "times.", + "Used when a dictionary expression binds the same key multiple times.", ), "W0122": ( "Use of exec", @@ -1533,7 +1533,7 @@ class NameChecker(_BasicChecker): "C0102": ( 'Black listed name "%s"', "blacklisted-name", - "Used when the name is listed in the black list (unauthorized " "names).", + "Used when the name is listed in the black list (unauthorized names).", ), "C0103": ( '%s name "%s" doesn\'t conform to %s', @@ -1975,7 +1975,7 @@ class PassChecker(_BasicChecker): "W0107": ( "Unnecessary pass statement", "unnecessary-pass", - 'Used when a "pass" statement that can be avoided is ' "encountered.", + 'Used when a "pass" statement that can be avoided is encountered.', ) } diff --git a/pylint/checkers/classes.py b/pylint/checkers/classes.py index c386d70e6..b0a85906b 100644 --- a/pylint/checkers/classes.py +++ b/pylint/checkers/classes.py @@ -459,12 +459,12 @@ MSGS = { "E0203": ( "Access to member %r before its definition line %s", "access-member-before-definition", - "Used when an instance member is accessed before it's actually " "assigned.", + "Used when an instance member is accessed before it's actually assigned.", ), "W0201": ( "Attribute %r defined outside __init__", "attribute-defined-outside-init", - "Used when an instance attribute is defined outside the __init__ " "method.", + "Used when an instance attribute is defined outside the __init__ method.", ), "W0212": ( "Access to a protected member %s of a client class", # E0214 @@ -550,7 +550,7 @@ MSGS = { "W0232": ( "Class has no __init__ method", "no-init", - "Used when a class has no __init__ method, neither its parent " "classes.", + "Used when a class has no __init__ method, neither its parent classes.", ), "W0233": ( "__init__ method from a non direct base class %r is called", @@ -566,14 +566,14 @@ MSGS = { "from the MRO.", ), "E0236": ( - "Invalid object %r in __slots__, must contain " "only non empty strings", + "Invalid object %r in __slots__, must contain only non empty strings", "invalid-slots-object", "Used when an invalid (non-string) object occurs in __slots__.", ), "E0237": ( "Assigning to attribute %r not defined in class slots", "assigning-non-slot", - "Used when assigning to an attribute not defined " "in the class slots.", + "Used when assigning to an attribute not defined in the class slots.", ), "E0238": ( "Invalid __slots__ object", @@ -584,7 +584,7 @@ MSGS = { "E0239": ( "Inheriting %r, which is not a class.", "inherit-non-class", - "Used when a class inherits from something which is not a " "class.", + "Used when a class inherits from something which is not a class.", ), "E0240": ( "Inconsistent method resolution order for class %r", @@ -599,17 +599,17 @@ MSGS = { "R0202": ( "Consider using a decorator instead of calling classmethod", "no-classmethod-decorator", - "Used when a class method is defined without using the decorator " "syntax.", + "Used when a class method is defined without using the decorator syntax.", ), "R0203": ( "Consider using a decorator instead of calling staticmethod", "no-staticmethod-decorator", - "Used when a static method is defined without using the decorator " "syntax.", + "Used when a static method is defined without using the decorator syntax.", ), "C0205": ( "Class __slots__ should be a non-string iterable", "single-string-used-for-slots", - "Used when a class __slots__ is a simple string, rather " "than an iterable.", + "Used when a class __slots__ is a simple string, rather than an iterable.", ), "R0205": ( "Class %r inherits from object, can be safely removed from bases in python3", diff --git a/pylint/checkers/design_analysis.py b/pylint/checkers/design_analysis.py index 0c1a21cb9..a0c48b978 100644 --- a/pylint/checkers/design_analysis.py +++ b/pylint/checkers/design_analysis.py @@ -86,7 +86,7 @@ MSGS = { "R0916": ( "Too many boolean expressions in if statement (%s/%s)", "too-many-boolean-expressions", - "Used when an if statement contains too many boolean " "expressions.", + "Used when an if statement contains too many boolean expressions.", ), } SPECIAL_OBJ = re.compile("^_{2}[a-z]+_{2}$") diff --git a/pylint/checkers/exceptions.py b/pylint/checkers/exceptions.py index 4eb65d923..e0a449ca8 100644 --- a/pylint/checkers/exceptions.py +++ b/pylint/checkers/exceptions.py @@ -88,7 +88,7 @@ MSGS = { "string is raised (i.e. a `TypeError` will be raised).", ), "E0703": ( - "Exception context set to something which is not an " "exception, nor None", + "Exception context set to something which is not an exception, nor None", "bad-exception-context", 'Used when using the syntax "raise ... from ...", ' "where the exception context is not an exception, " @@ -113,7 +113,7 @@ MSGS = { "E0711": ( "NotImplemented raised - should raise NotImplementedError", "notimplemented-raised", - "Used when NotImplemented is raised instead of " "NotImplementedError", + "Used when NotImplemented is raised instead of NotImplementedError", ), "E0712": ( "Catching an exception which doesn't inherit from Exception: %s", @@ -124,7 +124,7 @@ MSGS = { "W0702": ( "No exception type(s) specified", "bare-except", - "Used when an except clause doesn't specify exceptions type to " "catch.", + "Used when an except clause doesn't specify exceptions type to catch.", ), "W0703": ( "Catching too general exception %s", diff --git a/pylint/checkers/format.py b/pylint/checkers/format.py index 9e353e6d5..b5f35cd00 100644 --- a/pylint/checkers/format.py +++ b/pylint/checkers/format.py @@ -138,7 +138,7 @@ MSGS = { "C0303": ( "Trailing whitespace", "trailing-whitespace", - "Used when there is whitespace between the end of a line and the " "newline.", + "Used when there is whitespace between the end of a line and the newline.", ), "C0304": ( "Final newline missing", diff --git a/pylint/checkers/imports.py b/pylint/checkers/imports.py index 77ff79a60..e2d00eb76 100644 --- a/pylint/checkers/imports.py +++ b/pylint/checkers/imports.py @@ -211,7 +211,7 @@ MSGS = { "R0401": ( "Cyclic import (%s)", "cyclic-import", - "Used when a cyclic import between two or more modules is " "detected.", + "Used when a cyclic import between two or more modules is detected.", ), "W0401": ( "Wildcard import %s", @@ -226,7 +226,7 @@ MSGS = { "W0403": ( "Relative import %r, should be %r", "relative-import", - "Used when an import relative to the package directory is " "detected.", + "Used when an import relative to the package directory is detected.", {"maxversion": (3, 0)}, ), "W0404": ( @@ -248,7 +248,7 @@ MSGS = { "C0410": ( "Multiple imports on one line (%s)", "multiple-imports", - "Used when import statement importing multiple modules is " "detected.", + "Used when import statement importing multiple modules is detected.", ), "C0411": ( "%s should be placed before %s", @@ -262,7 +262,7 @@ MSGS = { "Used when imports are not grouped by packages", ), "C0413": ( - 'Import "%s" should be placed at the top of the ' "module", + 'Import "%s" should be placed at the top of the module', "wrong-import-position", "Used when code and imports are mixed", ), diff --git a/pylint/checkers/misc.py b/pylint/checkers/misc.py index 767da45ab..b0f52ca8b 100644 --- a/pylint/checkers/misc.py +++ b/pylint/checkers/misc.py @@ -80,7 +80,7 @@ class EncodingChecker(BaseChecker): "Used when a warning note as FIXME or XXX is detected.", ), "W0512": ( - 'Cannot decode using encoding "%s",' " unexpected byte at position %d", + 'Cannot decode using encoding "%s", unexpected byte at position %d', "invalid-encoded-data", "Used when a source line cannot be decoded using the specified " "source file encoding.", diff --git a/pylint/checkers/newstyle.py b/pylint/checkers/newstyle.py index 0da6a5228..89d31653b 100644 --- a/pylint/checkers/newstyle.py +++ b/pylint/checkers/newstyle.py @@ -31,7 +31,7 @@ MSGS = { "E1004": ( "Missing argument to super()", "missing-super-argument", - "Used when the super builtin didn't receive an " "argument.", + "Used when the super builtin didn't receive an argument.", {"maxversion": (3, 0)}, ), } diff --git a/pylint/checkers/python3.py b/pylint/checkers/python3.py index 34be179f0..44ebf2991 100644 --- a/pylint/checkers/python3.py +++ b/pylint/checkers/python3.py @@ -179,7 +179,7 @@ class Python3Checker(checkers.BaseChecker): "(Python 3 doesn't allow it)", ), "E1603": ( - "Implicit unpacking of exceptions is not supported " "in Python 3", + "Implicit unpacking of exceptions is not supported in Python 3", "unpacking-in-except", "Python3 will not allow implicit unpacking of " "exceptions in except clauses. " @@ -187,7 +187,7 @@ class Python3Checker(checkers.BaseChecker): {"old_names": [("W0712", "unpacking-in-except")]}, ), "E1604": ( - "Use raise ErrorClass(args) instead of " "raise ErrorClass, args.", + "Use raise ErrorClass(args) instead of raise ErrorClass, args.", "old-raise-syntax", "Used when the alternate raise syntax " "'raise foo, bar' is used " @@ -378,7 +378,7 @@ class Python3Checker(checkers.BaseChecker): "W1628": ( "__hex__ method defined", "hex-method", - "Used when a __hex__ method is defined " "(method is not used by Python 3)", + "Used when a __hex__ method is defined (method is not used by Python 3)", ), "W1629": ( "__nonzero__ method defined", @@ -389,7 +389,7 @@ class Python3Checker(checkers.BaseChecker): "W1630": ( "__cmp__ method defined", "cmp-method", - "Used when a __cmp__ method is defined " "(method is not used by Python 3)", + "Used when a __cmp__ method is defined (method is not used by Python 3)", ), # 'W1631': replaced by W1636 "W1632": ( @@ -413,7 +413,7 @@ class Python3Checker(checkers.BaseChecker): "W1635": ( "unichr built-in referenced", "unichr-builtin", - "Used when the unichr built-in is referenced " "(Use chr in Python 3)", + "Used when the unichr built-in is referenced (Use chr in Python 3)", ), "W1636": ( "map built-in referenced when not iterating", diff --git a/pylint/checkers/refactoring.py b/pylint/checkers/refactoring.py index e8f35ca5c..47a702587 100644 --- a/pylint/checkers/refactoring.py +++ b/pylint/checkers/refactoring.py @@ -96,7 +96,7 @@ class RefactoringChecker(checkers.BaseTokenChecker): "R1703": ( "The if statement can be replaced with %s", "simplifiable-if-statement", - "Used when an if statement can be replaced with " "'bool(test)'. ", + "Used when an if statement can be replaced with 'bool(test)'. ", {"old_names": [("R0102", "simplifiable-if-statement")]}, ), "R1704": ( @@ -203,7 +203,7 @@ class RefactoringChecker(checkers.BaseTokenChecker): "R1719": ( "The if expression can be replaced with %s", "simplifiable-if-expression", - "Used when an if expression can be replaced with " "'bool(test)'. ", + "Used when an if expression can be replaced with 'bool(test)'. ", ), } options = ( @@ -213,7 +213,7 @@ class RefactoringChecker(checkers.BaseTokenChecker): "default": 5, "type": "int", "metavar": "<int>", - "help": "Maximum number of nested blocks for function / " "method body", + "help": "Maximum number of nested blocks for function / method body", }, ), ( @@ -1155,7 +1155,7 @@ class NotChecker(checkers.BaseChecker): "C0113": ( 'Consider changing "%s" to "%s"', "unneeded-not", - "Used when a boolean expression contains an unneeded " "negation.", + "Used when a boolean expression contains an unneeded negation.", ) } name = "basic" diff --git a/pylint/checkers/stdlib.py b/pylint/checkers/stdlib.py index 57692b93d..7dad31d28 100644 --- a/pylint/checkers/stdlib.py +++ b/pylint/checkers/stdlib.py @@ -97,7 +97,7 @@ class StdlibChecker(BaseChecker): {"maxversion": (3, 5)}, ), "W1503": ( - "Redundant use of %s with constant " "value %r", + "Redundant use of %s with constant value %r", "redundant-unittest-assert", "The first argument of assertTrue and assertFalse is " "a condition. If a constant is passed as parameter, that " @@ -119,7 +119,7 @@ class StdlibChecker(BaseChecker): "By default, the first parameter is the group param, not the target param. ", ), "W1507": ( - "Using copy.copy(os.environ). Use os.environ.copy() " "instead. ", + "Using copy.copy(os.environ). Use os.environ.copy() instead. ", "shallow-copy-environ", "os.environ is not a dict object but proxy object, so " "shallow copy has still effects on original object. " diff --git a/pylint/checkers/strings.py b/pylint/checkers/strings.py index 0f7123a71..d1f227133 100644 --- a/pylint/checkers/strings.py +++ b/pylint/checkers/strings.py @@ -30,11 +30,13 @@ from collections import Counter import astroid from astroid.arguments import CallSite +from astroid.node_classes import Const from pylint.interfaces import ITokenChecker, IAstroidChecker, IRawChecker from pylint.checkers import BaseChecker, BaseTokenChecker from pylint.checkers import utils from pylint.checkers.utils import check_messages +_AST_NODE_STR_TYPES = ("__builtin__.unicode", "__builtin__.str", "builtins.str") _PY3K = sys.version_info[:2] >= (3, 0) _PY27 = sys.version_info[:2] == (2, 7) @@ -43,7 +45,7 @@ MSGS = { "E1300": ( "Unsupported format character %r (%#02x) at index %d", "bad-format-character", - "Used when an unsupported format character is used in a format" "string.", + "Used when an unsupported format character is used in a format string.", ), "E1301": ( "Format string ends in middle of conversion specifier", @@ -106,7 +108,7 @@ MSGS = { "E1310": ( "Suspicious argument in %s.%s call", "bad-str-strip-call", - "The argument to a str.{l,r,}strip call contains a" " duplicate character, ", + "The argument to a str.{l,r,}strip call contains a duplicate character, ", ), "W1302": ( "Invalid format string", @@ -546,14 +548,14 @@ class StringFormatChecker(BaseChecker): class StringConstantChecker(BaseTokenChecker): """Check string literals""" - __implements__ = (ITokenChecker, IRawChecker) + __implements__ = (IAstroidChecker, ITokenChecker, IRawChecker) name = "string_constant" msgs = { "W1401": ( "Anomalous backslash in string: '%s'. " "String constant might be missing an r prefix.", "anomalous-backslash-in-string", - "Used when a backslash is in a literal string but not as an " "escape.", + "Used when a backslash is in a literal string but not as an escape.", ), "W1402": ( "Anomalous Unicode escape in byte string: '%s'. " @@ -562,6 +564,13 @@ class StringConstantChecker(BaseTokenChecker): "Used when an escape like \\u is encountered in a byte " "string where it has no effect.", ), + "W1403": ( + "Implicit string concatenation found in %s", + "implicit-str-concat-in-sequence", + "String literals are implicitly concatenated in a " + "literal iterable definition : " + "maybe a comma is missing ?", + ), } # Characters that have a special meaning after a backslash in either @@ -575,15 +584,55 @@ class StringConstantChecker(BaseTokenChecker): # Unicode strings. UNICODE_ESCAPE_CHARACTERS = "uUN" + def __init__(self, *args, **kwargs): + super(StringConstantChecker, self).__init__(*args, **kwargs) + self.string_tokens = {} # token position -> (token value, next token) + def process_module(self, module): self._unicode_literals = "unicode_literals" in module.future_imports def process_tokens(self, tokens): - for (tok_type, token, (start_row, _), _, _) in tokens: + for i, (tok_type, token, start, _, _) in enumerate(tokens): if tok_type == tokenize.STRING: # 'token' is the whole un-parsed token; we can look at the start # of it to see whether it's a raw or unicode string etc. - self.process_string_token(token, start_row) + self.process_string_token(token, start[0]) + next_token = tokens[i + 1] if i + 1 < len(tokens) else None + self.string_tokens[start] = (str_eval(token), next_token) + + @check_messages(*(MSGS.keys())) + def visit_list(self, node): + self.check_for_concatenated_strings(node, "list") + + @check_messages(*(MSGS.keys())) + def visit_set(self, node): + self.check_for_concatenated_strings(node, "set") + + @check_messages(*(MSGS.keys())) + def visit_tuple(self, node): + self.check_for_concatenated_strings(node, "tuple") + + def check_for_concatenated_strings(self, iterable_node, iterable_type): + for elt in iterable_node.elts: + if isinstance(elt, Const) and elt.pytype() in _AST_NODE_STR_TYPES: + if elt.col_offset < 0: + # This can happen in case of escaped newlines + continue + matching_token, next_token = self.string_tokens[ + (elt.lineno, elt.col_offset) + ] + if matching_token != elt.value and next_token is not None: + next_token_type, next_token_pos = next_token[0], next_token[2] + # We do not warn if string concatenation happens over a newline + if ( + next_token_type == tokenize.STRING + and next_token_pos[0] == elt.lineno + ): + self.add_message( + "implicit-str-concat-in-sequence", + line=elt.lineno, + args=(iterable_type,), + ) def process_string_token(self, token, start_row): quote_char = None @@ -660,3 +709,19 @@ def register(linter): """required method to auto register this checker """ linter.register_checker(StringFormatChecker(linter)) linter.register_checker(StringConstantChecker(linter)) + + +def str_eval(token): + """ + Mostly replicate `ast.literal_eval(token)` manually to avoid any performance hit. + This supports f-strings, contrary to `ast.literal_eval`. + We have to support all string literal notations: + https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals + """ + if token[0:2].lower() in ("fr", "rf"): + token = token[2:] + elif token[0].lower() in ("r", "u", "f"): + token = token[1:] + if token[0:3] in ('"""', "'''"): + return token[3:-3] + return token[1:-1] diff --git a/pylint/checkers/typecheck.py b/pylint/checkers/typecheck.py index 5df5c9504..901161817 100644 --- a/pylint/checkers/typecheck.py +++ b/pylint/checkers/typecheck.py @@ -253,7 +253,7 @@ MSGS = { "E1121": ( "Too many positional arguments for %s call", "too-many-function-args", - "Used when a function call passes too many positional " "arguments.", + "Used when a function call passes too many positional arguments.", ), "E1123": ( "Unexpected keyword argument %r in %s call", diff --git a/pylint/checkers/variables.py b/pylint/checkers/variables.py index bf6825644..78d78a913 100644 --- a/pylint/checkers/variables.py +++ b/pylint/checkers/variables.py @@ -286,7 +286,7 @@ MSGS = { "E0601": ( "Using variable %r before assignment", "used-before-assignment", - "Used when a local variable is accessed before it's " "assignment.", + "Used when a local variable is accessed before it's assignment.", ), "E0602": ( "Undefined variable %r", @@ -357,7 +357,7 @@ MSGS = { "W0621": ( "Redefining name %r from outer scope (line %s)", "redefined-outer-name", - "Used when a variable's name hides a name defined in the outer " "scope.", + "Used when a variable's name hides a name defined in the outer scope.", ), "W0622": ( "Redefining built-in %r", @@ -367,7 +367,7 @@ MSGS = { "W0623": ( "Redefining name %r from %s in exception handler", "redefine-in-handler", - "Used when an exception handler assigns the exception " "to an existing name", + "Used when an exception handler assigns the exception to an existing name", ), "W0631": ( "Using possibly undefined loop variable %r", diff --git a/pylint/lint.py b/pylint/lint.py index 7e5414540..6c11dfc2a 100644 --- a/pylint/lint.py +++ b/pylint/lint.py @@ -190,7 +190,7 @@ MSGS = { "I0011": ( "Locally disabling %s (%s)", "locally-disabled", - "Used when an inline option disables a message or a messages " "category.", + "Used when an inline option disables a message or a messages category.", ), "I0013": ( "Ignoring entire file", diff --git a/pylint/test/functional/bad_continuation.py b/pylint/test/functional/bad_continuation.py index b1c3479c8..1c73a1122 100644 --- a/pylint/test/functional/bad_continuation.py +++ b/pylint/test/functional/bad_continuation.py @@ -1,5 +1,5 @@ """Regression test case for bad-continuation.""" -# pylint: disable=print-statement,using-constant-test,missing-docstring,wrong-import-position +# pylint: disable=print-statement,implicit-str-concat-in-sequence,using-constant-test,missing-docstring,wrong-import-position # Various alignment for brackets from __future__ import print_function diff --git a/pylint/test/functional/implicit_str_concat_in_sequence.py b/pylint/test/functional/implicit_str_concat_in_sequence.py new file mode 100644 index 000000000..8c4b5cb46 --- /dev/null +++ b/pylint/test/functional/implicit_str_concat_in_sequence.py @@ -0,0 +1,26 @@ +#pylint: disable=bad-continuation,invalid-name,missing-docstring + +# Basic test with a list +TEST_LIST1 = ['a' 'b'] # [implicit-str-concat-in-sequence] +# Testing with unicode strings in a tuple, with a comma AFTER concatenation +TEST_LIST2 = (u"a" u"b", u"c") # [implicit-str-concat-in-sequence] +# Testing with raw strings in a set, with a comma BEFORE concatenation +TEST_LIST3 = {r'''a''', r'''b''' r'''c'''} # [implicit-str-concat-in-sequence] +# Testing that only ONE warning is generated when string concatenation happens +# in the middle of a list +TEST_LIST4 = ["""a""", """b""" """c""", """d"""] # [implicit-str-concat-in-sequence] + +# The following shouldn't raise a warning because it is a function call +print('a', 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' 'ccc') + +# The following shouldn't raise a warning because string literals are +# on different lines +TEST_LIST5 = ('a', 'b' + 'c') + +# The following shouldn't raise a warning because of the escaped newline +TEST_LIST6 = ('a' 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb \ + ccc') + +# No warning for bytes +TEST_LIST7 = [b'A' b'B'] diff --git a/pylint/test/functional/implicit_str_concat_in_sequence.txt b/pylint/test/functional/implicit_str_concat_in_sequence.txt new file mode 100644 index 000000000..b9e1cbba9 --- /dev/null +++ b/pylint/test/functional/implicit_str_concat_in_sequence.txt @@ -0,0 +1,4 @@ +implicit-str-concat-in-sequence:4::Implicit string concatenation found in list +implicit-str-concat-in-sequence:6::Implicit string concatenation found in tuple +implicit-str-concat-in-sequence:8::Implicit string concatenation found in set +implicit-str-concat-in-sequence:11::Implicit string concatenation found in list diff --git a/pylint/test/unittest_checker_strings.py b/pylint/test/unittest_checker_strings.py index 57dc0af8d..4cac73747 100644 --- a/pylint/test/unittest_checker_strings.py +++ b/pylint/test/unittest_checker_strings.py @@ -10,6 +10,28 @@ from pylint.checkers import strings from pylint.testutils import CheckerTestCase, Message +TEST_TOKENS = ( + '"X"', + "'X'", + "'''X'''", + '"""X"""', + 'r"X"', + "R'X'", + 'u"X"', + "F'X'", + 'f"X"', + "F'X'", + 'fr"X"', + 'Fr"X"', + 'fR"X"', + 'FR"X"', + 'rf"X"', + 'rF"X"', + 'Rf"X"', + 'RF"X"', +) + + class TestStringChecker(CheckerTestCase): CHECKER_CLASS = strings.StringFormatChecker @@ -58,3 +80,7 @@ class TestStringChecker(CheckerTestCase): ) ): self.checker.visit_binop(node) + + def test_str_eval(self): + for token in TEST_TOKENS: + assert strings.str_eval(token) == "X" |