diff options
| author | Yury Gribov <tetra2005@gmail.com> | 2018-09-01 19:58:11 +0100 |
|---|---|---|
| committer | Claudiu Popa <pcmanticore@gmail.com> | 2018-09-04 10:41:57 +0200 |
| commit | ba62048e04fbe0e691cfccaf173c4ccbccca2992 (patch) | |
| tree | 06464a0015da095fd1b3d724c92985b4b5fb0590 | |
| parent | c00e464f9536dc3599f476a4fdcb23d832d7673b (diff) | |
| download | pylint-git-ba62048e04fbe0e691cfccaf173c4ccbccca2992.tar.gz | |
Added checker for format string type mismatches.
| -rw-r--r-- | CONTRIBUTORS.txt | 2 | ||||
| -rw-r--r-- | ChangeLog | 2 | ||||
| -rw-r--r-- | doc/whatsnew/2.2.rst | 1 | ||||
| -rw-r--r-- | pylint/checkers/logging.py | 2 | ||||
| -rw-r--r-- | pylint/checkers/strings.py | 55 | ||||
| -rw-r--r-- | pylint/checkers/utils.py | 9 | ||||
| -rw-r--r-- | pylint/test/unittest_checker_strings.py | 37 |
7 files changed, 101 insertions, 7 deletions
diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 540150494..1c00cabd2 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -225,3 +225,5 @@ contributors: * Roberto Leinardi: PyCharm plugin maintainer * Hornwitser: fix import graph + +* Yuri Gribov: contributor @@ -7,6 +7,8 @@ What's New in Pylint 2.2? Release date: TBA + * Report format string type mismatches. + * Handle ``AstroidSyntaxError`` when trying to import a module. Close #2313 diff --git a/doc/whatsnew/2.2.rst b/doc/whatsnew/2.2.rst index 209c7162b..ba60661a1 100644 --- a/doc/whatsnew/2.2.rst +++ b/doc/whatsnew/2.2.rst @@ -12,6 +12,7 @@ Summary -- Release highlights New checkers ============ +* String checker now reports format string type mismatches. Other Changes ============= diff --git a/pylint/checkers/logging.py b/pylint/checkers/logging.py index 12b79cdaa..23ad74dca 100644 --- a/pylint/checkers/logging.py +++ b/pylint/checkers/logging.py @@ -248,7 +248,7 @@ class LoggingChecker(checkers.BaseChecker): required_num_args = 0 else: try: - keyword_args, required_num_args = \ + keyword_args, required_num_args, _, _ = \ utils.parse_format_string(format_string) if keyword_args: # Keyword checking on logging strings is complicated by diff --git a/pylint/checkers/strings.py b/pylint/checkers/strings.py index 88a4ffdb9..b292e3317 100644 --- a/pylint/checkers/strings.py +++ b/pylint/checkers/strings.py @@ -22,6 +22,7 @@ """Checker for string formatting operations. """ +import builtins import sys import tokenize import string @@ -78,6 +79,10 @@ MSGS = { "too-few-format-args", "Used when a format string that uses unnamed conversion " "specifiers is given too few arguments"), + 'E1307': ("Argument %r does not match format type %r", + "bad-string-format-type", + "Used when a type required by format string " + "is not suitable for actual argument type"), 'E1310': ("Suspicious argument in %s.%s call", "bad-str-strip-call", "The argument to a str.{l,r,}strip call contains a" @@ -122,6 +127,10 @@ OTHER_NODES = (astroid.Const, astroid.List, astroid.Lambda, astroid.FunctionDef, astroid.ListComp, astroid.SetComp, astroid.GeneratorExp) +BUILTINS_STR = builtins.__name__ + ".str" +BUILTINS_FLOAT = builtins.__name__ + ".float" +BUILTINS_INT = builtins.__name__ + ".int" + if _PY3K: import _string # pylint: disable=wrong-import-position, wrong-import-order @@ -238,6 +247,21 @@ def get_access_path(key, parts): path.append("[{!r}]".format(specifier)) return str(key) + "".join(path) +def arg_matches_format_type(arg_type, format_type): + if format_type in "sr": + # All types can be printed with %s and %r + return True + if isinstance(arg_type, astroid.Instance): + arg_type = arg_type.pytype() + if arg_type == BUILTINS_STR: + return format_type == "c" + if arg_type == BUILTINS_FLOAT: + return format_type in "deEfFgGn%" + if arg_type == BUILTINS_INT: + # Integers allow all types + return True + return False + return True class StringFormatChecker(BaseChecker): """Checks string formatting operations to ensure that the format string @@ -248,6 +272,7 @@ class StringFormatChecker(BaseChecker): name = 'string' msgs = MSGS + # pylint: disable=too-many-branches @check_messages(*(MSGS.keys())) def visit_binop(self, node): if node.op != '%': @@ -260,8 +285,8 @@ class StringFormatChecker(BaseChecker): return format_string = left.value try: - required_keys, required_num_args = \ - utils.parse_format_string(format_string) + required_keys, required_num_args, required_key_types, \ + required_arg_types = utils.parse_format_string(format_string) except utils.UnsupportedFormatCharacter as e: c = format_string[e.index] self.add_message('bad-format-character', @@ -305,6 +330,20 @@ class StringFormatChecker(BaseChecker): if key not in required_keys: self.add_message('unused-format-string-key', node=node, args=key) + for key, arg in args.items: + if not isinstance(key, astroid.Const): + continue + format_type = required_key_types.get(key.value, None) + arg_type = utils.safe_infer(arg) + if (format_type is not None and + arg_type not in (None, astroid.Uninferable) and + not arg_matches_format_type(arg_type, + format_type)): + self.add_message('bad-string-format-type', + node=node, + args=(arg_type.pytype(), + format_type)) + # TODO: compare type elif isinstance(args, OTHER_NODES + (astroid.Tuple,)): type_name = type(args).__name__ self.add_message('format-needs-mapping', @@ -322,8 +361,10 @@ class StringFormatChecker(BaseChecker): rhs_tuple = utils.safe_infer(args) num_args = None if rhs_tuple not in (None, astroid.Uninferable): - num_args = len(rhs_tuple.elts) + args_elts = rhs_tuple.elts + num_args = len(args_elts) elif isinstance(args, OTHER_NODES + (astroid.Dict, astroid.DictComp)): + args_elts = [args] num_args = 1 else: # The RHS of the format specifier is a name or @@ -335,6 +376,14 @@ class StringFormatChecker(BaseChecker): self.add_message('too-many-format-args', node=node) elif num_args < required_num_args: self.add_message('too-few-format-args', node=node) + for arg, format_type in zip(args_elts, required_arg_types): + arg_type = utils.safe_infer(arg) + if (arg_type not in (None, astroid.Uninferable) and + not arg_matches_format_type(arg_type, format_type)): + self.add_message('bad-string-format-type', + node=node, + args=(arg_type.pytype(), + format_type)) @check_messages(*(MSGS.keys())) diff --git a/pylint/checkers/utils.py b/pylint/checkers/utils.py index 8ac9b1cce..b98941205 100644 --- a/pylint/checkers/utils.py +++ b/pylint/checkers/utils.py @@ -352,13 +352,16 @@ class UnsupportedFormatCharacter(Exception): Exception.__init__(self, index) self.index = index -def parse_format_string(format_string: str) -> Tuple[Set[str], int]: +def parse_format_string(format_string: str) -> \ + Tuple[Set[str], int, Dict[str, str], List[str]]: """Parses a format string, returning a tuple of (keys, num_args), where keys is the set of mapping keys in the format string, and num_args is the number of arguments required by the format string. Raises IncompleteFormatString or UnsupportedFormatCharacter if a parse error occurs.""" keys = set() + key_types = dict() + pos_types = [] num_args = 0 def next_char(i): i += 1 @@ -416,10 +419,12 @@ def parse_format_string(format_string: str) -> Tuple[Set[str], int]: raise UnsupportedFormatCharacter(i) if key: keys.add(key) + key_types[key] = char elif char != '%': num_args += 1 + pos_types.append(char) i += 1 - return keys, num_args + return keys, num_args, key_types, pos_types def is_attr_protected(attrname: str) -> bool: diff --git a/pylint/test/unittest_checker_strings.py b/pylint/test/unittest_checker_strings.py index 2e77b7466..64eedbeba 100644 --- a/pylint/test/unittest_checker_strings.py +++ b/pylint/test/unittest_checker_strings.py @@ -7,7 +7,7 @@ import astroid from pylint.checkers import strings -from pylint.testutils import CheckerTestCase +from pylint.testutils import CheckerTestCase, Message class TestStringChecker(CheckerTestCase): @@ -18,3 +18,38 @@ class TestStringChecker(CheckerTestCase): node = astroid.extract_node(code) with self.assertNoMessages(): self.checker.visit_call(node) + + def test_format_types(self): + for code in ("'%s' % 1", "'%d' % 1", "'%f' % 1"): + with self.assertNoMessages(): + node = astroid.extract_node(code) + self.checker.visit_binop(node) + + for code in ("'%s' % 1", + "'%(key)s' % {'key' : 1}", + "'%d' % 1", + "'%(key)d' % {'key' : 1}", + "'%f' % 1", + "'%(key)f' % {'key' : 1}", + "'%d' % 1.1", + "'%(key)d' % {'key' : 1.1}", + "'%s' % []", + "'%(key)s' % {'key' : []}", + "'%s' % None", + "'%(key)s' % {'key' : None}"): + with self.assertNoMessages(): + node = astroid.extract_node(code) + self.checker.visit_binop(node) + + for code, arg_type, format_type in [("'%d' % '1'", 'builtins.str', 'd'), + ("'%(key)d' % {'key' : '1'}", 'builtins.str', 'd'), + ("'%x' % 1.1", 'builtins.float', 'x'), + ("'%(key)x' % {'key' : 1.1}", 'builtins.float', 'x'), + ("'%d' % []", 'builtins.list', 'd'), + ("'%(key)d' % {'key' : []}", 'builtins.list', 'd')]: + node = astroid.extract_node(code) + with self.assertAddsMessages( + Message('bad-string-format-type', + node=node, + args=(arg_type, format_type))): + self.checker.visit_binop(node) |
