diff --git a/docs/source/error_codes.rst b/docs/source/error_codes.rst index e6aad9234ab3..a1706668c951 100644 --- a/docs/source/error_codes.rst +++ b/docs/source/error_codes.rst @@ -53,6 +53,43 @@ codes prefixed with ``mypy:`` will be validated: # OK: error code may be used by other type checker. y = 2 # type: ignore[something-else] +An alternative way to specify mypy-specific type ignores is to use +``# mypy: ignore[one-code, other-code]``. Such form is equivalent to writing +``# type: ignore[mypy:one-code, mypy:other-code]``. You can have both +``# type: ignore[...]`` and ``# mypy: ignore[...]`` on the same line, in this +case the error code lists will be merged. + +Note that to allow better interoperability with other tools, mypy recognizes +``# type: ignore`` and ``mypy: ignore`` in any position in a comment, including +after another "magic" comments used by other tools. To temporarily disable +an ignore, place it after an empty (i.e. whitespace only) comment. For example: + +.. code-block:: python + + from typing import Any + + # No error, type ignore is active. + x: str = 1 # noqa # pragma: no cover # type: ignore[assignment] + + # Error: Incompatible types in assignment (expression has type "int", variable has type "str") + y: str = 1 ## type: ignore[assignment] + + # Error: Incompatible types in assignment (expression has type "int", variable has type "str") + z: str = 1 # # type: ignore[assignment] + +The only exception to this logic, is if there is no other code on a line +except for the comment. In this case type ignores preceded by another comment +are disabled. This is done to avoid spurious errors about unused ignores in +a commented out block of code: + +.. code-block:: python + + # mypy: warn-unused-ignores + + x: int = 1 + # OK, no unused-ignore error below. + # x = "no way" # type: ignore[assignment] + Enabling/disabling specific error codes globally ------------------------------------------------ diff --git a/mypy/nativeparse.py b/mypy/nativeparse.py index 8b0a261a925f..e3e1e921f7e7 100644 --- a/mypy/nativeparse.py +++ b/mypy/nativeparse.py @@ -235,14 +235,25 @@ def native_parse( b, errors, ignores, + mypy_ignores, import_bytes, is_partial_package, uses_template_strings, source_hash, mypy_comments, ) = parse_to_binary_ast(filename, options, source, skip_function_bodies) + ignores, extra_errors = normalize_error_codes(ignores) errors.extend(extra_errors) + mypy_ignores, extra_errors = normalize_error_codes( + mypy_ignores, verify_all=True, prohibit_prefix=True + ) + errors.extend(extra_errors) + + ignores = merge_type_ignores(ignores, mypy_ignores) + ignores, extra_errors = remove_duplicate_codes(ignores) + errors.extend(extra_errors) + node = MypyFile([], []) node.path = filename node.raw_data = FileRawData( @@ -258,29 +269,89 @@ def native_parse( return node, errors, ignores -def normalize_error_codes(ignores: TypeIgnores) -> tuple[TypeIgnores, list[ParseError]]: +def normalize_error_codes( + ignores: TypeIgnores, verify_all: bool = False, prohibit_prefix: bool = False +) -> tuple[TypeIgnores, list[ParseError]]: """Remove mypy: prefix from error codes (if present).""" normalized = [] extra_errors: list[ParseError] = [] for line, codes in ignores: new_codes = [] for c in codes: - if not c.startswith("mypy:"): - new_codes.append(c) - continue + prefixed = c.startswith("mypy:") c = c.removeprefix("mypy:") - if c in error_codes: - new_codes.append(c) + # Always record unknown codes, so that type: ignore[unknown] + # will not become blanket ignore. + new_codes.append(c) + if (prefixed or verify_all) and c not in error_codes: + extra_errors.append( + { + "line": line, + "column": -1, + "message": f'Unrecognized error code "{c}"', + "blocker": False, + "code": "misc", + } + ) + if prefixed and prohibit_prefix: + extra_errors.append( + { + "line": line, + "column": -1, + "message": 'Error codes in "mypy: ignore" should not be prefixed', + "blocker": False, + "code": "misc", + } + ) + normalized.append((line, new_codes)) + return normalized, extra_errors + + +def merge_type_ignores(ignores: TypeIgnores, mypy_ignores: TypeIgnores) -> TypeIgnores: + """Merge error codes specified in # type: ignore[...] and in # mypy: ignore[...].""" + merged = [] + ignores_by_line = dict(ignores) + mypy_ignores_by_line = dict(mypy_ignores) + for line in sorted(ignores_by_line.keys() | mypy_ignores_by_line.keys()): + codes = ignores_by_line.get(line) + mypy_codes = mypy_ignores_by_line.get(line) + if codes is not None and mypy_codes is None: + new_codes = codes + elif codes is None and mypy_codes is not None: + new_codes = mypy_codes + else: + assert codes is not None and mypy_codes is not None + if not codes or not mypy_codes: + # If at least one ignore is blanket, make the merged ignore blanket. + # We are not overly strict here, in case someone wants to quickly + # silence everything. + new_codes = [] + else: + new_codes = codes + mypy_codes + merged.append((line, new_codes)) + return merged + + +def remove_duplicate_codes(ignores: TypeIgnores) -> tuple[TypeIgnores, list[ParseError]]: + normalized = [] + extra_errors: list[ParseError] = [] + for line, codes in ignores: + seen = set() + new_codes = [] + for c in codes: + if c in seen: + extra_errors.append( + { + "line": line, + "column": -1, + "message": f'Duplicate error code "{c}"', + "blocker": False, + "code": "misc", + } + ) continue - extra_errors.append( - { - "line": line, - "column": -1, - "message": f'Unrecognized error code "{c}"', - "blocker": False, - "code": "misc", - } - ) + seen.add(c) + new_codes.append(c) normalized.append((line, new_codes)) return normalized, extra_errors @@ -327,7 +398,17 @@ def parse_to_binary_ast( options: Options, source: str | bytes | None = None, skip_function_bodies: bool = False, -) -> tuple[bytes, list[ParseError], TypeIgnores, bytes, bool, bool, str, list[tuple[int, str]]]: +) -> tuple[ + bytes, + list[ParseError], + TypeIgnores, + TypeIgnores, + bytes, + bool, + bool, + str, + list[tuple[int, str]], +]: try: ast_bytes, errors, ignores, import_bytes, ast_data = ast_serialize.parse( filename, @@ -337,7 +418,7 @@ def parse_to_binary_ast( platform=options.platform, always_true=options.always_true, always_false=options.always_false, - cache_version=5, + cache_version=7, ) except ValueError as exc: if source is None: @@ -366,6 +447,7 @@ def parse_to_binary_ast( ast_bytes, errors, ignores, + ast_data["mypy_ignores"], import_bytes, ast_data["is_partial_package"], ast_data["uses_template_strings"], @@ -796,6 +878,7 @@ def read_func_def(state: State, data: ReadBuffer) -> FuncDef: func_def = FuncDef(name, arguments, body, typ=typ, type_args=type_params) if is_async: func_def.is_coroutine = True + func_def.has_type_comments = read_bool(data) read_loc(data, func_def) if type_params: state.check_min_version( diff --git a/mypy/nodes.py b/mypy/nodes.py index dc86550cebe4..20f7a0df804c 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -1114,6 +1114,7 @@ class FuncDef(FuncItem, SymbolNode, Statement): "docstring", "deprecated", "original_first_arg", + "has_type_comments", ) __match_args__ = ("name", "arguments", "type", "body") @@ -1156,6 +1157,9 @@ def __init__( # We record this status to avoid multiple (similar but different) errors in case # of partial types etc. self.is_invalid_redefinition = False + # Record if the function definition has any type comments (including per-argument). + # This is only available with the native parser (and always False otherwise). + self.has_type_comments = False @property def name(self) -> str: diff --git a/mypy/parse.py b/mypy/parse.py index cc1b9153f5dd..5ffbd20c674d 100644 --- a/mypy/parse.py +++ b/mypy/parse.py @@ -95,6 +95,7 @@ def load_from_raw( # Report parse errors, this replicates the logic in parse(). all_errors = raw_data.raw_errors + state.errors errors.set_file(fnam, module, options=options) + errors.set_file_ignored_lines(fnam, tree.ignored_lines, options.ignore_errors) for error in all_errors: # Note we never raise in this function, so it should not be called in coordinator. report_parse_error(error, errors) diff --git a/mypy/test/test_nativeparse.py b/mypy/test/test_nativeparse.py index efe3be63c97c..9ac377365038 100644 --- a/mypy/test/test_nativeparse.py +++ b/mypy/test/test_nativeparse.py @@ -268,15 +268,15 @@ def locs(start_line: int, start_column: int, end_line: int, end_column: int) -> def test_trivial_binary_data_from_file(self) -> None: with temp_source("print('hello')") as fnam: - b, _, _, _, _, _, _, _ = parse_to_binary_ast(fnam, Options()) + b, _, _, _, _, _, _, _, _ = parse_to_binary_ast(fnam, Options()) self._assert_trivial_binary_data(b) def test_trivial_binary_data_from_string_source(self) -> None: - b, _, _, _, _, _, _, _ = parse_to_binary_ast("", Options(), "print('hello')") + b, _, _, _, _, _, _, _, _ = parse_to_binary_ast("", Options(), "print('hello')") self._assert_trivial_binary_data(b) def test_trivial_binary_data_from_bytes_source(self) -> None: - b, _, _, _, _, _, _, _ = parse_to_binary_ast("", Options(), b"print('hello')") + b, _, _, _, _, _, _, _, _ = parse_to_binary_ast("", Options(), b"print('hello')") self._assert_trivial_binary_data(b) def test_invalid_bytes_raises(self) -> None: diff --git a/test-data/unit/check-deprecated.test b/test-data/unit/check-deprecated.test index 607e9d767956..10c5a7e69719 100644 --- a/test-data/unit/check-deprecated.test +++ b/test-data/unit/check-deprecated.test @@ -49,7 +49,7 @@ from typing_extensions import deprecated @deprecated("use f2 instead") def f() -> None: ... -f # E: function __main__.f is deprecated: use f2 instead # type: ignore[deprecated] +f # E: function __main__.f is deprecated: use f2 instead f(1) # E: function __main__.f is deprecated: use f2 instead \ # E: Too many arguments for "f" f[1] # E: function __main__.f is deprecated: use f2 instead \ diff --git a/test-data/unit/check-fastparse.test b/test-data/unit/check-fastparse.test index eaa3cd9a14fc..73d910334847 100644 --- a/test-data/unit/check-fastparse.test +++ b/test-data/unit/check-fastparse.test @@ -77,7 +77,8 @@ for i in (1, 2, 3, 100): # type: str # type: ignore pass [builtins fixtures/tuple.pyi] -[case testFastParseVariableCommentThenIgnore] +-- New parser has a different stance on this. +[case testFastParseVariableCommentThenIgnore_old_parser] a="test" # type: int #comment # type: ignore # E: Incompatible types in assignment (expression has type "str", variable has type "int") [case testFastParseProperty] diff --git a/test-data/unit/check-ignore.test b/test-data/unit/check-ignore.test index 71394dbc7da3..231733c809b0 100644 --- a/test-data/unit/check-ignore.test +++ b/test-data/unit/check-ignore.test @@ -309,3 +309,96 @@ x = 1 # type: ignore[mypy:foobar] main:2: error: Incompatible types in assignment (expression has type "int", variable has type "str") main:2: note: Error code "assignment" not covered by "type: ignore[arg-type, other:foobar]" comment main:3: error: Unrecognized error code "foobar" +main:3: note: Error code "misc" not covered by "type: ignore[foobar]" comment +main:3: error: Incompatible types in assignment (expression has type "int", variable has type "str") +main:3: note: Error code "assignment" not covered by "type: ignore[foobar]" comment + +[case testIgnoreNonBlockingParseError] +# flags: --python-version=3.10 +m[*(), :] # type: ignore[syntax] +[builtins fixtures/tuple.pyi] +[out] +main:2: error: Name "m" is not defined +main:2: note: Error code "name-defined" not covered by "type: ignore[syntax]" comment + +[case testIgnoreNonBlockingParseErrorSilent] +# cmd: mypy -m a +# flags: --python-version=3.10 --follow-import=silent +[file a.py] +import m +reveal_type(m.x) # N: Revealed type is "builtins.int" +[file m.py] +x: int +x = m[*(), :] +[builtins fixtures/tuple.pyi] +[out] + +[case testIgnoresWorkAfterOtherComments] +x: str = 1 # noqa # pragma: no cover # type: ignore +y: str = 1 # noqa # pragma: no cover # mypy: ignore +z: str = 1 # noqa # pragma: no cover # type: ignore[assignment] +t: str = 1 # noqa # pragma: no cover # mypy: ignore[assignment] + +x1: str = 1 ## type: ignore +y1: str = 1 ## mypy: ignore +z1: str = 1 ## type: ignore[assignment] +t1: str = 1 ## mypy: ignore[assignment] + +x2: str = 1 # noqa ## type: ignore +y2: str = 1 # noqa ## mypy: ignore +z2: str = 1 # noqa ## type: ignore[assignment] +t2: str = 1 # noqa ## mypy: ignore[assignment] +[out] +main:6: error: Incompatible types in assignment (expression has type "int", variable has type "str") +main:7: error: Incompatible types in assignment (expression has type "int", variable has type "str") +main:8: error: Incompatible types in assignment (expression has type "int", variable has type "str") +main:9: error: Incompatible types in assignment (expression has type "int", variable has type "str") +main:11: error: Incompatible types in assignment (expression has type "int", variable has type "str") +main:12: error: Incompatible types in assignment (expression has type "int", variable has type "str") +main:13: error: Incompatible types in assignment (expression has type "int", variable has type "str") +main:14: error: Incompatible types in assignment (expression has type "int", variable has type "str") + +[case testPrefixInMypyCommentNotAllowed] +x: str = 1 # mypy: ignore[mypy:assignment] +[out] +main:1: error: Error codes in "mypy: ignore" should not be prefixed +main:1: note: Error code "misc" not covered by "type: ignore[assignment]" comment + +[case testDuplicateErrorCodeNotAllowed] +x: str = 1 # mypy: ignore[assignment] # type: ignore[assignment] +[out] +main:1: error: Duplicate error code "assignment" +main:1: note: Error code "misc" not covered by "type: ignore[assignment]" comment + +[case testUnusedMypyIgnoreReported] +# flags: --warn-unused-ignores +x = 1 # mypy: ignore +y = 1 # mypy: ignore[assignment] +z: str = 1 # mypy: ignore[assignment, arg-type] +[out] +main:2: error: Unused "type: ignore" comment +main:3: error: Unused "type: ignore" comment +main:4: error: Unused "type: ignore[arg-type]" comment + +[case testNoUnusedIgnoreInCommentedBlock] +# flags: --warn-unused-ignores +# x: str = 1 # mypy: ignore +# y: str = 1 # type: ignore +# z: str = 1 # mypy: ignore[assignment] +# t: str = 1 # type: ignore[assignment] + +[case testIgnoreErrorCodesMerged] +def f() -> None: pass + +f(x) # mypy: ignore[name-defined] # type: ignore[call-arg] +f(x) # type: ignore[name-defined] # mypy: ignore[call-arg] + +[case testBlanketIgnoreWinsWhenMixed] +x # mypy: ignore # type: ignore[assignment] +y # mypy: ignore[assignment] # type: ignore + +[case testUnknownErrorCodeNotAllowedInMypyIgnore] +1 # mypy: ignore[whatever] +[out] +main:1: error: Unrecognized error code "whatever" +main:1: note: Error code "misc" not covered by "type: ignore[whatever]" comment