blob: ddf69c264c4cab756bf7c3a7c93a20fa0d4fd55b [file]
"""Expression type checker. This file is conceptually part of ExpressionChecker and TypeChecker."""
import re
from typing import cast, List, Tuple, Dict, Callable, Union
from mypy.types import (
Type, AnyType, TupleType, Instance, UnionType
)
from mypy.nodes import (
StrExpr, BytesExpr, UnicodeExpr, TupleExpr, DictExpr, Context, Expression
)
if False:
# break import cycle only needed for mypy
import mypy.checker
import mypy.checkexpr
from mypy import messages
from mypy.messages import MessageBuilder
class ConversionSpecifier:
def __init__(self, key: str, flags: str, width: str, precision: str, type: str) -> None:
self.key = key
self.flags = flags
self.width = width
self.precision = precision
self.type = type
def has_key(self) -> bool:
return self.key is not None
def has_star(self) -> bool:
return self.width == '*' or self.precision == '*'
class StringFormatterChecker:
"""String interpolation/formatter type checker.
This class works closely together with checker.ExpressionChecker.
"""
# Some services are provided by a TypeChecker instance.
chk = None # type: mypy.checker.TypeChecker
# This is shared with TypeChecker, but stored also here for convenience.
msg = None # type: MessageBuilder
# Some services are provided by a ExpressionChecker instance.
exprchk = None # type: mypy.checkexpr.ExpressionChecker
def __init__(self,
exprchk: 'mypy.checkexpr.ExpressionChecker',
chk: 'mypy.checker.TypeChecker',
msg: MessageBuilder) -> None:
"""Construct an expression type checker."""
self.chk = chk
self.exprchk = exprchk
self.msg = msg
# TODO: In Python 3, the bytes formatting has a more restricted set of options
# compared to string formatting.
# TODO: Bytes formatting in Python 3 is only supported in 3.5 and up.
def check_str_interpolation(self,
str: Union[StrExpr, BytesExpr, UnicodeExpr],
replacements: Expression) -> Type:
"""Check the types of the 'replacements' in a string interpolation
expression: str % replacements
"""
specifiers = self.parse_conversion_specifiers(str.value)
has_mapping_keys = self.analyze_conversion_specifiers(specifiers, str)
if has_mapping_keys is None:
pass # Error was reported
elif has_mapping_keys:
self.check_mapping_str_interpolation(specifiers, replacements)
else:
self.check_simple_str_interpolation(specifiers, replacements)
if isinstance(str, BytesExpr):
return self.named_type('builtins.bytes')
elif isinstance(str, UnicodeExpr):
return self.named_type('builtins.unicode')
elif isinstance(str, StrExpr):
return self.named_type('builtins.str')
else:
assert False
def parse_conversion_specifiers(self, format: str) -> List[ConversionSpecifier]:
key_regex = r'(\(([^()]*)\))?' # (optional) parenthesised sequence of characters
flags_regex = r'([#0\-+ ]*)' # (optional) sequence of flags
width_regex = r'(\*|[1-9][0-9]*)?' # (optional) minimum field width (* or numbers)
precision_regex = r'(?:\.(\*|[0-9]+)?)?' # (optional) . followed by * of numbers
length_mod_regex = r'[hlL]?' # (optional) length modifier (unused)
type_regex = r'(.)?' # conversion type
regex = ('%' + key_regex + flags_regex + width_regex +
precision_regex + length_mod_regex + type_regex)
specifiers = [] # type: List[ConversionSpecifier]
for parens_key, key, flags, width, precision, type in re.findall(regex, format):
if parens_key == '':
key = None
specifiers.append(ConversionSpecifier(key, flags, width, precision, type))
return specifiers
def analyze_conversion_specifiers(self, specifiers: List[ConversionSpecifier],
context: Context) -> bool:
has_star = any(specifier.has_star() for specifier in specifiers)
has_key = any(specifier.has_key() for specifier in specifiers)
all_have_keys = all(
specifier.has_key() or specifier.type == '%' for specifier in specifiers
)
if has_key and has_star:
self.msg.string_interpolation_with_star_and_key(context)
return None
if has_key and not all_have_keys:
self.msg.string_interpolation_mixing_key_and_non_keys(context)
return None
return has_key
def check_simple_str_interpolation(self, specifiers: List[ConversionSpecifier],
replacements: Expression) -> None:
checkers = self.build_replacement_checkers(specifiers, replacements)
if checkers is None:
return
rhs_type = self.accept(replacements)
rep_types = [] # type: List[Type]
if isinstance(rhs_type, TupleType):
rep_types = rhs_type.items
elif isinstance(rhs_type, AnyType):
return
else:
rep_types = [rhs_type]
if len(checkers) > len(rep_types):
self.msg.too_few_string_formatting_arguments(replacements)
elif len(checkers) < len(rep_types):
self.msg.too_many_string_formatting_arguments(replacements)
else:
if len(checkers) == 1:
check_node, check_type = checkers[0]
if isinstance(rhs_type, TupleType) and len(rhs_type.items) == 1:
check_type(rhs_type.items[0])
else:
check_node(replacements)
elif isinstance(replacements, TupleExpr):
for checks, rep_node in zip(checkers, replacements.items):
check_node, check_type = checks
check_node(rep_node)
else:
for checks, rep_type in zip(checkers, rep_types):
check_node, check_type = checks
check_type(rep_type)
def check_mapping_str_interpolation(self, specifiers: List[ConversionSpecifier],
replacements: Expression) -> None:
if (isinstance(replacements, DictExpr) and
all(isinstance(k, (StrExpr, BytesExpr))
for k, v in replacements.items)):
mapping = {} # type: Dict[str, Type]
for k, v in replacements.items:
key_str = cast(StrExpr, k).value
mapping[key_str] = self.accept(v)
for specifier in specifiers:
if specifier.type == '%':
# %% is allowed in mappings, no checking is required
continue
if specifier.key not in mapping:
self.msg.key_not_in_mapping(specifier.key, replacements)
return
rep_type = mapping[specifier.key]
expected_type = self.conversion_type(specifier.type, replacements)
if expected_type is None:
return
self.chk.check_subtype(rep_type, expected_type, replacements,
messages.INCOMPATIBLE_TYPES_IN_STR_INTERPOLATION,
'expression has type',
'placeholder with key \'%s\' has type' % specifier.key)
else:
rep_type = self.accept(replacements)
dict_type = self.chk.named_generic_type('builtins.dict',
[AnyType(), AnyType()])
self.chk.check_subtype(rep_type, dict_type, replacements,
messages.FORMAT_REQUIRES_MAPPING,
'expression has type', 'expected type for mapping is')
def build_replacement_checkers(self, specifiers: List[ConversionSpecifier],
context: Context) -> List[Tuple[Callable[[Expression], None],
Callable[[Type], None]]]:
checkers = [] # type: List[Tuple[Callable[[Expression], None], Callable[[Type], None]]]
for specifier in specifiers:
checker = self.replacement_checkers(specifier, context)
if checker is None:
return None
checkers.extend(checker)
return checkers
def replacement_checkers(self, specifier: ConversionSpecifier,
context: Context) -> List[Tuple[Callable[[Expression], None],
Callable[[Type], None]]]:
"""Returns a list of tuples of two functions that check whether a replacement is
of the right type for the specifier. The first functions take a node and checks
its type in the right type context. The second function just checks a type.
"""
checkers = [] # type: List[Tuple[Callable[[Expression], None], Callable[[Type], None]]]
if specifier.width == '*':
checkers.append(self.checkers_for_star(context))
if specifier.precision == '*':
checkers.append(self.checkers_for_star(context))
if specifier.type == 'c':
c = self.checkers_for_c_type(specifier.type, context)
if c is None:
return None
checkers.append(c)
elif specifier.type != '%':
c = self.checkers_for_regular_type(specifier.type, context)
if c is None:
return None
checkers.append(c)
return checkers
def checkers_for_star(self, context: Context) -> Tuple[Callable[[Expression], None],
Callable[[Type], None]]:
"""Returns a tuple of check functions that check whether, respectively,
a node or a type is compatible with a star in a conversion specifier
"""
expected = self.named_type('builtins.int')
def check_type(type: Type = None) -> None:
expected = self.named_type('builtins.int')
self.chk.check_subtype(type, expected, context, '* wants int')
def check_expr(expr: Expression) -> None:
type = self.accept(expr, expected)
check_type(type)
return check_expr, check_type
def checkers_for_regular_type(self, type: str,
context: Context) -> Tuple[Callable[[Expression], None],
Callable[[Type], None]]:
"""Returns a tuple of check functions that check whether, respectively,
a node or a type is compatible with 'type'. Return None in case of an
"""
expected_type = self.conversion_type(type, context)
if expected_type is None:
return None
def check_type(type: Type = None) -> None:
self.chk.check_subtype(type, expected_type, context,
messages.INCOMPATIBLE_TYPES_IN_STR_INTERPOLATION,
'expression has type', 'placeholder has type')
def check_expr(expr: Expression) -> None:
type = self.accept(expr, expected_type)
check_type(type)
return check_expr, check_type
def checkers_for_c_type(self, type: str,
context: Context) -> Tuple[Callable[[Expression], None],
Callable[[Type], None]]:
"""Returns a tuple of check functions that check whether, respectively,
a node or a type is compatible with 'type' that is a character type
"""
expected_type = self.conversion_type(type, context)
if expected_type is None:
return None
def check_type(type: Type = None) -> None:
self.chk.check_subtype(type, expected_type, context,
messages.INCOMPATIBLE_TYPES_IN_STR_INTERPOLATION,
'expression has type', 'placeholder has type')
def check_expr(expr: Expression) -> None:
"""int, or str with length 1"""
type = self.accept(expr, expected_type)
if isinstance(expr, (StrExpr, BytesExpr)) and len(cast(StrExpr, expr).value) != 1:
self.msg.requires_int_or_char(context)
check_type(type)
return check_expr, check_type
def conversion_type(self, p: str, context: Context) -> Type:
"""Return the type that is accepted for a string interpolation
conversion specifier type.
Note that both Python's float (e.g. %f) and integer (e.g. %d)
specifier types accept both float and integers.
"""
if p in ['s', 'r']:
return AnyType()
elif p in ['d', 'i', 'o', 'u', 'x', 'X',
'e', 'E', 'f', 'F', 'g', 'G']:
return UnionType([self.named_type('builtins.int'),
self.named_type('builtins.float')])
elif p in ['c']:
return UnionType([self.named_type('builtins.int'),
self.named_type('builtins.float'),
self.named_type('builtins.str')])
else:
self.msg.unsupported_placeholder(p, context)
return None
#
# Helpers
#
def named_type(self, name: str) -> Instance:
"""Return an instance type with type given by the name and no type
arguments. Alias for TypeChecker.named_type.
"""
return self.chk.named_type(name)
def accept(self, expr: Expression, context: Type = None) -> Type:
"""Type check a node. Alias for TypeChecker.accept."""
return self.chk.accept(expr, context)