From 8c2649b1e8d148f3a9c9dc7459d9f7fbb18d2cde Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Fri, 19 Jul 2019 14:06:56 +0100 Subject: [PATCH 01/17] WIP basic support for error codes --- mypy/build.py | 4 +- mypy/errors.py | 66 ++++++++++++++++++---------- mypy/main.py | 3 ++ mypy/messages.py | 43 +++++++++++++----- mypy/options.py | 1 + mypy/test/testcheck.py | 1 + test-data/unit/check-errorcodes.test | 10 +++++ 7 files changed, 93 insertions(+), 35 deletions(-) create mode 100644 test-data/unit/check-errorcodes.test diff --git a/mypy/build.py b/mypy/build.py index d8fd4eb292a4..febecb962671 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -198,7 +198,9 @@ def _build(sources: List[BuildSource], reports = Reports(data_dir, options.report_dirs) source_set = BuildSourceSet(sources) - errors = Errors(options.show_error_context, options.show_column_numbers) + errors = Errors(options.show_error_context, + options.show_column_numbers, + options.show_error_codes) plugin, snapshot = load_plugins(options, errors, stdout) # Construct a build manager object to hold state during the build. diff --git a/mypy/errors.py b/mypy/errors.py index 11ae673ad93b..914142908409 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -9,6 +9,7 @@ from mypy.scope import Scope from mypy.options import Options from mypy.version import __version__ as mypy_version +from mypy.errorcodes import ErrorCode T = TypeVar('T') allowed_duplicates = ['@overload', 'Got:', 'Expected:'] # type: Final @@ -45,6 +46,9 @@ class ErrorInfo: # The error message. message = '' + # The error code. + code = None # type: Optional[ErrorCode] + # If True, we should halt build after the file that generated this error. blocker = False @@ -68,6 +72,7 @@ def __init__(self, column: int, severity: str, message: str, + code: Optional[ErrorCode], blocker: bool, only_once: bool, origin: Optional[Tuple[str, int, int]] = None, @@ -81,12 +86,23 @@ def __init__(self, self.column = column self.severity = severity self.message = message + self.code = code self.blocker = blocker self.only_once = only_once self.origin = origin or (file, line, line) self.target = target +# Type used internally to represent errors: +# (path, line, column, severity, message, code) +ErrorTuple = Tuple[Optional[str], + int, + int, + str, + str, + Optional[ErrorCode]] + + class Errors: """Container for compile errors. @@ -135,10 +151,13 @@ class Errors: target_module = None # type: Optional[str] scope = None # type: Optional[Scope] - def __init__(self, show_error_context: bool = False, - show_column_numbers: bool = False) -> None: + def __init__(self, + show_error_context: bool = False, + show_column_numbers: bool = False, + show_error_codes: bool = False) -> None: self.show_error_context = show_error_context self.show_column_numbers = show_column_numbers + self.show_error_codes = show_error_codes self.initialize() def initialize(self) -> None: @@ -226,6 +245,8 @@ def report(self, line: int, column: Optional[int], message: str, + code: Optional[ErrorCode] = None, + *, blocker: bool = False, severity: str = 'error', file: Optional[str] = None, @@ -237,7 +258,9 @@ def report(self, Args: line: line number of error + column: column number of error message: message to report + code: error code (defaults to 'misc' for 'error' severity) blocker: if True, don't continue analysis after this error severity: 'error' or 'note' file: if non-None, override current file as context @@ -268,7 +291,7 @@ def report(self, end_line = origin_line info = ErrorInfo(self.import_context(), file, self.current_module(), type, - function, line, column, severity, message, + function, line, column, severity, message, code, blocker, only_once, origin=(self.file, origin_line, end_line), target=self.current_target()) @@ -323,7 +346,7 @@ def generate_unused_ignore_errors(self, file: str) -> None: # Don't use report since add_error_info will ignore the error! info = ErrorInfo(self.import_context(), file, self.current_module(), None, None, line, -1, 'error', "unused 'type: ignore' comment", - False, False) + None, False, False) self._add_error_info(file, info) def is_typeshed_file(self, file: str) -> bool: @@ -373,7 +396,7 @@ def format_messages(self, error_info: List[ErrorInfo]) -> List[str]: a = [] # type: List[str] errors = self.render_messages(self.sort_messages(error_info)) errors = self.remove_duplicates(errors) - for file, line, column, severity, message in errors: + for file, line, column, severity, message, code in errors: s = '' if file is not None: if self.show_column_numbers and line >= 0 and column >= 0: @@ -385,6 +408,8 @@ def format_messages(self, error_info: List[ErrorInfo]) -> List[str]: s = '{}: {}: {}'.format(srcloc, severity, message) else: s = message + if self.show_error_codes and code: + s = '{} [{}]'.format(s, code.code) a.append(s) return a @@ -420,18 +445,16 @@ def targets(self) -> Set[str]: for info in errs if info.target) - def render_messages(self, errors: List[ErrorInfo]) -> List[Tuple[Optional[str], int, int, - str, str]]: + def render_messages(self, + errors: List[ErrorInfo]) -> List[ErrorTuple]: """Translate the messages into a sequence of tuples. - Each tuple is of form (path, line, col, severity, message). + Each tuple is of form (path, line, col, severity, message, code). The rendered sequence includes information about error contexts. The path item may be None. If the line item is negative, the line number is not defined for the tuple. """ - result = [] # type: List[Tuple[Optional[str], int, int, str, str]] - # (path, line, column, severity, message) - + result = [] # type: List[ErrorTuple] prev_import_context = [] # type: List[Tuple[str, int]] prev_function_or_member = None # type: Optional[str] prev_type = None # type: Optional[str] @@ -455,7 +478,7 @@ def render_messages(self, errors: List[ErrorInfo]) -> List[Tuple[Optional[str], # Remove prefix to ignore from path (if present) to # simplify path. path = remove_path_prefix(path, self.ignore_prefix) - result.append((None, -1, -1, 'note', fmt.format(path, line))) + result.append((None, -1, -1, 'note', fmt.format(path, line), None)) i -= 1 file = self.simplify_path(e.file) @@ -467,27 +490,27 @@ def render_messages(self, errors: List[ErrorInfo]) -> List[Tuple[Optional[str], e.type != prev_type): if e.function_or_member is None: if e.type is None: - result.append((file, -1, -1, 'note', 'At top level:')) + result.append((file, -1, -1, 'note', 'At top level:', None)) else: result.append((file, -1, -1, 'note', 'In class "{}":'.format( - e.type))) + e.type), None)) else: if e.type is None: result.append((file, -1, -1, 'note', 'In function "{}":'.format( - e.function_or_member))) + e.function_or_member), None)) else: result.append((file, -1, -1, 'note', 'In member "{}" of class "{}":'.format( - e.function_or_member, e.type))) + e.function_or_member, e.type), None)) elif e.type != prev_type: if e.type is None: - result.append((file, -1, -1, 'note', 'At top level:')) + result.append((file, -1, -1, 'note', 'At top level:', None)) else: result.append((file, -1, -1, 'note', - 'In class "{}":'.format(e.type))) + 'In class "{}":'.format(e.type), None)) - result.append((file, e.line, e.column, e.severity, e.message)) + result.append((file, e.line, e.column, e.severity, e.message, e.code)) prev_import_context = e.import_ctx prev_function_or_member = e.function_or_member @@ -518,10 +541,9 @@ def sort_messages(self, errors: List[ErrorInfo]) -> List[ErrorInfo]: result.extend(a) return result - def remove_duplicates(self, errors: List[Tuple[Optional[str], int, int, str, str]] - ) -> List[Tuple[Optional[str], int, int, str, str]]: + def remove_duplicates(self, errors: List[ErrorTuple]) -> List[ErrorTuple]: """Remove duplicates from a sorted error list.""" - res = [] # type: List[Tuple[Optional[str], int, int, str, str]] + res = [] # type: List[ErrorTuple] i = 0 while i < len(errors): dup = False diff --git a/mypy/main.py b/mypy/main.py index 188e8c2fcb9f..63c3c6e449b1 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -568,6 +568,9 @@ def add_invertible_flag(flag: str, add_invertible_flag('--show-column-numbers', default=False, help="Show column numbers in error messages", group=error_group) + add_invertible_flag('--show-error-codes', default=False, + help="Show error codes in error messages", + group=error_group) strict_help = "Strict mode; enables the following flags: {}".format( ", ".join(strict_flag_names)) diff --git a/mypy/messages.py b/mypy/messages.py index c77b6b802165..079b0f26b35a 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -32,7 +32,9 @@ CallExpr ) from mypy.util import unmangle -from mypy import message_registry +from mypy.errorcodes import ErrorCode +from mypy import message_registry, errorcodes + ARG_CONSTRUCTOR_NAMES = { ARG_POS: "Arg", @@ -104,8 +106,14 @@ def enable_errors(self) -> None: def is_errors(self) -> bool: return self.errors.is_errors() - def report(self, msg: str, context: Optional[Context], severity: str, - file: Optional[str] = None, origin: Optional[Context] = None, + def report(self, + msg: str, + context: Optional[Context], + severity: str, + *, + code: Optional[ErrorCode] = None, + file: Optional[str] = None, + origin: Optional[Context] = None, offset: int = 0) -> None: """Report an error or note (unless disabled).""" if origin is not None: @@ -119,12 +127,18 @@ def report(self, msg: str, context: Optional[Context], severity: str, context.get_column() if context else -1, msg, severity=severity, file=file, offset=offset, origin_line=origin.get_line() if origin else None, - end_line=end_line) - - def fail(self, msg: str, context: Optional[Context], file: Optional[str] = None, + end_line=end_line, + code=code) + + def fail(self, + msg: str, + context: Optional[Context], + *, + code: Optional[ErrorCode] = None, + file: Optional[str] = None, origin: Optional[Context] = None) -> None: """Report an error message (unless disabled).""" - self.report(msg, context, 'error', file=file, origin=origin) + self.report(msg, context, 'error', code=code, file=file, origin=origin) def note(self, msg: str, context: Context, file: Optional[str] = None, origin: Optional[Context] = None, offset: int = 0) -> None: @@ -220,13 +234,18 @@ def has_no_attr(self, original_type: Type, typ: Type, member: str, context: Cont # TODO: Handle differences in division between Python 2 and 3 more cleanly matches = [] if matches: - self.fail('{} has no attribute "{}"; maybe {}?{}'.format( - format_type(original_type), member, pretty_or(matches), extra), - context) + self.fail( + '{} has no attribute "{}"; maybe {}?{}'.format( + format_type(original_type), member, pretty_or(matches), extra), + context, + code=errorcodes.ATTR_DEFINED) failed = True if not failed: - self.fail('{} has no attribute "{}"{}'.format(format_type(original_type), - member, extra), context) + self.fail( + '{} has no attribute "{}"{}'.format( + format_type(original_type),member, extra), + context, + code=errorcodes.ATTR_DEFINED) elif isinstance(original_type, UnionType): # The checker passes "object" in lieu of "None" for attribute # checks, so we manually convert it back. diff --git a/mypy/options.py b/mypy/options.py index 0aeb939d9e97..fabd6f22fa97 100644 --- a/mypy/options.py +++ b/mypy/options.py @@ -242,6 +242,7 @@ def __init__(self) -> None: # -- experimental options -- self.shadow_file = None # type: Optional[List[List[str]]] self.show_column_numbers = False # type: bool + self.show_error_codes = False self.dump_graph = False self.dump_deps = False self.logical_deps = False diff --git a/mypy/test/testcheck.py b/mypy/test/testcheck.py index 88f4ed7899e6..61be7bce24a3 100644 --- a/mypy/test/testcheck.py +++ b/mypy/test/testcheck.py @@ -86,6 +86,7 @@ 'check-newsemanal.test', 'check-inline-config.test', 'check-reports.test', + 'check-errorcodes.test', ] # Tests that use Python 3.8-only AST features (like expression-scoped ignores): diff --git a/test-data/unit/check-errorcodes.test b/test-data/unit/check-errorcodes.test new file mode 100644 index 000000000000..fbd3ab144dae --- /dev/null +++ b/test-data/unit/check-errorcodes.test @@ -0,0 +1,10 @@ +[case testErrorCodeAttribute] +# flags: --show-error-codes +import m +m.x +'x'.foobar +[file m.py] +[builtins fixtures/module.pyi] +[out] +main:3: error: Module has no attribute "x" [attr-defined] +main:4: error: "str" has no attribute "foobar" [attr-defined] From 8d03a6658d0f8ecb15131192dec6abb87cd7cfd5 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Fri, 19 Jul 2019 14:08:29 +0100 Subject: [PATCH 02/17] Clean up test --- mypy/test/testcheck.py | 2 ++ test-data/unit/check-errorcodes.test | 8 ++------ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/mypy/test/testcheck.py b/mypy/test/testcheck.py index 61be7bce24a3..5bb54d4e3aa0 100644 --- a/mypy/test/testcheck.py +++ b/mypy/test/testcheck.py @@ -167,6 +167,8 @@ def run_case_once(self, testcase: DataDrivenTestCase, options.new_semantic_analyzer = True if 'columns' in testcase.file: options.show_column_numbers = True + if 'errorcodes' in testcase.file: + options.show_error_codes = True if incremental_step and options.incremental: # Don't overwrite # flags: --no-incremental in incremental test cases diff --git a/test-data/unit/check-errorcodes.test b/test-data/unit/check-errorcodes.test index fbd3ab144dae..0d1507f4af9c 100644 --- a/test-data/unit/check-errorcodes.test +++ b/test-data/unit/check-errorcodes.test @@ -1,10 +1,6 @@ [case testErrorCodeAttribute] -# flags: --show-error-codes import m -m.x -'x'.foobar +m.x # E: Module has no attribute "x" [attr-defined] +'x'.foobar # E: "str" has no attribute "foobar" [attr-defined] [file m.py] [builtins fixtures/module.pyi] -[out] -main:3: error: Module has no attribute "x" [attr-defined] -main:4: error: "str" has no attribute "foobar" [attr-defined] From e7ac143580354cea80e54caa8ea539a4cbafe6c7 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Fri, 19 Jul 2019 14:20:04 +0100 Subject: [PATCH 03/17] WIP semantic analyzer errors --- mypy/messages.py | 6 +++--- mypy/newsemanal/semanal.py | 23 +++++++++++++---------- test-data/unit/check-errorcodes.test | 9 ++++++++- 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/mypy/messages.py b/mypy/messages.py index 079b0f26b35a..00b75d8199c7 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -33,7 +33,7 @@ ) from mypy.util import unmangle from mypy.errorcodes import ErrorCode -from mypy import message_registry, errorcodes +from mypy import message_registry, errorcodes as codes ARG_CONSTRUCTOR_NAMES = { @@ -238,14 +238,14 @@ def has_no_attr(self, original_type: Type, typ: Type, member: str, context: Cont '{} has no attribute "{}"; maybe {}?{}'.format( format_type(original_type), member, pretty_or(matches), extra), context, - code=errorcodes.ATTR_DEFINED) + code=codes.ATTR_DEFINED) failed = True if not failed: self.fail( '{} has no attribute "{}"{}'.format( format_type(original_type),member, extra), context, - code=errorcodes.ATTR_DEFINED) + code=codes.ATTR_DEFINED) elif isinstance(original_type, UnionType): # The checker passes "object" in lieu of "None" for attribute # checks, so we manually convert it back. diff --git a/mypy/newsemanal/semanal.py b/mypy/newsemanal/semanal.py index edb9112bdc9c..46a95b874022 100644 --- a/mypy/newsemanal/semanal.py +++ b/mypy/newsemanal/semanal.py @@ -81,7 +81,8 @@ from mypy.visitor import NodeVisitor from mypy.errors import Errors, report_internal_error from mypy.messages import best_matches, MessageBuilder, pretty_or -from mypy import message_registry +from mypy.errorcodes import ErrorCode +from mypy import message_registry, errorcodes as codes from mypy.types import ( FunctionLike, UnboundType, TypeVarDef, TupleType, UnionType, StarType, function_type, CallableType, Overloaded, Instance, Type, AnyType, LiteralType, LiteralValue, @@ -3172,12 +3173,12 @@ def visit_for_stmt(self, s: ForStmt) -> None: def visit_break_stmt(self, s: BreakStmt) -> None: self.statement = s if self.loop_depth == 0: - self.fail("'break' outside loop", s, True, blocker=True) + self.fail("'break' outside loop", s, serious=True, blocker=True) def visit_continue_stmt(self, s: ContinueStmt) -> None: self.statement = s if self.loop_depth == 0: - self.fail("'continue' outside loop", s, True, blocker=True) + self.fail("'continue' outside loop", s, serious=True, blocker=True) def visit_if_stmt(self, s: IfStmt) -> None: self.statement = s @@ -3367,10 +3368,10 @@ def visit_star_expr(self, expr: StarExpr) -> None: def visit_yield_from_expr(self, e: YieldFromExpr) -> None: if not self.is_func_scope(): # not sure - self.fail("'yield from' outside function", e, True, blocker=True) + self.fail("'yield from' outside function", e, serious=True, blocker=True) else: if self.function_stack[-1].is_coroutine: - self.fail("'yield from' in async function", e, True, blocker=True) + self.fail("'yield from' in async function", e, serious=True, blocker=True) else: self.function_stack[-1].is_generator = True if e.expr: @@ -3747,11 +3748,11 @@ def visit__promote_expr(self, expr: PromoteExpr) -> None: def visit_yield_expr(self, expr: YieldExpr) -> None: if not self.is_func_scope(): - self.fail("'yield' outside function", expr, True, blocker=True) + self.fail("'yield' outside function", expr, serious=True, blocker=True) else: if self.function_stack[-1].is_coroutine: if self.options.python_version < (3, 6): - self.fail("'yield' in async function", expr, True, blocker=True) + self.fail("'yield' in async function", expr, serious=True, blocker=True) else: self.function_stack[-1].is_generator = True self.function_stack[-1].is_async_generator = True @@ -4455,7 +4456,7 @@ def name_not_defined(self, name: str, ctx: Context, namespace: Optional[str] = N self.record_incomplete_ref() return message = "Name '{}' is not defined".format(name) - self.fail(message, ctx) + self.fail(message, ctx, code=codes.NAME_DEFINED) if 'builtins.{}'.format(name) in SUGGESTED_TEST_FIXTURES: # The user probably has a missing definition in a test fixture. Let's verify. @@ -4529,7 +4530,9 @@ def is_local_name(self, name: str) -> bool: def fail(self, msg: str, ctx: Context, - serious: bool = False, *, + serious: bool = False, + *, + code: Optional[ErrorCode] = None, blocker: bool = False) -> None: if (not serious and not self.options.check_untyped_defs and @@ -4538,7 +4541,7 @@ def fail(self, return # In case it's a bug and we don't really have context assert ctx is not None, msg - self.errors.report(ctx.get_line(), ctx.get_column(), msg, blocker=blocker) + self.errors.report(ctx.get_line(), ctx.get_column(), msg, blocker=blocker, code=code) def fail_blocker(self, msg: str, ctx: Context) -> None: self.fail(msg, ctx, blocker=True) diff --git a/test-data/unit/check-errorcodes.test b/test-data/unit/check-errorcodes.test index 0d1507f4af9c..f1967794d50d 100644 --- a/test-data/unit/check-errorcodes.test +++ b/test-data/unit/check-errorcodes.test @@ -1,6 +1,13 @@ -[case testErrorCodeAttribute] +[case testErrorCodeNoAttribute] import m m.x # E: Module has no attribute "x" [attr-defined] 'x'.foobar # E: "str" has no attribute "foobar" [attr-defined] [file m.py] [builtins fixtures/module.pyi] + +[case testErrorCodeUndefinedName] +x # E: Name 'x' is not defined [name-defined] +def f() -> None: + y # E: Name 'y' is not defined [name-defined] +[file m.py] +[builtins fixtures/module.pyi] From cf7a01530de6aefb28e990acf2e6d8a633530952 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Fri, 19 Jul 2019 14:23:34 +0100 Subject: [PATCH 04/17] WIP default to 'misc' category for errors --- mypy/errors.py | 4 ++++ test-data/unit/check-errorcodes.test | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/mypy/errors.py b/mypy/errors.py index 914142908409..5d2444d412fd 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -10,6 +10,7 @@ from mypy.options import Options from mypy.version import __version__ as mypy_version from mypy.errorcodes import ErrorCode +from mypy import errorcodes as codes T = TypeVar('T') allowed_duplicates = ['@overload', 'Got:', 'Expected:'] # type: Final @@ -290,6 +291,9 @@ def report(self, if end_line is None: end_line = origin_line + if severity == 'error' and code is None: + code = codes.MISC + info = ErrorInfo(self.import_context(), file, self.current_module(), type, function, line, column, severity, message, code, blocker, only_once, diff --git a/test-data/unit/check-errorcodes.test b/test-data/unit/check-errorcodes.test index f1967794d50d..08a5cd9bf850 100644 --- a/test-data/unit/check-errorcodes.test +++ b/test-data/unit/check-errorcodes.test @@ -11,3 +11,9 @@ def f() -> None: y # E: Name 'y' is not defined [name-defined] [file m.py] [builtins fixtures/module.pyi] + +[case testErrorCodeUnclassifiedError] +class A: + def __init__(self) -> int: \ + # E: The return type of "__init__" must be None [misc] + pass From bd3bd36d4e005aedc3cca3562407a443511a4951 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Fri, 19 Jul 2019 14:24:29 +0100 Subject: [PATCH 05/17] WIP add test case --- test-data/unit/check-errorcodes.test | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test-data/unit/check-errorcodes.test b/test-data/unit/check-errorcodes.test index 08a5cd9bf850..a12e6ec105b9 100644 --- a/test-data/unit/check-errorcodes.test +++ b/test-data/unit/check-errorcodes.test @@ -17,3 +17,6 @@ class A: def __init__(self) -> int: \ # E: The return type of "__init__" must be None [misc] pass + +[case testErrorCodeNoteHasNoCode] +reveal_type(1) # N: Revealed type is 'builtins.int' From d7356fc700adff1e3af824581bded7c4e4158dde Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 23 Jul 2019 12:37:34 +0100 Subject: [PATCH 06/17] WIP may be incomplete --- mypy/fastparse.py | 24 ++++++++++++++++++------ test-data/unit/check-errorcodes.test | 10 +++++++++- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/mypy/fastparse.py b/mypy/fastparse.py index 76e849726008..f65e2c078e6e 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -33,7 +33,8 @@ TypeOfAny, Instance, RawExpressionType, ) from mypy import defaults -from mypy import message_registry +from mypy.errorcodes import ErrorCode +from mypy import message_registry, errorcodes as codes from mypy.errors import Errors from mypy.options import Options from mypy.reachability import mark_block_unreachable @@ -173,7 +174,7 @@ def parse(source: Union[str, bytes], tree.path = fnam tree.is_stub = is_stub_file except SyntaxError as e: - errors.report(e.lineno, e.offset, e.msg, blocker=True) + errors.report(e.lineno, e.offset, e.msg, blocker=True, code=codes.SYNTAX) tree = MypyFile([], [], False, set()) if raise_on_error and errors.is_errors(): @@ -269,9 +270,17 @@ def __init__(self, def note(self, msg: str, line: int, column: int) -> None: self.errors.report(line, column, msg, severity='note') - def fail(self, msg: str, line: int, column: int, blocker: bool = True) -> None: + def fail(self, + msg: str, + line: int, + column: int, + blocker: bool = True, + *, + code: Optional[ErrorCode] = None) -> None: + if code is None: + code = codes.SYNTAX if blocker or not self.options.ignore_errors: - self.errors.report(line, column, msg, blocker=blocker) + self.errors.report(line, column, msg, blocker=blocker, code=code) def visit(self, node: Optional[AST]) -> Any: if node is None: @@ -1287,9 +1296,12 @@ def parent(self) -> Optional[AST]: return None return self.node_stack[-2] - def fail(self, msg: str, line: int, column: int) -> None: + def fail(self, msg: str, line: int, column: int, *, code: Optional[ErrorCode] = None) -> None: + print(1, code) + if code is None: + code = codes.SYNTAX if self.errors: - self.errors.report(line, column, msg, blocker=True) + self.errors.report(line, column, msg, blocker=True, code=code) def note(self, msg: str, line: int, column: int) -> None: if self.errors: diff --git a/test-data/unit/check-errorcodes.test b/test-data/unit/check-errorcodes.test index a12e6ec105b9..d37ffbe6a406 100644 --- a/test-data/unit/check-errorcodes.test +++ b/test-data/unit/check-errorcodes.test @@ -19,4 +19,12 @@ class A: pass [case testErrorCodeNoteHasNoCode] -reveal_type(1) # N: Revealed type is 'builtins.int' +reveal_type(1) # N: Revealed type is 'builtins.int' + +[case testErrorCodeSyntaxError] +1 '' # E: invalid syntax [syntax] + +[case testErrorCodeSyntaxError2] +def f(): # E: Type signature has too many arguments [syntax] + # type: (int) -> None + 1 From 451254303905b2c97fda5ab52d9a457f596a7b21 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 23 Jul 2019 16:48:32 +0100 Subject: [PATCH 07/17] More WIP --- mypy/errorcodes.py | 50 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 mypy/errorcodes.py diff --git a/mypy/errorcodes.py b/mypy/errorcodes.py new file mode 100644 index 000000000000..adbb815252a5 --- /dev/null +++ b/mypy/errorcodes.py @@ -0,0 +1,50 @@ +"""Classification of possible errors mypy can detect. + +These can be used for filtering specific errors. +""" + +from typing import List +from typing_extensions import Final + + +# All created error codes are implicitly stored in this list. +all_error_codes = [] # type: List[ErrorCode] + + +class ErrorCode: + def __init__(self, code: str, description: str, category: str) -> None: + self.code = code + self.description = description + self.category = category + + def __str__(self) -> str: + return ''.format(self.code) + + +ATTR_DEFINED = ErrorCode( + 'attr-defined', "Check that attribute exists", 'General') # type: Final +NAME_DEFINED = ErrorCode( + 'name-defined', "Check that name is defined", 'General') # type: Final +CALL_ARG = ErrorCode( + 'call-arg', "Check number, names and kinds of arguments in calls", 'General') # type: Final +ARG_TYPE = ErrorCode( + 'arg-type', "Check argument types in calls", 'General') # type: Final +VALID_TYPE = ErrorCode( + 'valid-type', "Check that type (annotation) is valid", 'General') # type: Final +MISSING_ANN = ErrorCode( + 'var-annotated', "Require variable annotation if type can't be inferred", + 'General') # type: Final +OVERRIDE = ErrorCode( + 'override', "Check that method override is compatible with base class", + 'General') # type: Final +RETURN_VALUE = ErrorCode( + 'return-value', "Check that return value is compatible with signature", + 'General') # type: Final +ASSIGNMENT = ErrorCode( + 'assignment', "Check that assigned value is compatible with target", 'General') # type: Final + +SYNTAX = ErrorCode( + 'syntax', "Report syntax errors", 'General') # type: Final + +MISC = ErrorCode( + 'misc', "Miscenallenous other checks", 'General') # type: Final From 282fd9eca1832aa342c9c844d560344b09a1628d Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 25 Jul 2019 17:23:34 +0100 Subject: [PATCH 08/17] Add error code to Python 2 syntax errors --- mypy/fastparse.py | 15 ++++----------- mypy/fastparse2.py | 6 +++--- test-data/unit/check-errorcodes.test | 12 ++++++++++++ 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/mypy/fastparse.py b/mypy/fastparse.py index f65e2c078e6e..e412f40d574f 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -274,13 +274,9 @@ def fail(self, msg: str, line: int, column: int, - blocker: bool = True, - *, - code: Optional[ErrorCode] = None) -> None: - if code is None: - code = codes.SYNTAX + blocker: bool = True) -> None: if blocker or not self.options.ignore_errors: - self.errors.report(line, column, msg, blocker=blocker, code=code) + self.errors.report(line, column, msg, blocker=blocker, code=codes.SYNTAX) def visit(self, node: Optional[AST]) -> Any: if node is None: @@ -1296,12 +1292,9 @@ def parent(self) -> Optional[AST]: return None return self.node_stack[-2] - def fail(self, msg: str, line: int, column: int, *, code: Optional[ErrorCode] = None) -> None: - print(1, code) - if code is None: - code = codes.SYNTAX + def fail(self, msg: str, line: int, column: int) -> None: if self.errors: - self.errors.report(line, column, msg, blocker=True, code=code) + self.errors.report(line, column, msg, blocker=True, code=codes.SYNTAX) def note(self, msg: str, line: int, column: int) -> None: if self.errors: diff --git a/mypy/fastparse2.py b/mypy/fastparse2.py index 3fefbf9661f1..675e8a2ac0f1 100644 --- a/mypy/fastparse2.py +++ b/mypy/fastparse2.py @@ -41,7 +41,7 @@ from mypy.types import ( Type, CallableType, AnyType, UnboundType, EllipsisType, TypeOfAny, Instance, ) -from mypy import message_registry +from mypy import message_registry, errorcodes as codes from mypy.errors import Errors from mypy.fastparse import TypeConverter, parse_type_comment, bytes_to_human_readable_repr from mypy.options import Options @@ -111,7 +111,7 @@ def parse(source: Union[str, bytes], tree.path = fnam tree.is_stub = is_stub_file except SyntaxError as e: - errors.report(e.lineno, e.offset, e.msg, blocker=True) + errors.report(e.lineno, e.offset, e.msg, blocker=True, code=codes.SYNTAX) tree = MypyFile([], [], False, set()) if raise_on_error and errors.is_errors(): @@ -166,7 +166,7 @@ def __init__(self, def fail(self, msg: str, line: int, column: int, blocker: bool = True) -> None: if blocker or not self.options.ignore_errors: - self.errors.report(line, column, msg, blocker=blocker) + self.errors.report(line, column, msg, blocker=blocker, code=codes.SYNTAX) def visit(self, node: Optional[AST]) -> Any: # same as in typed_ast stub if node is None: diff --git a/test-data/unit/check-errorcodes.test b/test-data/unit/check-errorcodes.test index d37ffbe6a406..e73ba43b21dd 100644 --- a/test-data/unit/check-errorcodes.test +++ b/test-data/unit/check-errorcodes.test @@ -1,3 +1,7 @@ +-- Tests for error codes +-- +-- These implicitly use --show-error-codes. + [case testErrorCodeNoAttribute] import m m.x # E: Module has no attribute "x" [attr-defined] @@ -28,3 +32,11 @@ reveal_type(1) # N: Revealed type is 'builtins.int' def f(): # E: Type signature has too many arguments [syntax] # type: (int) -> None 1 + +[case testErrorCodeSyntaxError_python2] +1 '' # E: invalid syntax [syntax] + +[case testErrorCodeSyntaxError2_python2] +def f(): # E: Type signature has too many arguments [syntax] + # type: (int) -> None + 1 From eb0ab56a6b42625e7d1a6ee9d01158a47bc213d0 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Fri, 26 Jul 2019 13:29:48 +0100 Subject: [PATCH 09/17] Basic `# type: ignore` support --- mypy/errors.py | 21 ++++-- mypy/fastparse.py | 98 ++++++++++++++-------------- mypy/fastparse2.py | 58 +++++++--------- mypy/nodes.py | 10 +-- mypy/treetransform.py | 4 +- test-data/unit/check-errorcodes.test | 20 ++++++ 6 files changed, 118 insertions(+), 93 deletions(-) diff --git a/mypy/errors.py b/mypy/errors.py index 5d2444d412fd..2329483ab5cb 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -128,8 +128,9 @@ class Errors: # Path to current file. file = '' # type: str - # Ignore errors on these lines of each file. - ignored_lines = None # type: Dict[str, Set[int]] + # Ignore some errors on these lines of each file + # (path -> line -> error-codes) + ignored_lines = None # type: Dict[str, Dict[int, List[str]]] # Lines on which an error was actually ignored. used_ignored_lines = None # type: Dict[str, Set[int]] @@ -217,7 +218,7 @@ def set_file(self, file: str, self.scope = scope def set_file_ignored_lines(self, file: str, - ignored_lines: Set[int], + ignored_lines: Dict[int, List[str]], ignore_all: bool = False) -> None: self.ignored_lines[file] = ignored_lines if ignore_all: @@ -320,7 +321,7 @@ def add_error_info(self, info: ErrorInfo) -> None: # Check each line in this context for "type: ignore" comments. # line == end_line for most nodes, so we only loop once. for scope_line in range(line, end_line + 1): - if scope_line in self.ignored_lines[file]: + if self.is_ignored_error(scope_line, info, self.ignored_lines[file]): # Annotation requests us to ignore all errors on this line. self.used_ignored_lines[file].add(scope_line) return @@ -332,6 +333,16 @@ def add_error_info(self, info: ErrorInfo) -> None: self.only_once_messages.add(info.message) 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: + return False + elif not ignores[line]: + # Empty list means that we ignore all errors + return True + elif info.code: + return info.code.code in ignores[line] + return False + 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: @@ -346,7 +357,7 @@ def clear_errors_in_targets(self, path: str, targets: Set[str]) -> None: def generate_unused_ignore_errors(self, file: str) -> None: ignored_lines = self.ignored_lines[file] if not self.is_typeshed_file(file) and file not in self.ignored_files: - for line in ignored_lines - self.used_ignored_lines[file]: + for line in set(ignored_lines) - self.used_ignored_lines[file]: # Don't use report since add_error_info will ignore the error! info = ErrorInfo(self.import_context(), file, self.current_module(), None, None, line, -1, 'error', "unused 'type: ignore' comment", diff --git a/mypy/fastparse.py b/mypy/fastparse.py index e412f40d574f..9358245bd5fa 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -125,7 +125,7 @@ def ast3_parse(source: Union[str, bytes], filename: str, mode: str, TYPE_COMMENT_SYNTAX_ERROR = 'syntax error in type comment' # type: Final -TYPE_IGNORE_PATTERN = re.compile(r'[^#]*#\s*type:\s*ignore\s*($|#)') +TYPE_IGNORE_PATTERN = re.compile(r'[^#]*#\s*type:\s*ignore\s*(\[[^[#]*\]\s*)?($|#)') # Older versions of typing don't allow using overload outside stubs, @@ -175,7 +175,7 @@ def parse(source: Union[str, bytes], tree.is_stub = is_stub_file except SyntaxError as e: errors.report(e.lineno, e.offset, e.msg, blocker=True, code=codes.SYNTAX) - tree = MypyFile([], [], False, set()) + tree = MypyFile([], [], False, {}) if raise_on_error and errors.is_errors(): errors.raise_error() @@ -183,12 +183,25 @@ def parse(source: Union[str, bytes], return tree +def parse_type_ignore_tag(tag: str) -> List[str]: + if not tag: + return [] + m = re.match(r'\[(.*)\]', tag) + if m is None: + return [] + return [m.group(1)] + + def parse_type_comment(type_comment: str, line: int, column: int, errors: Optional[Errors], assume_str_is_unicode: bool = True, - ) -> Tuple[bool, Optional[Type]]: + ) -> Tuple[Optional[List[str]], Optional[Type]]: + """Parse type portion of a type comment (+ optional type ignore). + + Return (ignore info, parsed type). + """ try: typ = ast3_parse(type_comment, '', 'eval') except SyntaxError as e: @@ -196,17 +209,22 @@ def parse_type_comment(type_comment: str, stripped_type = type_comment.split("#", 2)[0].strip() err_msg = "{} '{}'".format(TYPE_COMMENT_SYNTAX_ERROR, stripped_type) errors.report(line, e.offset, err_msg, blocker=True) - return False, None + return None, None else: raise else: - extra_ignore = TYPE_IGNORE_PATTERN.match(type_comment) is not None + extra_ignore = TYPE_IGNORE_PATTERN.match(type_comment) + if extra_ignore: + tag = extra_ignore.group(1) + ignored = parse_type_ignore_tag(tag) # type: Optional[List[str]] + else: + ignored = None assert isinstance(typ, ast3_Expression) converted = TypeConverter(errors, line=line, override_column=column, assume_str_is_unicode=assume_str_is_unicode).visit(typ.body) - return extra_ignore, converted + return ignored, converted def parse_type_string(expr_string: str, expr_fallback_name: str, @@ -262,7 +280,7 @@ def __init__(self, self.is_stub = is_stub self.errors = errors - self.type_ignores = set() # type: Set[int] + self.type_ignores = {} # type: Dict[int, List[str]] # Cache of visit_X methods keyed by type of visited object self.visitor_cache = {} # type: Dict[type, Callable[[Optional[AST]], Any]] @@ -327,6 +345,21 @@ def translate_stmt_list(self, return res + def translate_type_comment(self, + n: Union[ast3.stmt, ast3.arg], + type_comment: Optional[str]) -> Optional[Type]: + if type_comment is None: + return None + else: + lineno = n.lineno + extra_ignore, typ = parse_type_comment(type_comment, + lineno, + n.col_offset, + self.errors) + if extra_ignore is not None: + self.type_ignores[lineno] = extra_ignore + return typ + op_map = { ast3.Add: '+', ast3.Sub: '-', @@ -429,7 +462,8 @@ def translate_module_id(self, id: str) -> str: return id def visit_Module(self, mod: ast3.Module) -> MypyFile: - self.type_ignores = {ti.lineno for ti in mod.type_ignores} + self.type_ignores = {ti.lineno: parse_type_ignore_tag(ti.tag) # type: ignore + for ti in mod.type_ignores} body = self.fix_function_overloads(self.translate_stmt_list(mod.body, ismodule=True)) return MypyFile(body, self.imports, @@ -644,12 +678,8 @@ def make_argument(self, arg: ast3.arg, default: Optional[ast3.expr], kind: int, arg_type = None if annotation is not None: arg_type = TypeConverter(self.errors, line=arg.lineno).visit(annotation) - elif type_comment is not None: - extra_ignore, arg_type = parse_type_comment(type_comment, arg.lineno, - arg.col_offset, self.errors) - if extra_ignore: - self.type_ignores.add(arg.lineno) - + else: + arg_type = self.translate_type_comment(arg, type_comment) return Argument(Var(arg.arg), arg_type, self.visit(default), kind) def fail_arg(self, msg: str, arg: ast3.arg) -> None: @@ -703,13 +733,7 @@ def visit_Delete(self, n: ast3.Delete) -> DelStmt: def visit_Assign(self, n: ast3.Assign) -> AssignmentStmt: lvalues = self.translate_expr_list(n.targets) rvalue = self.visit(n.value) - if n.type_comment is not None: - extra_ignore, typ = parse_type_comment(n.type_comment, n.lineno, n.col_offset, - self.errors) - if extra_ignore: - self.type_ignores.add(n.lineno) - else: - typ = None + typ = self.translate_type_comment(n, n.type_comment) s = AssignmentStmt(lvalues, rvalue, type=typ, new_syntax=False) return self.set_line(s, n) @@ -738,13 +762,7 @@ def visit_NamedExpr(self, n: NamedExpr) -> None: # For(expr target, expr iter, stmt* body, stmt* orelse, string? type_comment) def visit_For(self, n: ast3.For) -> ForStmt: - if n.type_comment is not None: - extra_ignore, target_type = parse_type_comment(n.type_comment, n.lineno, n.col_offset, - self.errors) - if extra_ignore: - self.type_ignores.add(n.lineno) - else: - target_type = None + target_type = self.translate_type_comment(n, n.type_comment) node = ForStmt(self.visit(n.target), self.visit(n.iter), self.as_required_block(n.body, n.lineno), @@ -754,13 +772,7 @@ def visit_For(self, n: ast3.For) -> ForStmt: # AsyncFor(expr target, expr iter, stmt* body, stmt* orelse, string? type_comment) def visit_AsyncFor(self, n: ast3.AsyncFor) -> ForStmt: - if n.type_comment is not None: - extra_ignore, target_type = parse_type_comment(n.type_comment, n.lineno, n.col_offset, - self.errors) - if extra_ignore: - self.type_ignores.add(n.lineno) - else: - target_type = None + target_type = self.translate_type_comment(n, n.type_comment) node = ForStmt(self.visit(n.target), self.visit(n.iter), self.as_required_block(n.body, n.lineno), @@ -786,13 +798,7 @@ def visit_If(self, n: ast3.If) -> IfStmt: # With(withitem* items, stmt* body, string? type_comment) def visit_With(self, n: ast3.With) -> WithStmt: - if n.type_comment is not None: - extra_ignore, target_type = parse_type_comment(n.type_comment, n.lineno, - n.col_offset, self.errors) - if extra_ignore: - self.type_ignores.add(n.lineno) - else: - target_type = None + target_type = self.translate_type_comment(n, n.type_comment) node = WithStmt([self.visit(i.context_expr) for i in n.items], [self.visit(i.optional_vars) for i in n.items], self.as_required_block(n.body, n.lineno), @@ -801,13 +807,7 @@ def visit_With(self, n: ast3.With) -> WithStmt: # AsyncWith(withitem* items, stmt* body, string? type_comment) def visit_AsyncWith(self, n: ast3.AsyncWith) -> WithStmt: - if n.type_comment is not None: - extra_ignore, target_type = parse_type_comment(n.type_comment, n.lineno, - n.col_offset, self.errors) - if extra_ignore: - self.type_ignores.add(n.lineno) - else: - target_type = None + target_type = self.translate_type_comment(n, n.type_comment) s = WithStmt([self.visit(i.context_expr) for i in n.items], [self.visit(i.optional_vars) for i in n.items], self.as_required_block(n.body, n.lineno), diff --git a/mypy/fastparse2.py b/mypy/fastparse2.py index 675e8a2ac0f1..690554c67ff3 100644 --- a/mypy/fastparse2.py +++ b/mypy/fastparse2.py @@ -43,7 +43,9 @@ ) from mypy import message_registry, errorcodes as codes from mypy.errors import Errors -from mypy.fastparse import TypeConverter, parse_type_comment, bytes_to_human_readable_repr +from mypy.fastparse import ( + TypeConverter, parse_type_comment, bytes_to_human_readable_repr, parse_type_ignore_tag +) from mypy.options import Options from mypy.reachability import mark_block_unreachable @@ -112,7 +114,7 @@ def parse(source: Union[str, bytes], tree.is_stub = is_stub_file except SyntaxError as e: errors.report(e.lineno, e.offset, e.msg, blocker=True, code=codes.SYNTAX) - tree = MypyFile([], [], False, set()) + tree = MypyFile([], [], False, {}) if raise_on_error and errors.is_errors(): errors.raise_error() @@ -162,7 +164,7 @@ def __init__(self, # Cache of visit_X methods keyed by type of visited object self.visitor_cache = {} # type: Dict[type, Callable[[Optional[AST]], Any]] - self.type_ignores = set() # type: Set[int] + self.type_ignores = {} # type: Dict[int, List[str]] def fail(self, msg: str, line: int, column: int, blocker: bool = True) -> None: if blocker or not self.options.ignore_errors: @@ -216,6 +218,20 @@ def translate_stmt_list(self, res.append(node) return res + def translate_type_comment(self, n: ast27.stmt, type_comment: Optional[str]) -> Optional[Type]: + if type_comment is None: + return None + else: + lineno = n.lineno + extra_ignore, typ = parse_type_comment(type_comment, + lineno, + n.col_offset, + self.errors, + assume_str_is_unicode=self.unicode_literals) + if extra_ignore is not None: + self.type_ignores[lineno] = extra_ignore + return typ + op_map = { ast27.Add: '+', ast27.Sub: '-', @@ -319,7 +335,8 @@ def translate_module_id(self, id: str) -> str: return id def visit_Module(self, mod: ast27.Module) -> MypyFile: - self.type_ignores = {ti.lineno for ti in mod.type_ignores} + self.type_ignores = {ti.lineno: parse_type_ignore_tag(ti.tag) # type: ignore + for ti in mod.type_ignores} body = self.fix_function_overloads(self.translate_stmt_list(mod.body)) return MypyFile(body, self.imports, @@ -569,16 +586,7 @@ def visit_Delete(self, n: ast27.Delete) -> DelStmt: # Assign(expr* targets, expr value, string? type_comment) def visit_Assign(self, n: ast27.Assign) -> AssignmentStmt: - typ = None - if n.type_comment: - extra_ignore, typ = parse_type_comment(n.type_comment, - n.lineno, - n.col_offset, - self.errors, - assume_str_is_unicode=self.unicode_literals) - if extra_ignore: - self.type_ignores.add(n.lineno) - + typ = self.translate_type_comment(n, n.type_comment) stmt = AssignmentStmt(self.translate_expr_list(n.targets), self.visit(n.value), type=typ) @@ -593,16 +601,7 @@ def visit_AugAssign(self, n: ast27.AugAssign) -> OperatorAssignmentStmt: # For(expr target, expr iter, stmt* body, stmt* orelse, string? type_comment) def visit_For(self, n: ast27.For) -> ForStmt: - if n.type_comment is not None: - extra_ignore, typ = parse_type_comment(n.type_comment, - n.lineno, - n.col_offset, - self.errors, - assume_str_is_unicode=self.unicode_literals) - if extra_ignore: - self.type_ignores.add(n.lineno) - else: - typ = None + typ = self.translate_type_comment(n, n.type_comment) stmt = ForStmt(self.visit(n.target), self.visit(n.iter), self.as_required_block(n.body, n.lineno), @@ -626,16 +625,7 @@ def visit_If(self, n: ast27.If) -> IfStmt: # With(withitem* items, stmt* body, string? type_comment) def visit_With(self, n: ast27.With) -> WithStmt: - if n.type_comment is not None: - extra_ignore, typ = parse_type_comment(n.type_comment, - n.lineno, - n.col_offset, - self.errors, - assume_str_is_unicode=self.unicode_literals) - if extra_ignore: - self.type_ignores.add(n.lineno) - else: - typ = None + typ = self.translate_type_comment(n, n.type_comment) stmt = WithStmt([self.visit(n.context_expr)], [self.visit(n.optional_vars)], self.as_required_block(n.body, n.lineno), diff --git a/mypy/nodes.py b/mypy/nodes.py index 44f5fe610039..cce5f2a04eac 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -243,8 +243,10 @@ class MypyFile(SymbolNode): names = None # type: SymbolTable # All import nodes within the file (also ones within functions etc.) imports = None # type: List[ImportBase] - # Lines to ignore when checking - ignored_lines = None # type: Set[int] + # Lines on which to ignore certain errors when checking. + # If the value is empty, ignore all errors; otherwise, the list contains all + # error codes to ignore. + ignored_lines = None # type: Dict[int, List[str]] # Is this file represented by a stub file (.pyi)? is_stub = False # Is this loaded from the cache and thus missing the actual body of the file? @@ -260,7 +262,7 @@ def __init__(self, defs: List[Statement], imports: List['ImportBase'], is_bom: bool = False, - ignored_lines: Optional[Set[int]] = None) -> None: + ignored_lines: Optional[Dict[int, List[str]]] = None) -> None: super().__init__() self.defs = defs self.line = 1 # Dummy line number @@ -271,7 +273,7 @@ def __init__(self, if ignored_lines: self.ignored_lines = ignored_lines else: - self.ignored_lines = set() + self.ignored_lines = {} def local_definitions(self) -> Iterator[Definition]: """Return all definitions within the module (including nested). diff --git a/mypy/treetransform.py b/mypy/treetransform.py index d84eb681cb0e..2ab1e8789330 100644 --- a/mypy/treetransform.py +++ b/mypy/treetransform.py @@ -59,8 +59,10 @@ def __init__(self) -> None: def visit_mypy_file(self, node: MypyFile) -> MypyFile: # NOTE: The 'names' and 'imports' instance variables will be empty! + ignored_lines = {line: codes[:] + for line, codes in node.ignored_lines.items()} new = MypyFile(self.statements(node.defs), [], node.is_bom, - ignored_lines=set(node.ignored_lines)) + ignored_lines=ignored_lines) new._fullname = node._fullname new.path = node.path new.names = SymbolTable() diff --git a/test-data/unit/check-errorcodes.test b/test-data/unit/check-errorcodes.test index e73ba43b21dd..451af5e6d7be 100644 --- a/test-data/unit/check-errorcodes.test +++ b/test-data/unit/check-errorcodes.test @@ -40,3 +40,23 @@ def f(): # E: Type signature has too many arguments [syntax] def f(): # E: Type signature has too many arguments [syntax] # type: (int) -> None 1 + +[case testErrorCodeIgnore1] +'x'.foobar # type: ignore[attr-defined] +'x'.foobar # type: ignore[xyz] # E: "str" has no attribute "foobar" [attr-defined] +'x'.foobar # type: ignore + +[case testErrorCodeIgnore2] +a = 'x'.foobar # type: int # type: ignore[attr-defined] +b = 'x'.foobar # type: int # type: ignore[xyz] # E: "str" has no attribute "foobar" [attr-defined] +c = 'x'.foobar # type: int # type: ignore + +[case testErrorCodeIgnore1_python2] +'x'.foobar # type: ignore[attr-defined] +'x'.foobar # type: ignore[xyz] # E: "str" has no attribute "foobar" [attr-defined] +'x'.foobar # type: ignore + +[case testErrorCodeIgnore2_python2] +a = 'x'.foobar # type: int # type: ignore[attr-defined] +b = 'x'.foobar # type: int # type: ignore[xyz] # E: "str" has no attribute "foobar" [attr-defined] +c = 'x'.foobar # type: int # type: ignore From 9b2ddc09b3ec3a73067eea7da0c58944a4204d0b Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Fri, 26 Jul 2019 16:53:59 +0100 Subject: [PATCH 10/17] Support ignoring multiple error codes --- mypy/fastparse.py | 4 ++-- test-data/unit/check-errorcodes.test | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/mypy/fastparse.py b/mypy/fastparse.py index 9358245bd5fa..3f58a64e6412 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -186,10 +186,10 @@ def parse(source: Union[str, bytes], def parse_type_ignore_tag(tag: str) -> List[str]: if not tag: return [] - m = re.match(r'\[(.*)\]', tag) + m = re.match(r'\[([^#]*)\]', tag) if m is None: return [] - return [m.group(1)] + return [code.strip() for code in m.group(1).split(',')] def parse_type_comment(type_comment: str, diff --git a/test-data/unit/check-errorcodes.test b/test-data/unit/check-errorcodes.test index 451af5e6d7be..b67ce6eff6be 100644 --- a/test-data/unit/check-errorcodes.test +++ b/test-data/unit/check-errorcodes.test @@ -60,3 +60,8 @@ c = 'x'.foobar # type: int # type: ignore a = 'x'.foobar # type: int # type: ignore[attr-defined] b = 'x'.foobar # type: int # type: ignore[xyz] # E: "str" has no attribute "foobar" [attr-defined] c = 'x'.foobar # type: int # type: ignore + +[case testErrorCodeIgnoreMultiple] +a = 'x'.foobar(b) # type: ignore[name-defined, attr-defined] +a = 'x'.foobar(b) # type: ignore[name-defined, xyz] # E: "str" has no attribute "foobar" [attr-defined] +a = 'x'.foobar(b) # type: ignore[xyz, w, attr-defined] # E: Name 'b' is not defined [name-defined] From 9e558bcb1c760611950b525a80777a71fcecb862 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Fri, 26 Jul 2019 16:55:04 +0100 Subject: [PATCH 11/17] Add TODO comment --- mypy/fastparse.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mypy/fastparse.py b/mypy/fastparse.py index 3f58a64e6412..e549ec3cea0a 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -184,6 +184,7 @@ def parse(source: Union[str, bytes], def parse_type_ignore_tag(tag: str) -> List[str]: + # TODO: Implement proper parsing and error checking if not tag: return [] m = re.match(r'\[([^#]*)\]', tag) From af51696038f58d11c860723902388d57beffbbc0 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Fri, 26 Jul 2019 16:57:11 +0100 Subject: [PATCH 12/17] Add test cases --- test-data/unit/check-errorcodes.test | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/test-data/unit/check-errorcodes.test b/test-data/unit/check-errorcodes.test index b67ce6eff6be..104308dc66d3 100644 --- a/test-data/unit/check-errorcodes.test +++ b/test-data/unit/check-errorcodes.test @@ -61,7 +61,20 @@ a = 'x'.foobar # type: int # type: ignore[attr-defined] b = 'x'.foobar # type: int # type: ignore[xyz] # E: "str" has no attribute "foobar" [attr-defined] c = 'x'.foobar # type: int # type: ignore -[case testErrorCodeIgnoreMultiple] +[case testErrorCodeIgnoreMultiple1] a = 'x'.foobar(b) # type: ignore[name-defined, attr-defined] a = 'x'.foobar(b) # type: ignore[name-defined, xyz] # E: "str" has no attribute "foobar" [attr-defined] a = 'x'.foobar(b) # type: ignore[xyz, w, attr-defined] # E: Name 'b' is not defined [name-defined] + +[case testErrorCodeIgnoreMultiple2] +a = 'x'.foobar(b) # type: int # type: ignore[name-defined, attr-defined] +b = 'x'.foobar(b) # type: int # type: ignore[name-defined, xyz] # E: "str" has no attribute "foobar" [attr-defined] + +[case testErrorCodeIgnoreMultiple1_python2] +a = 'x'.foobar(b) # type: ignore[name-defined, attr-defined] +a = 'x'.foobar(b) # type: ignore[name-defined, xyz] # E: "str" has no attribute "foobar" [attr-defined] +a = 'x'.foobar(b) # type: ignore[xyz, w, attr-defined] # E: Name 'b' is not defined [name-defined] + +[case testErrorCodeIgnoreMultiple2_python2] +a = 'x'.foobar(b) # type: int # type: ignore[name-defined, attr-defined] +b = 'x'.foobar(b) # type: int # type: ignore[name-defined, xyz] # E: "str" has no attribute "foobar" [attr-defined] From 62c0f261b0616163bb94a67861aa00b8603007db Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Fri, 26 Jul 2019 18:17:14 +0100 Subject: [PATCH 13/17] Fix type ignore after Python 2 argument comment --- mypy/fastparse2.py | 15 ++++++++++--- test-data/unit/check-errorcodes.test | 32 ++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/mypy/fastparse2.py b/mypy/fastparse2.py index 690554c67ff3..b57f712f9a4c 100644 --- a/mypy/fastparse2.py +++ b/mypy/fastparse2.py @@ -44,7 +44,8 @@ from mypy import message_registry, errorcodes as codes from mypy.errors import Errors from mypy.fastparse import ( - TypeConverter, parse_type_comment, bytes_to_human_readable_repr, parse_type_ignore_tag + TypeConverter, parse_type_comment, bytes_to_human_readable_repr, parse_type_ignore_tag, + TYPE_IGNORE_PATTERN ) from mypy.options import Options from mypy.reachability import mark_block_unreachable @@ -533,12 +534,20 @@ def convert_arg(self, index: int, arg: ast27.expr, line: int, raise RuntimeError("'{}' is not a valid argument.".format(ast27.dump(arg))) return Var(v) - def get_type(self, i: int, type_comments: Sequence[Optional[str]], + def get_type(self, + i: int, + type_comments: Sequence[Optional[str]], converter: TypeConverter) -> Optional[Type]: if i < len(type_comments): comment = type_comments[i] if comment is not None: - return converter.visit_raw_str(comment) + typ = converter.visit_raw_str(comment) + extra_ignore = TYPE_IGNORE_PATTERN.match(comment) + if extra_ignore: + tag = extra_ignore.group(1) + ignored = parse_type_ignore_tag(tag) + self.type_ignores[converter.line] = ignored + return typ return None def stringify_name(self, n: AST) -> str: diff --git a/test-data/unit/check-errorcodes.test b/test-data/unit/check-errorcodes.test index 104308dc66d3..0893808345bb 100644 --- a/test-data/unit/check-errorcodes.test +++ b/test-data/unit/check-errorcodes.test @@ -78,3 +78,35 @@ a = 'x'.foobar(b) # type: ignore[xyz, w, attr-defined] # E: Name 'b' is not de [case testErrorCodeIgnoreMultiple2_python2] a = 'x'.foobar(b) # type: int # type: ignore[name-defined, attr-defined] b = 'x'.foobar(b) # type: int # type: ignore[name-defined, xyz] # E: "str" has no attribute "foobar" [attr-defined] + +[case testErrorCodeIgnoreAfterArgComment] +def f(x # type: xyz # type: ignore[name-defined] # Comment + ): + # type () -> None + pass + +def g(x # type: xyz # type: ignore # Comment + ): + # type () -> None + pass + +def h(x # type: xyz # type: ignore[foo] # E: Name 'xyz' is not defined [name-defined] + ): + # type () -> None + pass + +[case testErrorCodeIgnoreAfterArgComment_python2] +def f(x # type: xyz # type: ignore[name-defined] # Comment + ): + # type () -> None + pass + +def g(x # type: xyz # type: ignore # Comment + ): + # type () -> None + pass + +def h(x # type: xyz # type: ignore[foo] # E: Name 'xyz' is not defined [name-defined] + ): + # type () -> None + pass From a3aee9740ff73cb8b98a5f407150f363bff1e8f3 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Fri, 26 Jul 2019 18:24:08 +0100 Subject: [PATCH 14/17] Fix lint --- mypy/fastparse.py | 3 +-- mypy/fastparse2.py | 2 +- mypy/messages.py | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/mypy/fastparse.py b/mypy/fastparse.py index e549ec3cea0a..96ff25b8203d 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -3,7 +3,7 @@ import typing # for typing.Type, which conflicts with types.Type from typing import ( - Tuple, Union, TypeVar, Callable, Sequence, Optional, Any, Dict, cast, List, overload, Set + Tuple, Union, TypeVar, Callable, Sequence, Optional, Any, Dict, cast, List, overload ) from typing_extensions import Final, Literal @@ -33,7 +33,6 @@ TypeOfAny, Instance, RawExpressionType, ) from mypy import defaults -from mypy.errorcodes import ErrorCode from mypy import message_registry, errorcodes as codes from mypy.errors import Errors from mypy.options import Options diff --git a/mypy/fastparse2.py b/mypy/fastparse2.py index b57f712f9a4c..22eaabdfd387 100644 --- a/mypy/fastparse2.py +++ b/mypy/fastparse2.py @@ -17,7 +17,7 @@ import sys import typing # for typing.Type, which conflicts with types.Type -from typing import Tuple, Union, TypeVar, Callable, Sequence, Optional, Any, Dict, cast, List, Set +from typing import Tuple, Union, TypeVar, Callable, Sequence, Optional, Any, Dict, cast, List from typing_extensions import Final, Literal from mypy.sharedparse import ( diff --git a/mypy/messages.py b/mypy/messages.py index 00b75d8199c7..85c65662d233 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -243,7 +243,7 @@ def has_no_attr(self, original_type: Type, typ: Type, member: str, context: Cont if not failed: self.fail( '{} has no attribute "{}"{}'.format( - format_type(original_type),member, extra), + format_type(original_type), member, extra), context, code=codes.ATTR_DEFINED) elif isinstance(original_type, UnionType): From 28b357a1c04950d42bd43436b827304eeb6773d5 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Wed, 7 Aug 2019 16:01:40 +0300 Subject: [PATCH 15/17] Force new semantic analyzer --- mypy/test/testcheck.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mypy/test/testcheck.py b/mypy/test/testcheck.py index 5d3bd2e2c384..0445a12178b4 100644 --- a/mypy/test/testcheck.py +++ b/mypy/test/testcheck.py @@ -170,6 +170,7 @@ def run_case_once(self, testcase: DataDrivenTestCase, options.show_column_numbers = True if 'errorcodes' in testcase.file: options.show_error_codes = True + options.new_semantic_analyzer = True if incremental_step and options.incremental: # Don't overwrite # flags: --no-incremental in incremental test cases From d81cb87d21d255c909a3928a3ac866fd2463e120 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Wed, 7 Aug 2019 18:35:30 +0300 Subject: [PATCH 16/17] Attempt to fix mypyc compiled version --- mypy/fastparse.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mypy/fastparse.py b/mypy/fastparse.py index 96ff25b8203d..cf6e374c41a6 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -182,7 +182,7 @@ def parse(source: Union[str, bytes], return tree -def parse_type_ignore_tag(tag: str) -> List[str]: +def parse_type_ignore_tag(tag: Optional[str]) -> List[str]: # TODO: Implement proper parsing and error checking if not tag: return [] @@ -215,7 +215,8 @@ def parse_type_comment(type_comment: str, else: extra_ignore = TYPE_IGNORE_PATTERN.match(type_comment) if extra_ignore: - tag = extra_ignore.group(1) + # Typeshed has a non-optional return type for group! + tag = cast(Any, extra_ignore).group(1) # type: Optional[str] ignored = parse_type_ignore_tag(tag) # type: Optional[List[str]] else: ignored = None From 19253816cc9ad63b12f549ecf1dca3650b14c259 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Wed, 7 Aug 2019 19:06:49 +0300 Subject: [PATCH 17/17] Fix also fastparse2 --- mypy/fastparse2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/fastparse2.py b/mypy/fastparse2.py index 22eaabdfd387..1545fbde10e6 100644 --- a/mypy/fastparse2.py +++ b/mypy/fastparse2.py @@ -544,7 +544,7 @@ def get_type(self, typ = converter.visit_raw_str(comment) extra_ignore = TYPE_IGNORE_PATTERN.match(comment) if extra_ignore: - tag = extra_ignore.group(1) + tag = cast(Any, extra_ignore).group(1) # type: Optional[str] ignored = parse_type_ignore_tag(tag) self.type_ignores[converter.line] = ignored return typ