diff --git a/docs/source/command_line.rst b/docs/source/command_line.rst index 10760f418026..ed40803510d4 100644 --- a/docs/source/command_line.rst +++ b/docs/source/command_line.rst @@ -553,6 +553,36 @@ of the above sections. Note: the exact list of flags enabled by running :option:`--strict` may change over time. +.. option:: --disable-error-code + + This flag allows disabling one or multiple error codes globally. + + .. code-block:: python + + # no flag + x = 'a string' + x.trim() # error: "str" has no attribute "trim" [attr-defined] + + # --disable-error-code attr-defined + x = 'a string' + x.trim() + +.. option:: --enable-error-code + + This flag allows enabling one or multiple error codes globally. + + Note: This flag will override disabled error codes from the --disable-error-code + flag + + .. code-block:: python + + # --disable-error-code attr-defined + x = 'a string' + x.trim() + + # --disable-error-code attr-defined --enable-error-code attr-defined + x = 'a string' + x.trim() # error: "str" has no attribute "trim" [attr-defined] .. _configuring-error-messages: diff --git a/mypy/build.py b/mypy/build.py index 8d84196af642..c94ca94a3d70 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -223,7 +223,9 @@ def _build(sources: List[BuildSource], options.show_error_codes, options.pretty, lambda path: read_py_file(path, cached_read, options.python_version), - options.show_absolute_path) + options.show_absolute_path, + options.enabled_error_codes, + options.disabled_error_codes) plugin, snapshot = load_plugins(options, errors, stdout, extra_plugins) # Add catch-all .gitignore to cache dir if we created it diff --git a/mypy/errorcodes.py b/mypy/errorcodes.py index 47206c53e9de..0df1989d17dd 100644 --- a/mypy/errorcodes.py +++ b/mypy/errorcodes.py @@ -3,19 +3,26 @@ These can be used for filtering specific errors. """ -from typing import List +from typing import Dict, List from typing_extensions import Final # All created error codes are implicitly stored in this list. all_error_codes = [] # type: List[ErrorCode] +error_codes = {} # type: Dict[str, ErrorCode] + class ErrorCode: - def __init__(self, code: str, description: str, category: str) -> None: + def __init__(self, code: str, + description: str, + category: str, + default_enabled: bool = True) -> None: self.code = code self.description = description self.category = category + self.default_enabled = default_enabled + error_codes[code] = self def __str__(self) -> str: return ''.format(self.code) diff --git a/mypy/errors.py b/mypy/errors.py index b3747658b6f3..465bc5f0cabd 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -164,7 +164,9 @@ def __init__(self, show_error_codes: bool = False, pretty: bool = False, read_source: Optional[Callable[[str], Optional[List[str]]]] = None, - show_absolute_path: bool = False) -> None: + show_absolute_path: bool = False, + enabled_error_codes: Optional[Set[ErrorCode]] = None, + disabled_error_codes: Optional[Set[ErrorCode]] = None) -> None: self.show_error_context = show_error_context self.show_column_numbers = show_column_numbers self.show_error_codes = show_error_codes @@ -172,6 +174,8 @@ def __init__(self, self.pretty = pretty # We use fscache to read source code when showing snippets. self.read_source = read_source + self.enabled_error_codes = enabled_error_codes or set() + self.disabled_error_codes = disabled_error_codes or set() self.initialize() def initialize(self) -> None: @@ -195,7 +199,9 @@ def copy(self) -> 'Errors': self.show_error_codes, self.pretty, self.read_source, - self.show_absolute_path) + self.show_absolute_path, + self.enabled_error_codes, + self.disabled_error_codes) new.file = self.file new.import_ctx = self.import_ctx[:] new.function_or_member = self.function_or_member[:] @@ -351,15 +357,25 @@ def add_error_info(self, info: ErrorInfo) -> None: self._add_error_info(file, info) def is_ignored_error(self, line: int, info: ErrorInfo, ignores: Dict[int, List[str]]) -> bool: - if line not in ignores: + if info.code and self.is_error_code_enabled(info.code) is False: + return True + elif line not in ignores: return False elif not ignores[line]: # Empty list means that we ignore all errors return True - elif info.code: + elif info.code and self.is_error_code_enabled(info.code) is True: return info.code.code in ignores[line] return False + def is_error_code_enabled(self, error_code: ErrorCode) -> bool: + if error_code in self.disabled_error_codes: + return False + elif error_code in self.enabled_error_codes: + return True + else: + return error_code.default_enabled + def clear_errors_in_targets(self, path: str, targets: Set[str]) -> None: """Remove errors in specific fine-grained targets within a file.""" if path in self.error_info_map: diff --git a/mypy/main.py b/mypy/main.py index 8e80364234b4..7038ce40a5c1 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -18,6 +18,7 @@ from mypy.find_sources import create_source_list, InvalidSourceList from mypy.fscache import FileSystemCache from mypy.errors import CompileError +from mypy.errorcodes import error_codes from mypy.options import Options, BuildType from mypy.config_parser import parse_version, parse_config_file from mypy.split_namespace import SplitNamespace @@ -612,6 +613,14 @@ def add_invertible_flag(flag: str, '--strict', action='store_true', dest='special-opts:strict', help=strict_help) + strictness_group.add_argument( + '--disable-error-code', metavar='NAME', action='append', default=[], + help="Disable a specific error code") + strictness_group.add_argument( + '--enable-error-code', metavar='NAME', action='append', default=[], + help="Enable a specific error code" + ) + error_group = parser.add_argument_group( title='Configuring error messages', description="Adjust the amount of detail shown in error messages.") @@ -860,6 +869,23 @@ def set_strict_flags() -> None: parser.error("You can't make a variable always true and always false (%s)" % ', '.join(sorted(overlap))) + # Process `--enable-error-code` and `--disable-error-code` flags + disabled_codes = set(options.disable_error_code) + enabled_codes = set(options.enable_error_code) + + valid_error_codes = set(error_codes.keys()) + + invalid_codes = (enabled_codes | disabled_codes) - valid_error_codes + if invalid_codes: + parser.error("Invalid error code(s): %s" % + ', '.join(sorted(invalid_codes))) + + options.disabled_error_codes |= {error_codes[code] for code in disabled_codes} + options.enabled_error_codes |= {error_codes[code] for code in enabled_codes} + + # Enabling an error code always overrides disabling + options.disabled_error_codes -= options.enabled_error_codes + # Set build flags. if options.strict_optional_whitelist is not None: # TODO: Deprecate, then kill this flag diff --git a/mypy/options.py b/mypy/options.py index 54e106d2efe7..975c3d8b40f6 100644 --- a/mypy/options.py +++ b/mypy/options.py @@ -3,12 +3,15 @@ import pprint import sys -from typing_extensions import Final +from typing_extensions import Final, TYPE_CHECKING from typing import Dict, List, Mapping, Optional, Pattern, Set, Tuple, Callable, Any from mypy import defaults from mypy.util import get_class_descriptors, replace_object_state +if TYPE_CHECKING: + from mypy.errors import ErrorCode + class BuildType: STANDARD = 0 # type: Final[int] @@ -177,6 +180,14 @@ def __init__(self) -> None: # Variable names considered False self.always_false = [] # type: List[str] + # Error codes to disable + self.disable_error_code = [] # type: List[str] + self.disabled_error_codes = set() # type: Set[ErrorCode] + + # Error codes to enable + self.enable_error_code = [] # type: List[str] + self.enabled_error_codes = set() # type: Set[ErrorCode] + # Use script name instead of __main__ self.scripts_are_modules = False diff --git a/test-data/unit/check-flags.test b/test-data/unit/check-flags.test index 0e23476c4d0e..865995376f5d 100644 --- a/test-data/unit/check-flags.test +++ b/test-data/unit/check-flags.test @@ -1530,3 +1530,48 @@ def f(a = None): no_implicit_optional = True \[mypy-m] no_implicit_optional = False + +[case testDisableErrorCode] +# flags: --disable-error-code attr-defined +x = 'should be fine' +x.trim() + +[case testDisableDifferentErrorCode] +# flags: --disable-error-code name-defined --show-error-code +x = 'should not be fine' +x.trim() # E: "str" has no attribute "trim" [attr-defined] + +[case testDisableMultipleErrorCode] +# flags: --disable-error-code attr-defined --disable-error-code return-value --show-error-code +x = 'should be fine' +x.trim() + +def bad_return_type() -> str: + return None + +bad_return_type('no args taken!') # E: Too many arguments for "bad_return_type" [call-arg] + +[case testEnableErrorCode] +# flags: --disable-error-code attr-defined --enable-error-code attr-defined --show-error-code +x = 'should be fine' +x.trim() # E: "str" has no attribute "trim" [attr-defined] + +[case testEnableDifferentErrorCode] +# flags: --disable-error-code attr-defined --enable-error-code name-defined --show-error-code +x = 'should not be fine' +x.trim() # E: "str" has no attribute "trim" [attr-defined] + +[case testEnableMultipleErrorCode] +# flags: \ + --disable-error-code attr-defined \ + --disable-error-code return-value \ + --disable-error-code call-arg \ + --enable-error-code attr-defined \ + --enable-error-code return-value --show-error-code +x = 'should be fine' +x.trim() # E: "str" has no attribute "trim" [attr-defined] + +def bad_return_type() -> str: + return None # E: Incompatible return value type (got "None", expected "str") [return-value] + +bad_return_type('no args taken!') diff --git a/test-data/unit/cmdline.test b/test-data/unit/cmdline.test index c8fbb512da01..24e3d31bc342 100644 --- a/test-data/unit/cmdline.test +++ b/test-data/unit/cmdline.test @@ -1092,3 +1092,53 @@ import foo.bar [out] src/foo/bar.py: error: Source file found twice under different module names: 'src.foo.bar' and 'foo.bar' == Return code: 2 + +[case testEnableInvalidErrorCode] +# cmd: mypy --enable-error-code YOLO test.py +[file test.py] +x = 1 +[out] +usage: mypy [-h] [-v] [-V] [more options; see below] + [-m MODULE] [-p PACKAGE] [-c PROGRAM_TEXT] [files ...] +mypy: error: Invalid error code(s): YOLO +== Return code: 2 + +[case testDisableInvalidErrorCode] +# cmd: mypy --disable-error-code YOLO test.py +[file test.py] +x = 1 +[out] +usage: mypy [-h] [-v] [-V] [more options; see below] + [-m MODULE] [-p PACKAGE] [-c PROGRAM_TEXT] [files ...] +mypy: error: Invalid error code(s): YOLO +== Return code: 2 + +[case testEnableAndDisableInvalidErrorCode] +# cmd: mypy --disable-error-code YOLO --enable-error-code YOLO2 test.py +[file test.py] +x = 1 +[out] +usage: mypy [-h] [-v] [-V] [more options; see below] + [-m MODULE] [-p PACKAGE] [-c PROGRAM_TEXT] [files ...] +mypy: error: Invalid error code(s): YOLO, YOLO2 +== Return code: 2 + +[case testEnableValidAndInvalidErrorCode] +# cmd: mypy --enable-error-code attr-defined --enable-error-code YOLO test.py +[file test.py] +x = 1 +[out] +usage: mypy [-h] [-v] [-V] [more options; see below] + [-m MODULE] [-p PACKAGE] [-c PROGRAM_TEXT] [files ...] +mypy: error: Invalid error code(s): YOLO +== Return code: 2 + +[case testDisableValidAndInvalidErrorCode] +# cmd: mypy --disable-error-code attr-defined --disable-error-code YOLO test.py +[file test.py] +x = 1 +[out] +usage: mypy [-h] [-v] [-V] [more options; see below] + [-m MODULE] [-p PACKAGE] [-c PROGRAM_TEXT] [files ...] +mypy: error: Invalid error code(s): YOLO +== Return code: 2