summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLucas Cimon <lucas.cimon@gmail.com>2018-10-10 19:56:25 +0200
committerClaudiu Popa <pcmanticore@gmail.com>2018-10-10 20:56:25 +0300
commitfcc01516ae176ad3fdedc4497328105f3314e376 (patch)
treeff912ba3ca5640e5f782a62d3a3765754ae5ced4
parent04cf3f8298fa1c718f950c90c34ee99a17f3c503 (diff)
downloadpylint-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.txt2
-rw-r--r--ChangeLog2
-rw-r--r--doc/whatsnew/2.2.rst9
-rw-r--r--pylint/checkers/base.py18
-rw-r--r--pylint/checkers/classes.py18
-rw-r--r--pylint/checkers/design_analysis.py2
-rw-r--r--pylint/checkers/exceptions.py6
-rw-r--r--pylint/checkers/format.py2
-rw-r--r--pylint/checkers/imports.py8
-rw-r--r--pylint/checkers/misc.py2
-rw-r--r--pylint/checkers/newstyle.py2
-rw-r--r--pylint/checkers/python3.py10
-rw-r--r--pylint/checkers/refactoring.py8
-rw-r--r--pylint/checkers/stdlib.py4
-rw-r--r--pylint/checkers/strings.py77
-rw-r--r--pylint/checkers/typecheck.py2
-rw-r--r--pylint/checkers/variables.py6
-rw-r--r--pylint/lint.py2
-rw-r--r--pylint/test/functional/bad_continuation.py2
-rw-r--r--pylint/test/functional/implicit_str_concat_in_sequence.py26
-rw-r--r--pylint/test/functional/implicit_str_concat_in_sequence.txt4
-rw-r--r--pylint/test/unittest_checker_strings.py26
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
diff --git a/ChangeLog b/ChangeLog
index 396030a8f..962141f2f 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -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"