summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYury Gribov <tetra2005@gmail.com>2018-09-01 19:58:11 +0100
committerClaudiu Popa <pcmanticore@gmail.com>2018-09-04 10:41:57 +0200
commitba62048e04fbe0e691cfccaf173c4ccbccca2992 (patch)
tree06464a0015da095fd1b3d724c92985b4b5fb0590
parentc00e464f9536dc3599f476a4fdcb23d832d7673b (diff)
downloadpylint-git-ba62048e04fbe0e691cfccaf173c4ccbccca2992.tar.gz
Added checker for format string type mismatches.
-rw-r--r--CONTRIBUTORS.txt2
-rw-r--r--ChangeLog2
-rw-r--r--doc/whatsnew/2.2.rst1
-rw-r--r--pylint/checkers/logging.py2
-rw-r--r--pylint/checkers/strings.py55
-rw-r--r--pylint/checkers/utils.py9
-rw-r--r--pylint/test/unittest_checker_strings.py37
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
diff --git a/ChangeLog b/ChangeLog
index ef84874ff..b83c1ad51 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -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)