diff --git a/mypy/checkstrformat.py b/mypy/checkstrformat.py index 974985d8b4fc..b903c59a7192 100644 --- a/mypy/checkstrformat.py +++ b/mypy/checkstrformat.py @@ -199,15 +199,11 @@ def parse_format_value( custom_match, start_pos=start_pos, non_standard_format_spec=True ) else: - msg.fail( - "Invalid conversion specifier in format string", - ctx, - code=codes.STRING_FORMATTING, - ) + msg.fail(message_registry.FORMAT_STR_INVALID_SPECIFIER, ctx) return None if conv_spec.key and ("{" in conv_spec.key or "}" in conv_spec.key): - msg.fail("Conversion value must not contain { or }", ctx, code=codes.STRING_FORMATTING) + msg.fail(message_registry.FORMAT_STR_BRACES_IN_SPECIFIER, ctx) return None result.append(conv_spec) @@ -218,11 +214,7 @@ def parse_format_value( and ("{" in conv_spec.format_spec or "}" in conv_spec.format_spec) ): if nested: - msg.fail( - "Formatting nesting must be at most two levels deep", - ctx, - code=codes.STRING_FORMATTING, - ) + msg.fail(message_registry.FORMAT_STR_NESTING_ATMOST_TWO_LEVELS, ctx) return None sub_conv_specs = parse_format_value(conv_spec.format_spec, ctx, msg, nested=True) if sub_conv_specs is None: @@ -260,11 +252,7 @@ def find_non_escaped_targets( if pos < len(format_value) - 1 and format_value[pos + 1] == "}": pos += 1 else: - msg.fail( - "Invalid conversion specifier in format string: unexpected }", - ctx, - code=codes.STRING_FORMATTING, - ) + msg.fail(message_registry.FORMAT_STR_UNEXPECTED_RBRACE, ctx) return None else: # Adjust nesting level, then either continue adding chars or move on. @@ -279,11 +267,7 @@ def find_non_escaped_targets( next_spec = "" pos += 1 if nesting: - msg.fail( - "Invalid conversion specifier in format string: unmatched {", - ctx, - code=codes.STRING_FORMATTING, - ) + msg.fail(message_registry.FORMAT_STR_UNMATCHED_LBRACE, ctx) return None return result @@ -369,9 +353,8 @@ def check_specs_in_format_call( ): # TODO: add support for some custom specs like datetime? self.msg.fail( - "Unrecognized format" ' specification "{}"'.format(spec.format_spec[1:]), + message_registry.UNRECOGNIZED_FORMAT_SPEC.format(spec.format_spec[1:]), call, - code=codes.STRING_FORMATTING, ) continue # Adjust expected and actual types. @@ -390,10 +373,10 @@ def check_specs_in_format_call( # If the explicit conversion is given, then explicit conversion is called _first_. if spec.conversion[1] not in "rsa": self.msg.fail( - 'Invalid conversion type "{}",' - ' must be one of "r", "s" or "a"'.format(spec.conversion[1]), + message_registry.FORMAT_STR_INVALID_CONVERSION_TYPE.format( + spec.conversion[1] + ), call, - code=codes.STRING_FORMATTING, ) actual_type = self.named_type("builtins.str") @@ -433,13 +416,7 @@ def perform_special_format_checks( if has_type_component(actual_type, "builtins.bytes") and not custom_special_method( actual_type, "__str__" ): - self.msg.fail( - 'If x = b\'abc\' then f"{x}" or "{}".format(x) produces "b\'abc\'", ' - 'not "abc". If this is desired behavior, use f"{x!r}" or "{!r}".format(x). ' - "Otherwise, decode the bytes", - call, - code=codes.STR_BYTES_PY3, - ) + self.msg.fail(message_registry.FORMAT_STR_BYTES_USE_REPR, call) if spec.flags: numeric_types = UnionType( [self.named_type("builtins.int"), self.named_type("builtins.float")] @@ -451,11 +428,7 @@ def perform_special_format_checks( and not is_subtype(actual_type, numeric_types) and not custom_special_method(actual_type, "__format__") ): - self.msg.fail( - "Numeric flags are only allowed for numeric types", - call, - code=codes.STRING_FORMATTING, - ) + self.msg.fail(message_registry.FORMAT_STR_INVALID_NUMERIC_FLAG, call) def find_replacements_in_call(self, call: CallExpr, keys: list[str]) -> list[Expression]: """Find replacement expression for every specifier in str.format() call. @@ -469,19 +442,14 @@ def find_replacements_in_call(self, call: CallExpr, keys: list[str]) -> list[Exp expr = self.get_expr_by_position(int(key), call) if not expr: self.msg.fail( - "Cannot find replacement for positional" - " format specifier {}".format(key), - call, - code=codes.STRING_FORMATTING, + message_registry.FORMAT_STR_REPLACEMENT_NOT_FOUND.format(key), call ) expr = TempNode(AnyType(TypeOfAny.from_error)) else: expr = self.get_expr_by_name(key, call) if not expr: self.msg.fail( - "Cannot find replacement for named" ' format specifier "{}"'.format(key), - call, - code=codes.STRING_FORMATTING, + message_registry.FORMAT_STR_NAMED_REPLACEMENT_NOT_FOUND.format(key), call ) expr = TempNode(AnyType(TypeOfAny.from_error)) result.append(expr) @@ -555,11 +523,7 @@ def auto_generate_keys(self, all_specs: list[ConversionSpecifier], ctx: Context) some_defined = any(s.key and s.key.isdecimal() for s in all_specs) all_defined = all(bool(s.key) for s in all_specs) if some_defined and not all_defined: - self.msg.fail( - "Cannot combine automatic field numbering and manual field specification", - ctx, - code=codes.STRING_FORMATTING, - ) + self.msg.fail(message_registry.FORMAT_STR_PARTIAL_FIELD_NUMBERING, ctx) return False if all_defined: return True @@ -594,11 +558,7 @@ def apply_field_accessors( dummy, fnam="", module=None, options=self.chk.options, errors=temp_errors ) if temp_errors.is_errors(): - self.msg.fail( - f'Syntax error in format specifier "{spec.field}"', - ctx, - code=codes.STRING_FORMATTING, - ) + self.msg.fail(message_registry.FORMAT_STR_SYNTAX_ERROR.format(spec.field), ctx) return TempNode(AnyType(TypeOfAny.from_error)) # These asserts are guaranteed by the original regexp. @@ -637,10 +597,7 @@ class User(TypedDict): """ if not isinstance(temp_ast, (MemberExpr, IndexExpr)): self.msg.fail( - "Only index and member expressions are allowed in" - ' format field accessors; got "{}"'.format(spec.field), - ctx, - code=codes.STRING_FORMATTING, + message_registry.FORMAT_STR_INVALID_ACCESSOR_EXPR.format(spec.field), ctx ) return False if isinstance(temp_ast, MemberExpr): @@ -651,10 +608,10 @@ class User(TypedDict): assert spec.key, "Call this method only after auto-generating keys!" assert spec.field self.msg.fail( - "Invalid index expression in format field" - ' accessor "{}"'.format(spec.field[len(spec.key) :]), + message_registry.FORMAT_STR_INVALID_INDEX_ACCESSOR.format( + spec.field[len(spec.key) :] + ), ctx, - code=codes.STRING_FORMATTING, ) return False if isinstance(temp_ast.index, NameExpr): @@ -683,11 +640,7 @@ def check_str_interpolation(self, expr: FormatStringExpr, replacements: Expressi specifiers = parse_conversion_specifiers(expr.value) has_mapping_keys = self.analyze_conversion_specifiers(specifiers, expr) if isinstance(expr, BytesExpr) and self.chk.options.python_version < (3, 5): - self.msg.fail( - "Bytes formatting is only supported in Python 3.5 and later", - replacements, - code=codes.STRING_FORMATTING, - ) + self.msg.fail(message_registry.FORMAT_STR_BYTES_ABOVE_PY35, replacements) return AnyType(TypeOfAny.from_error) if has_mapping_keys is None: @@ -794,9 +747,7 @@ def check_mapping_str_interpolation( # Special case: for bytes formatting keys must be bytes. if not isinstance(k, BytesExpr): self.msg.fail( - "Dictionary keys in bytes formatting must be bytes, not strings", - expr, - code=codes.STRING_FORMATTING, + message_registry.FORMAT_STR_BYTES_DICT_KEYS_MUST_BE_BYTES, expr ) key_str = cast(FormatStringExpr, k).value mapping[key_str] = self.accept(v) @@ -948,21 +899,12 @@ def check_s_special_cases(self, expr: FormatStringExpr, typ: Type, context: Cont if isinstance(expr, StrExpr): # Couple special cases for string formatting. if has_type_component(typ, "builtins.bytes"): - self.msg.fail( - 'If x = b\'abc\' then "%s" % x produces "b\'abc\'", not "abc". ' - 'If this is desired behavior use "%r" % x. Otherwise, decode the bytes', - context, - code=codes.STR_BYTES_PY3, - ) + self.msg.fail(message_registry.FORMAT_STR_BYTES_USE_REPR_OLD, context) return False if isinstance(expr, BytesExpr): # A special case for bytes formatting: b'%s' actually requires bytes on Python 3. if has_type_component(typ, "builtins.str"): - self.msg.fail( - "On Python 3 b'%s' requires bytes, not string", - context, - code=codes.STRING_FORMATTING, - ) + self.msg.fail(message_registry.FORMAT_STR_BYTES_REQUIRED_PY3, context) return False return True @@ -1024,18 +966,10 @@ def conversion_type( INT_TYPES = REQUIRE_INT_NEW if format_call else REQUIRE_INT_OLD if p == "b" and not format_call: if self.chk.options.python_version < (3, 5): - self.msg.fail( - 'Format character "b" is only supported in Python 3.5 and later', - context, - code=codes.STRING_FORMATTING, - ) + self.msg.fail(message_registry.FORMAT_STR_INVALID_BYTES_SPECIFIER_PY35, context) return None if not isinstance(expr, BytesExpr): - self.msg.fail( - 'Format character "b" is only supported on bytes patterns', - context, - code=codes.STRING_FORMATTING, - ) + self.msg.fail(message_registry.FORMAT_STR_INVALID_BYTES_SPECIFIER, context) return None return self.named_type("builtins.bytes") elif p == "a": diff --git a/mypy/errorcodes.py b/mypy/errorcodes.py index 50a82be9816d..947dbe9b81c5 100644 --- a/mypy/errorcodes.py +++ b/mypy/errorcodes.py @@ -112,10 +112,10 @@ def __str__(self) -> str: VALID_NEWTYPE: Final = ErrorCode( "valid-newtype", "Check that argument 2 to NewType is valid", "General" ) -STRING_FORMATTING: Final = ErrorCode( +STRING_FORMATTING: Final[ErrorCode] = ErrorCode( "str-format", "Check that string formatting/interpolation is type-safe", "General" ) -STR_BYTES_PY3: Final = ErrorCode( +STR_BYTES_PY3: Final[ErrorCode] = ErrorCode( "str-bytes-safe", "Warn about implicit coercions related to bytes and string types", "General" ) EXIT_RETURN: Final = ErrorCode( diff --git a/mypy/message_registry.py b/mypy/message_registry.py index c5164d48fd13..1459e89d84fb 100644 --- a/mypy/message_registry.py +++ b/mypy/message_registry.py @@ -316,3 +316,74 @@ def with_additional_msg(self, info: str) -> ErrorMessage: ARG_NAME_EXPECTED_STRING_LITERAL: Final = ErrorMessage( "Expected string literal for argument name, got {}", codes.SYNTAX ) + +FORMAT_STR_INVALID_SPECIFIER: Final = ErrorMessage( + "Invalid conversion specifier in format string", codes.STRING_FORMATTING +) +FORMAT_STR_BRACES_IN_SPECIFIER: Final = ErrorMessage( + "Conversion value must not contain { or }", codes.STRING_FORMATTING +) +FORMAT_STR_NESTING_ATMOST_TWO_LEVELS: Final = ErrorMessage( + "Formatting nesting must be at most two levels deep", codes.STRING_FORMATTING +) +FORMAT_STR_UNEXPECTED_RBRACE: Final = ErrorMessage( + "Invalid conversion specifier in format string: unexpected }", codes.STRING_FORMATTING +) +FORMAT_STR_UNMATCHED_LBRACE: Final = ErrorMessage( + "Invalid conversion specifier in format string: unmatched {", codes.STRING_FORMATTING +) +UNRECOGNIZED_FORMAT_SPEC: Final = ErrorMessage( + 'Unrecognized format specification "{}"', codes.STRING_FORMATTING +) +FORMAT_STR_INVALID_CONVERSION_TYPE: Final = ErrorMessage( + 'Invalid conversion type "{}", must be one of "r", "s" or "a"', codes.STRING_FORMATTING +) +FORMAT_STR_BYTES_USE_REPR: Final = ErrorMessage( + 'If x = b\'abc\' then f"{x}" or "{}".format(x) produces "b\'abc\'", ' + 'not "abc". If this is desired behavior, use f"{x!r}" or "{!r}".format(x). ' + "Otherwise, decode the bytes", + codes.STR_BYTES_PY3, +) +FORMAT_STR_BYTES_USE_REPR_OLD: Final = ErrorMessage( + 'If x = b\'abc\' then "%s" % x produces "b\'abc\'", not "abc". ' + 'If this is desired behavior use "%r" % x. Otherwise, decode the bytes', + codes.STR_BYTES_PY3, +) +FORMAT_STR_INVALID_NUMERIC_FLAG: Final = ErrorMessage( + "Numeric flags are only allowed for numeric types", codes.STRING_FORMATTING +) +FORMAT_STR_REPLACEMENT_NOT_FOUND: Final = ErrorMessage( + "Cannot find replacement for positional format specifier {}", codes.STRING_FORMATTING +) +FORMAT_STR_NAMED_REPLACEMENT_NOT_FOUND: Final = ErrorMessage( + 'Cannot find replacement for named format specifier "{}"', codes.STRING_FORMATTING +) +FORMAT_STR_PARTIAL_FIELD_NUMBERING: Final = ErrorMessage( + "Cannot combine automatic field numbering and manual field specification", + codes.STRING_FORMATTING, +) +FORMAT_STR_SYNTAX_ERROR: Final = ErrorMessage( + 'Syntax error in format specifier "{}"', codes.STRING_FORMATTING +) +FORMAT_STR_INVALID_ACCESSOR_EXPR: Final = ErrorMessage( + 'Only index and member expressions are allowed in format field accessors; got "{}"', + codes.STRING_FORMATTING, +) +FORMAT_STR_INVALID_INDEX_ACCESSOR: Final = ErrorMessage( + 'Invalid index expression in format field accessor "{}"', codes.STRING_FORMATTING +) +FORMAT_STR_BYTES_ABOVE_PY35: Final = ErrorMessage( + "Bytes formatting is only supported in Python 3.5 and later", codes.STRING_FORMATTING +) +FORMAT_STR_BYTES_DICT_KEYS_MUST_BE_BYTES: Final = ErrorMessage( + "Dictionary keys in bytes formatting must be bytes, not strings", codes.STRING_FORMATTING +) +FORMAT_STR_BYTES_REQUIRED_PY3: Final = ErrorMessage( + "On Python 3 b'%s' requires bytes, not string", codes.STRING_FORMATTING +) +FORMAT_STR_INVALID_BYTES_SPECIFIER_PY35: Final = ErrorMessage( + 'Format character "b" is only supported in Python 3.5 and later', codes.STRING_FORMATTING +) +FORMAT_STR_INVALID_BYTES_SPECIFIER: Final = ErrorMessage( + 'Format character "b" is only supported on bytes patterns', codes.STRING_FORMATTING +) diff --git a/mypy/messages.py b/mypy/messages.py index 9d703a1a974a..1ad35ab2f47e 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -16,7 +16,7 @@ import re from contextlib import contextmanager from textwrap import dedent -from typing import Any, Callable, Collection, Iterable, Iterator, List, Sequence, cast +from typing import Any, Callable, Collection, Iterable, Iterator, List, Sequence, cast, overload from typing_extensions import Final import mypy.typeops @@ -269,6 +269,7 @@ def span_from_context(ctx: Context) -> Iterable[int]: allow_dups=allow_dups, ) + @overload def fail( self, msg: str, @@ -278,10 +279,40 @@ def fail( file: str | None = None, allow_dups: bool = False, secondary_context: Context | None = None, + ) -> None: + ... + + @overload + def fail( + self, + msg: message_registry.ErrorMessage, + context: Context | None, + *, + file: str | None = None, + allow_dups: bool = False, + secondary_context: Context | None = None, + ) -> None: + ... + + def fail( + self, + msg: str | message_registry.ErrorMessage, + context: Context | None, + *, + code: ErrorCode | None = None, + file: str | None = None, + allow_dups: bool = False, + secondary_context: Context | None = None, ) -> None: """Report an error message (unless disabled).""" + if isinstance(msg, message_registry.ErrorMessage): + msg_str = msg.value + code = msg.code + else: + msg_str = msg + self.report( - msg, + msg_str, context, "error", code=code,