From a39ab0122b6dc7d0d3a551c9807e465fb87b4d11 Mon Sep 17 00:00:00 2001 From: Brandt Bucher Date: Mon, 8 Apr 2019 10:26:20 -0700 Subject: [PATCH 01/12] Give ignores expression-wide scope in Python 3.8. --- mypy/errors.py | 8 ++++---- mypy/fastparse.py | 34 +++++++++++++++++++++++----------- mypy/fastparse2.py | 4 ++-- mypy/nodes.py | 6 +++--- mypy/strconv.py | 4 ++-- mypy/treetransform.py | 2 +- 6 files changed, 35 insertions(+), 23 deletions(-) diff --git a/mypy/errors.py b/mypy/errors.py index 0053e3ec08c4..a5ef2e01b6d6 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -114,7 +114,7 @@ class Errors: file = '' # type: str # Ignore errors on these lines of each file. - ignored_lines = None # type: Dict[str, Set[int]] + ignored_lines = None # type: Dict[str, Dict[int, int]] # Lines on which an error was actually ignored. used_ignored_lines = None # type: Dict[str, Set[int]] @@ -199,7 +199,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, int], ignore_all: bool = False) -> None: self.ignored_lines[file] = ignored_lines if ignore_all: @@ -278,7 +278,7 @@ def add_error_info(self, info: ErrorInfo) -> None: if not info.blocker: # Blockers cannot be ignored if file in self.ignored_lines and line in self.ignored_lines[file]: # Annotation requests us to ignore all errors on this line. - self.used_ignored_lines[file].add(line) + self.used_ignored_lines[file].add(self.ignored_lines[file][line]) return if file in self.ignored_files: return @@ -300,7 +300,7 @@ def clear_errors_in_targets(self, path: str, targets: Set[str]) -> None: self.error_info_map[path] = new_errors def generate_unused_ignore_notes(self, file: str) -> None: - ignored_lines = self.ignored_lines[file] + ignored_lines = set(self.ignored_lines[file].values()) if not self.is_typeshed_file(file) and file not in self.ignored_files: for line in ignored_lines - self.used_ignored_lines[file]: # Don't use report since add_error_info will ignore the error! diff --git a/mypy/fastparse.py b/mypy/fastparse.py index d47e1a97148d..3e66ce4821bf 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -171,7 +171,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) - tree = MypyFile([], [], False, set()) + tree = MypyFile([], [], False, {}) if raise_on_error and errors.is_errors(): errors.raise_error() @@ -251,7 +251,7 @@ def __init__(self, self.is_stub = is_stub self.errors = errors - self.extra_type_ignores = [] # type: List[int] + self.type_ignores = {} # type: Dict[int, int] # Cache of visit_X methods keyed by type of visited object self.visitor_cache = {} # type: Dict[type, Callable[[Optional[AST]], Any]] @@ -271,8 +271,20 @@ def visit(self, node: Optional[AST]) -> Any: method = 'visit_' + node.__class__.__name__ visitor = getattr(self, method) self.visitor_cache[typeobj] = visitor + if (3, 8) < sys.version_info and isinstance(node, ast3.expr): + self.scope_ignores(node) return visitor(node) + def scope_ignores(self, node: ast3.expr) -> None: + end_lineno = getattr(node, "end_lineno", None) + if end_lineno is None: + return + node_lines = range(node.lineno, end_lineno + 1) + for line in node_lines: + if line in self.type_ignores.values(): + self.type_ignores.update(dict.fromkeys(node_lines, line)) + return + def set_line(self, node: N, n: Union[ast3.expr, ast3.stmt]) -> N: node.line = n.lineno node.column = n.col_offset @@ -394,13 +406,13 @@ def translate_module_id(self, id: str) -> str: return id def visit_Module(self, mod: ast3.Module) -> MypyFile: + self.type_ignores = {ti.lineno: ti.lineno for ti in mod.type_ignores} body = self.fix_function_overloads(self.translate_stmt_list(mod.body)) - ignores = [ti.lineno for ti in mod.type_ignores] - ignores.extend(self.extra_type_ignores) + self.type_ignores.update({ti.lineno: ti.lineno for ti in mod.type_ignores}) return MypyFile(body, self.imports, False, - set(ignores), + self.type_ignores, ) # --- stmt --- @@ -596,7 +608,7 @@ def make_argument(self, arg: ast3.arg, default: Optional[ast3.expr], kind: int, elif type_comment is not None: extra_ignore, arg_type = parse_type_comment(type_comment, arg.lineno, self.errors) if extra_ignore: - self.extra_type_ignores.append(arg.lineno) + self.type_ignores[arg.lineno] = arg.lineno return Argument(Var(arg.arg), arg_type, self.visit(default), kind) @@ -654,7 +666,7 @@ def visit_Assign(self, n: ast3.Assign) -> AssignmentStmt: if n.type_comment is not None: extra_ignore, typ = parse_type_comment(n.type_comment, n.lineno, self.errors) if extra_ignore: - self.extra_type_ignores.append(n.lineno) + self.type_ignores[n.lineno] = n.lineno else: typ = None s = AssignmentStmt(lvalues, rvalue, type=typ, new_syntax=False) @@ -688,7 +700,7 @@ 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, self.errors) if extra_ignore: - self.extra_type_ignores.append(n.lineno) + self.type_ignores[n.lineno] = n.lineno else: target_type = None node = ForStmt(self.visit(n.target), @@ -703,7 +715,7 @@ 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, self.errors) if extra_ignore: - self.extra_type_ignores.append(n.lineno) + self.type_ignores[n.lineno] = n.lineno else: target_type = None node = ForStmt(self.visit(n.target), @@ -734,7 +746,7 @@ 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, self.errors) if extra_ignore: - self.extra_type_ignores.append(n.lineno) + self.type_ignores[n.lineno] = n.lineno else: target_type = None node = WithStmt([self.visit(i.context_expr) for i in n.items], @@ -748,7 +760,7 @@ 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, self.errors) if extra_ignore: - self.extra_type_ignores.append(n.lineno) + self.type_ignores[n.lineno] = n.lineno else: target_type = None s = WithStmt([self.visit(i.context_expr) for i in n.items], diff --git a/mypy/fastparse2.py b/mypy/fastparse2.py index 95f415c629cb..66cee7c994da 100644 --- a/mypy/fastparse2.py +++ b/mypy/fastparse2.py @@ -113,7 +113,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) - tree = MypyFile([], [], False, set()) + tree = MypyFile([], [], False, {}) if raise_on_error and errors.is_errors(): errors.raise_error() @@ -308,7 +308,7 @@ def visit_Module(self, mod: ast27.Module) -> MypyFile: return MypyFile(body, self.imports, False, - set(ignores), + {line: line for line in ignores}, ) # --- stmt --- diff --git a/mypy/nodes.py b/mypy/nodes.py index 06be60b0f6fe..67b11738cf09 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -227,7 +227,7 @@ class MypyFile(SymbolNode): # 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] + ignored_lines = None # type: Dict[int, int] # 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? @@ -243,7 +243,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, int]] = None) -> None: super().__init__() self.defs = defs self.line = 1 # Dummy line number @@ -254,7 +254,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/strconv.py b/mypy/strconv.py index 87f7bd97af71..d47d16cd479c 100644 --- a/mypy/strconv.py +++ b/mypy/strconv.py @@ -97,8 +97,8 @@ def visit_mypy_file(self, o: 'mypy.nodes.MypyFile') -> str: # case# output in all platforms. a.insert(0, o.path.replace(os.sep, '/')) if o.ignored_lines: - a.append('IgnoredLines(%s)' % ', '.join(str(line) - for line in sorted(o.ignored_lines))) + a.append('IgnoredLines(%s)' % ', '.join( + str(line) for line in sorted(set(o.ignored_lines.values())))) return self.dump(a, o) def visit_import(self, o: 'mypy.nodes.Import') -> str: diff --git a/mypy/treetransform.py b/mypy/treetransform.py index f4bc96e39db6..0ab7fe89926f 100644 --- a/mypy/treetransform.py +++ b/mypy/treetransform.py @@ -60,7 +60,7 @@ def __init__(self) -> None: def visit_mypy_file(self, node: MypyFile) -> MypyFile: # NOTE: The 'names' and 'imports' instance variables will be empty! new = MypyFile(self.statements(node.defs), [], node.is_bom, - ignored_lines=set(node.ignored_lines)) + ignored_lines=node.ignored_lines) new._fullname = node._fullname new.path = node.path new.names = SymbolTable() From 6b066e205b7a8aa83e64bcc0b3c6785d99597beb Mon Sep 17 00:00:00 2001 From: Brandt Bucher Date: Mon, 8 Apr 2019 17:49:56 -0700 Subject: [PATCH 02/12] Add scoped ignore tests and fix nesting issues. --- mypy/fastparse.py | 6 +++--- mypy/test/testcheck.py | 3 +++ test-data/unit/check-38.test | 21 +++++++++++++++++++++ 3 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 test-data/unit/check-38.test diff --git a/mypy/fastparse.py b/mypy/fastparse.py index 3e66ce4821bf..bc8264510744 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -271,9 +271,10 @@ def visit(self, node: Optional[AST]) -> Any: method = 'visit_' + node.__class__.__name__ visitor = getattr(self, method) self.visitor_cache[typeobj] = visitor + result = visitor(node) if (3, 8) < sys.version_info and isinstance(node, ast3.expr): self.scope_ignores(node) - return visitor(node) + return result def scope_ignores(self, node: ast3.expr) -> None: end_lineno = getattr(node, "end_lineno", None) @@ -282,7 +283,7 @@ def scope_ignores(self, node: ast3.expr) -> None: node_lines = range(node.lineno, end_lineno + 1) for line in node_lines: if line in self.type_ignores.values(): - self.type_ignores.update(dict.fromkeys(node_lines, line)) + self.type_ignores = {**dict.fromkeys(node_lines, line), **self.type_ignores} return def set_line(self, node: N, n: Union[ast3.expr, ast3.stmt]) -> N: @@ -408,7 +409,6 @@ def translate_module_id(self, id: str) -> str: def visit_Module(self, mod: ast3.Module) -> MypyFile: self.type_ignores = {ti.lineno: ti.lineno for ti in mod.type_ignores} body = self.fix_function_overloads(self.translate_stmt_list(mod.body)) - self.type_ignores.update({ti.lineno: ti.lineno for ti in mod.type_ignores}) return MypyFile(body, self.imports, False, diff --git a/mypy/test/testcheck.py b/mypy/test/testcheck.py index e089b2ad590e..26048972524e 100644 --- a/mypy/test/testcheck.py +++ b/mypy/test/testcheck.py @@ -85,6 +85,9 @@ 'check-newsemanal.test', ] +if (3, 8) < sys.version_info: + typecheck_files.append('check-38.test') + class TypeCheckSuite(DataSuite): files = typecheck_files diff --git a/test-data/unit/check-38.test b/test-data/unit/check-38.test new file mode 100644 index 000000000000..3a211f311c7d --- /dev/null +++ b/test-data/unit/check-38.test @@ -0,0 +1,21 @@ +[case testScopedIgnore] +def f(a: int): ... +f( + 'BAD' +) # type: ignore + +[case testMultipleScopedIgnores] +# flags: --warn-unused-ignores +def f(a: int): ... +f( + 'BAD' # type: ignore +) # type: ignore # N: unused 'type: ignore' comment + +[case testNestedScopedIgnores] +# flags: --warn-unused-ignores +def f(a: int) -> int: ... +f( # type: ignore # N: unused 'type: ignore' comment + f( + 'BAD' # type: ignore + ) # type: ignore # N: unused 'type: ignore' comment +) # type: ignore # N: unused 'type: ignore' comment From e660a029af0dd1f6d5640631973ec6ee7ab35b66 Mon Sep 17 00:00:00 2001 From: Brandt Bucher Date: Tue, 9 Apr 2019 11:58:09 -0700 Subject: [PATCH 03/12] Trivial code review changes. --- mypy/errors.py | 4 +++- mypy/fastparse.py | 4 +++- mypy/nodes.py | 3 ++- mypy/test/testcheck.py | 2 +- mypy/treetransform.py | 2 +- 5 files changed, 10 insertions(+), 5 deletions(-) diff --git a/mypy/errors.py b/mypy/errors.py index a5ef2e01b6d6..f08582b1f05a 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -113,7 +113,8 @@ class Errors: # Path to current file. file = '' # type: str - # Ignore errors on these lines of each file. + # Ignore errors on these lines of each file. Keys are modules, values are mappings from: + # ignored line -> line of "type: ignore" comment it is scoped to within that module. ignored_lines = None # type: Dict[str, Dict[int, int]] # Lines on which an error was actually ignored. @@ -300,6 +301,7 @@ def clear_errors_in_targets(self, path: str, targets: Set[str]) -> None: self.error_info_map[path] = new_errors def generate_unused_ignore_notes(self, file: str) -> None: + # The values() here are the line numbers of *actual* "type: ignore" comments. ignored_lines = set(self.ignored_lines[file].values()) if not self.is_typeshed_file(file) and file not in self.ignored_files: for line in ignored_lines - self.used_ignored_lines[file]: diff --git a/mypy/fastparse.py b/mypy/fastparse.py index bc8264510744..4025ff512908 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -251,6 +251,8 @@ def __init__(self, self.is_stub = is_stub self.errors = errors + # Lines to ignore when checking. This is a mapping from: + # ignored line -> line of "type: ignore" comment it is scoped to. self.type_ignores = {} # type: Dict[int, int] # Cache of visit_X methods keyed by type of visited object @@ -272,7 +274,7 @@ def visit(self, node: Optional[AST]) -> Any: visitor = getattr(self, method) self.visitor_cache[typeobj] = visitor result = visitor(node) - if (3, 8) < sys.version_info and isinstance(node, ast3.expr): + if sys.version_info >= (3, 8) and isinstance(node, ast3.expr): self.scope_ignores(node) return result diff --git a/mypy/nodes.py b/mypy/nodes.py index 67b11738cf09..8615377912ee 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -226,7 +226,8 @@ 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 + # Lines to ignore when checking. This is a mapping from: + # ignored line -> line of "type: ignore" comment it is scoped to. ignored_lines = None # type: Dict[int, int] # Is this file represented by a stub file (.pyi)? is_stub = False diff --git a/mypy/test/testcheck.py b/mypy/test/testcheck.py index 26048972524e..385f8b516622 100644 --- a/mypy/test/testcheck.py +++ b/mypy/test/testcheck.py @@ -85,7 +85,7 @@ 'check-newsemanal.test', ] -if (3, 8) < sys.version_info: +if sys.version_info >= (3, 8): typecheck_files.append('check-38.test') diff --git a/mypy/treetransform.py b/mypy/treetransform.py index 0ab7fe89926f..4aca8f35ce5e 100644 --- a/mypy/treetransform.py +++ b/mypy/treetransform.py @@ -60,7 +60,7 @@ def __init__(self) -> None: def visit_mypy_file(self, node: MypyFile) -> MypyFile: # NOTE: The 'names' and 'imports' instance variables will be empty! new = MypyFile(self.statements(node.defs), [], node.is_bom, - ignored_lines=node.ignored_lines) + ignored_lines=node.ignored_lines.copy()) new._fullname = node._fullname new.path = node.path new.names = SymbolTable() From 39df3eee510eeefeff185155460096434e02f66e Mon Sep 17 00:00:00 2001 From: Brandt Bucher Date: Tue, 9 Apr 2019 12:19:37 -0700 Subject: [PATCH 04/12] Add clarifying info to version guards. --- mypy/fastparse.py | 1 + mypy/test/testcheck.py | 1 + 2 files changed, 2 insertions(+) diff --git a/mypy/fastparse.py b/mypy/fastparse.py index 4025ff512908..3775139b4068 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -274,6 +274,7 @@ def visit(self, node: Optional[AST]) -> Any: visitor = getattr(self, method) self.visitor_cache[typeobj] = visitor result = visitor(node) + # In Python 3.8, we can expand the scope of ignores to a whole expression: if sys.version_info >= (3, 8) and isinstance(node, ast3.expr): self.scope_ignores(node) return result diff --git a/mypy/test/testcheck.py b/mypy/test/testcheck.py index 385f8b516622..f201f663cc81 100644 --- a/mypy/test/testcheck.py +++ b/mypy/test/testcheck.py @@ -85,6 +85,7 @@ 'check-newsemanal.test', ] +# Tests that use Python 3.8-only AST features (like expression-scoped ignores): if sys.version_info >= (3, 8): typecheck_files.append('check-38.test') From 001c8e5519842866942112ce9cb05b1a5200cc50 Mon Sep 17 00:00:00 2001 From: Brandt Bucher Date: Tue, 9 Apr 2019 17:25:12 -0700 Subject: [PATCH 05/12] Improve test cases. --- test-data/unit/check-38.test | 71 +++++++++++++++++++++++++++++++++--- 1 file changed, 66 insertions(+), 5 deletions(-) diff --git a/test-data/unit/check-38.test b/test-data/unit/check-38.test index 3a211f311c7d..99ec989751c0 100644 --- a/test-data/unit/check-38.test +++ b/test-data/unit/check-38.test @@ -1,17 +1,69 @@ -[case testScopedIgnore] +[case testIssue1032] def f(a: int): ... f( 'BAD' ) # type: ignore -[case testMultipleScopedIgnores] +[case testMultipleScopedIgnores1] # flags: --warn-unused-ignores -def f(a: int): ... -f( +( # type: ignore # N: unused 'type: ignore' comment + 'BAD' # type: ignore + + # type: ignore # N: unused 'type: ignore' comment + 0 # type: ignore # N: unused 'type: ignore' comment +) # type: ignore # N: unused 'type: ignore' comment + +[case testMultipleScopedIgnores2] +# flags: --warn-unused-ignores +( # type: ignore # N: unused 'type: ignore' comment + 'BAD' + - # type: ignore + 0 # type: ignore # N: unused 'type: ignore' comment +) # type: ignore # N: unused 'type: ignore' comment + +[case testMultipleScopedIgnores3] +# flags: --warn-unused-ignores +( # type: ignore # N: unused 'type: ignore' comment + 'BAD' + / + 0 # type: ignore +) # type: ignore # N: unused 'type: ignore' comment + +[case testMultipleScopedIgnoresList] +# flags: --warn-unused-ignores +[ # type: ignore # type: ignore # N: unused 'type: ignore' comment 'BAD' # type: ignore + & # type: ignore # N: unused 'type: ignore' comment + 0, # type: ignore # N: unused 'type: ignore' comment +] # type: ignore # N: unused 'type: ignore' comment +[builtins fixtures/list.pyi] + +[case testMultipleScopedIgnoresSet] +# flags: --warn-unused-ignores +{ # type: ignore # N: unused 'type: ignore' comment + 'BAD' + | # type: ignore + 0, # type: ignore # N: unused 'type: ignore' comment +} # type: ignore # N: unused 'type: ignore' comment +[builtins fixtures/set.pyi] + +[case testMultipleScopedIgnoresTuple] +# flags: --warn-unused-ignores +( # type: ignore # N: unused 'type: ignore' comment + 'BAD' + ^ + 0, # type: ignore +) # type: ignore # N: unused 'type: ignore' comment + +[case testNestedScopedIgnores1] +# flags: --warn-unused-ignores +def f(a: int) -> int: ... +f( # type: ignore # N: unused 'type: ignore' comment + f( # type: ignore + 'BAD' # type: ignore # N: unused 'type: ignore' comment + ) # type: ignore # N: unused 'type: ignore' comment ) # type: ignore # N: unused 'type: ignore' comment -[case testNestedScopedIgnores] +[case testNestedScopedIgnores2] # flags: --warn-unused-ignores def f(a: int) -> int: ... f( # type: ignore # N: unused 'type: ignore' comment @@ -19,3 +71,12 @@ f( # type: ignore # N: unused 'type: ignore' comment 'BAD' # type: ignore ) # type: ignore # N: unused 'type: ignore' comment ) # type: ignore # N: unused 'type: ignore' comment + +[case testNestedScopedIgnores3] +# flags: --warn-unused-ignores +def f(a: int) -> int: ... +f( # type: ignore # N: unused 'type: ignore' comment + f( + 'BAD' + ) # type: ignore +) # type: ignore # N: unused 'type: ignore' comment From 57f6e1ab25287a2f0eb530d540aeb318ef0167f1 Mon Sep 17 00:00:00 2001 From: Brandt Bucher Date: Tue, 9 Apr 2019 17:53:09 -0700 Subject: [PATCH 06/12] Refactoring from code review. --- mypy/fastparse.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/mypy/fastparse.py b/mypy/fastparse.py index 3775139b4068..daaac571904a 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -251,6 +251,9 @@ def __init__(self, self.is_stub = is_stub self.errors = errors + # The line numbers of *actual* "type: ignore" comments: + self.type_ignore_comments = set() # type: Set[int] + # Lines to ignore when checking. This is a mapping from: # ignored line -> line of "type: ignore" comment it is scoped to. self.type_ignores = {} # type: Dict[int, int] @@ -273,20 +276,21 @@ def visit(self, node: Optional[AST]) -> Any: method = 'visit_' + node.__class__.__name__ visitor = getattr(self, method) self.visitor_cache[typeobj] = visitor - result = visitor(node) # In Python 3.8, we can expand the scope of ignores to a whole expression: if sys.version_info >= (3, 8) and isinstance(node, ast3.expr): self.scope_ignores(node) - return result + return visitor(node) def scope_ignores(self, node: ast3.expr) -> None: end_lineno = getattr(node, "end_lineno", None) if end_lineno is None: return node_lines = range(node.lineno, end_lineno + 1) + # Check to see if this expression overlaps with any "type: ignore" comments. + # If so, take the first one and grow its scope to cover the whole node: for line in node_lines: - if line in self.type_ignores.values(): - self.type_ignores = {**dict.fromkeys(node_lines, line), **self.type_ignores} + if line in self.type_ignore_comments: + self.type_ignores.update(dict.fromkeys(node_lines, line)) return def set_line(self, node: N, n: Union[ast3.expr, ast3.stmt]) -> N: @@ -410,8 +414,9 @@ def translate_module_id(self, id: str) -> str: return id def visit_Module(self, mod: ast3.Module) -> MypyFile: - self.type_ignores = {ti.lineno: ti.lineno for ti in mod.type_ignores} + self.type_ignore_comments = {ti.lineno for ti in mod.type_ignores} body = self.fix_function_overloads(self.translate_stmt_list(mod.body)) + self.type_ignores.update({line: line for line in self.type_ignore_comments}) return MypyFile(body, self.imports, False, @@ -611,7 +616,7 @@ def make_argument(self, arg: ast3.arg, default: Optional[ast3.expr], kind: int, elif type_comment is not None: extra_ignore, arg_type = parse_type_comment(type_comment, arg.lineno, self.errors) if extra_ignore: - self.type_ignores[arg.lineno] = arg.lineno + self.type_ignore_comments.add(arg.lineno) return Argument(Var(arg.arg), arg_type, self.visit(default), kind) @@ -669,7 +674,7 @@ def visit_Assign(self, n: ast3.Assign) -> AssignmentStmt: if n.type_comment is not None: extra_ignore, typ = parse_type_comment(n.type_comment, n.lineno, self.errors) if extra_ignore: - self.type_ignores[n.lineno] = n.lineno + self.type_ignore_comments.add(n.lineno) else: typ = None s = AssignmentStmt(lvalues, rvalue, type=typ, new_syntax=False) @@ -703,7 +708,7 @@ 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, self.errors) if extra_ignore: - self.type_ignores[n.lineno] = n.lineno + self.type_ignore_comments.add(n.lineno) else: target_type = None node = ForStmt(self.visit(n.target), @@ -718,7 +723,7 @@ 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, self.errors) if extra_ignore: - self.type_ignores[n.lineno] = n.lineno + self.type_ignore_comments.add(n.lineno) else: target_type = None node = ForStmt(self.visit(n.target), @@ -749,7 +754,7 @@ 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, self.errors) if extra_ignore: - self.type_ignores[n.lineno] = n.lineno + self.type_ignore_comments.add(n.lineno) else: target_type = None node = WithStmt([self.visit(i.context_expr) for i in n.items], @@ -763,7 +768,7 @@ 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, self.errors) if extra_ignore: - self.type_ignores[n.lineno] = n.lineno + self.type_ignore_comments.add(n.lineno) else: target_type = None s = WithStmt([self.visit(i.context_expr) for i in n.items], From 88b7716a35785e6320f0659b595b4da05549825e Mon Sep 17 00:00:00 2001 From: Brandt Bucher Date: Tue, 9 Apr 2019 18:03:11 -0700 Subject: [PATCH 07/12] Fix linting error. --- mypy/fastparse.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/fastparse.py b/mypy/fastparse.py index daaac571904a..ce33aef28912 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -2,7 +2,7 @@ import sys from typing import ( - Tuple, Union, TypeVar, Callable, Sequence, Optional, Any, Dict, cast, List, overload + Tuple, Union, TypeVar, Callable, Sequence, Optional, Any, Dict, cast, List, overload, Set, ) MYPY = False if MYPY: From 043a8966e6b33b517b7e9da2cf4f7562ed17ee72 Mon Sep 17 00:00:00 2001 From: Brandt Bucher Date: Wed, 10 Apr 2019 09:47:08 -0700 Subject: [PATCH 08/12] Back out previous implementation. --- mypy/errors.py | 12 +++++------- mypy/fastparse.py | 44 ++++++++++++------------------------------- mypy/fastparse2.py | 4 ++-- mypy/nodes.py | 9 ++++----- mypy/strconv.py | 4 ++-- mypy/treetransform.py | 2 +- 6 files changed, 26 insertions(+), 49 deletions(-) diff --git a/mypy/errors.py b/mypy/errors.py index f08582b1f05a..0053e3ec08c4 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -113,9 +113,8 @@ class Errors: # Path to current file. file = '' # type: str - # Ignore errors on these lines of each file. Keys are modules, values are mappings from: - # ignored line -> line of "type: ignore" comment it is scoped to within that module. - ignored_lines = None # type: Dict[str, Dict[int, int]] + # Ignore errors on these lines of each file. + ignored_lines = None # type: Dict[str, Set[int]] # Lines on which an error was actually ignored. used_ignored_lines = None # type: Dict[str, Set[int]] @@ -200,7 +199,7 @@ def set_file(self, file: str, self.scope = scope def set_file_ignored_lines(self, file: str, - ignored_lines: Dict[int, int], + ignored_lines: Set[int], ignore_all: bool = False) -> None: self.ignored_lines[file] = ignored_lines if ignore_all: @@ -279,7 +278,7 @@ def add_error_info(self, info: ErrorInfo) -> None: if not info.blocker: # Blockers cannot be ignored if file in self.ignored_lines and line in self.ignored_lines[file]: # Annotation requests us to ignore all errors on this line. - self.used_ignored_lines[file].add(self.ignored_lines[file][line]) + self.used_ignored_lines[file].add(line) return if file in self.ignored_files: return @@ -301,8 +300,7 @@ def clear_errors_in_targets(self, path: str, targets: Set[str]) -> None: self.error_info_map[path] = new_errors def generate_unused_ignore_notes(self, file: str) -> None: - # The values() here are the line numbers of *actual* "type: ignore" comments. - ignored_lines = set(self.ignored_lines[file].values()) + 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]: # Don't use report since add_error_info will ignore the error! diff --git a/mypy/fastparse.py b/mypy/fastparse.py index ce33aef28912..d47e1a97148d 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -2,7 +2,7 @@ import sys 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 ) MYPY = False if MYPY: @@ -171,7 +171,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) - tree = MypyFile([], [], False, {}) + tree = MypyFile([], [], False, set()) if raise_on_error and errors.is_errors(): errors.raise_error() @@ -251,12 +251,7 @@ def __init__(self, self.is_stub = is_stub self.errors = errors - # The line numbers of *actual* "type: ignore" comments: - self.type_ignore_comments = set() # type: Set[int] - - # Lines to ignore when checking. This is a mapping from: - # ignored line -> line of "type: ignore" comment it is scoped to. - self.type_ignores = {} # type: Dict[int, int] + self.extra_type_ignores = [] # type: List[int] # Cache of visit_X methods keyed by type of visited object self.visitor_cache = {} # type: Dict[type, Callable[[Optional[AST]], Any]] @@ -276,23 +271,8 @@ def visit(self, node: Optional[AST]) -> Any: method = 'visit_' + node.__class__.__name__ visitor = getattr(self, method) self.visitor_cache[typeobj] = visitor - # In Python 3.8, we can expand the scope of ignores to a whole expression: - if sys.version_info >= (3, 8) and isinstance(node, ast3.expr): - self.scope_ignores(node) return visitor(node) - def scope_ignores(self, node: ast3.expr) -> None: - end_lineno = getattr(node, "end_lineno", None) - if end_lineno is None: - return - node_lines = range(node.lineno, end_lineno + 1) - # Check to see if this expression overlaps with any "type: ignore" comments. - # If so, take the first one and grow its scope to cover the whole node: - for line in node_lines: - if line in self.type_ignore_comments: - self.type_ignores.update(dict.fromkeys(node_lines, line)) - return - def set_line(self, node: N, n: Union[ast3.expr, ast3.stmt]) -> N: node.line = n.lineno node.column = n.col_offset @@ -414,13 +394,13 @@ def translate_module_id(self, id: str) -> str: return id def visit_Module(self, mod: ast3.Module) -> MypyFile: - self.type_ignore_comments = {ti.lineno for ti in mod.type_ignores} body = self.fix_function_overloads(self.translate_stmt_list(mod.body)) - self.type_ignores.update({line: line for line in self.type_ignore_comments}) + ignores = [ti.lineno for ti in mod.type_ignores] + ignores.extend(self.extra_type_ignores) return MypyFile(body, self.imports, False, - self.type_ignores, + set(ignores), ) # --- stmt --- @@ -616,7 +596,7 @@ def make_argument(self, arg: ast3.arg, default: Optional[ast3.expr], kind: int, elif type_comment is not None: extra_ignore, arg_type = parse_type_comment(type_comment, arg.lineno, self.errors) if extra_ignore: - self.type_ignore_comments.add(arg.lineno) + self.extra_type_ignores.append(arg.lineno) return Argument(Var(arg.arg), arg_type, self.visit(default), kind) @@ -674,7 +654,7 @@ def visit_Assign(self, n: ast3.Assign) -> AssignmentStmt: if n.type_comment is not None: extra_ignore, typ = parse_type_comment(n.type_comment, n.lineno, self.errors) if extra_ignore: - self.type_ignore_comments.add(n.lineno) + self.extra_type_ignores.append(n.lineno) else: typ = None s = AssignmentStmt(lvalues, rvalue, type=typ, new_syntax=False) @@ -708,7 +688,7 @@ 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, self.errors) if extra_ignore: - self.type_ignore_comments.add(n.lineno) + self.extra_type_ignores.append(n.lineno) else: target_type = None node = ForStmt(self.visit(n.target), @@ -723,7 +703,7 @@ 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, self.errors) if extra_ignore: - self.type_ignore_comments.add(n.lineno) + self.extra_type_ignores.append(n.lineno) else: target_type = None node = ForStmt(self.visit(n.target), @@ -754,7 +734,7 @@ 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, self.errors) if extra_ignore: - self.type_ignore_comments.add(n.lineno) + self.extra_type_ignores.append(n.lineno) else: target_type = None node = WithStmt([self.visit(i.context_expr) for i in n.items], @@ -768,7 +748,7 @@ 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, self.errors) if extra_ignore: - self.type_ignore_comments.add(n.lineno) + self.extra_type_ignores.append(n.lineno) else: target_type = None s = WithStmt([self.visit(i.context_expr) for i in n.items], diff --git a/mypy/fastparse2.py b/mypy/fastparse2.py index 66cee7c994da..95f415c629cb 100644 --- a/mypy/fastparse2.py +++ b/mypy/fastparse2.py @@ -113,7 +113,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) - tree = MypyFile([], [], False, {}) + tree = MypyFile([], [], False, set()) if raise_on_error and errors.is_errors(): errors.raise_error() @@ -308,7 +308,7 @@ def visit_Module(self, mod: ast27.Module) -> MypyFile: return MypyFile(body, self.imports, False, - {line: line for line in ignores}, + set(ignores), ) # --- stmt --- diff --git a/mypy/nodes.py b/mypy/nodes.py index 8615377912ee..06be60b0f6fe 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -226,9 +226,8 @@ 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. This is a mapping from: - # ignored line -> line of "type: ignore" comment it is scoped to. - ignored_lines = None # type: Dict[int, int] + # Lines to ignore when checking + ignored_lines = None # type: Set[int] # 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? @@ -244,7 +243,7 @@ def __init__(self, defs: List[Statement], imports: List['ImportBase'], is_bom: bool = False, - ignored_lines: Optional[Dict[int, int]] = None) -> None: + ignored_lines: Optional[Set[int]] = None) -> None: super().__init__() self.defs = defs self.line = 1 # Dummy line number @@ -255,7 +254,7 @@ def __init__(self, if ignored_lines: self.ignored_lines = ignored_lines else: - self.ignored_lines = {} + self.ignored_lines = set() def local_definitions(self) -> Iterator[Definition]: """Return all definitions within the module (including nested). diff --git a/mypy/strconv.py b/mypy/strconv.py index d47d16cd479c..87f7bd97af71 100644 --- a/mypy/strconv.py +++ b/mypy/strconv.py @@ -97,8 +97,8 @@ def visit_mypy_file(self, o: 'mypy.nodes.MypyFile') -> str: # case# output in all platforms. a.insert(0, o.path.replace(os.sep, '/')) if o.ignored_lines: - a.append('IgnoredLines(%s)' % ', '.join( - str(line) for line in sorted(set(o.ignored_lines.values())))) + a.append('IgnoredLines(%s)' % ', '.join(str(line) + for line in sorted(o.ignored_lines))) return self.dump(a, o) def visit_import(self, o: 'mypy.nodes.Import') -> str: diff --git a/mypy/treetransform.py b/mypy/treetransform.py index 4aca8f35ce5e..f4bc96e39db6 100644 --- a/mypy/treetransform.py +++ b/mypy/treetransform.py @@ -60,7 +60,7 @@ def __init__(self) -> None: def visit_mypy_file(self, node: MypyFile) -> MypyFile: # NOTE: The 'names' and 'imports' instance variables will be empty! new = MypyFile(self.statements(node.defs), [], node.is_bom, - ignored_lines=node.ignored_lines.copy()) + ignored_lines=set(node.ignored_lines)) new._fullname = node._fullname new.path = node.path new.names = SymbolTable() From 019d64043982543501c5c8913fe11eff2024abe6 Mon Sep 17 00:00:00 2001 From: Brandt Bucher Date: Wed, 10 Apr 2019 14:55:04 -0700 Subject: [PATCH 09/12] Add breaking test cases. --- test-data/unit/check-38.test | 119 +++++++++++++++++------------------ 1 file changed, 58 insertions(+), 61 deletions(-) diff --git a/test-data/unit/check-38.test b/test-data/unit/check-38.test index 99ec989751c0..445bc0c3bc02 100644 --- a/test-data/unit/check-38.test +++ b/test-data/unit/check-38.test @@ -1,82 +1,79 @@ -[case testIssue1032] +[case testIgnoreScopeIssue1032] def f(a: int): ... f( - 'BAD' + "IGNORE" ) # type: ignore -[case testMultipleScopedIgnores1] +case testIgnoreScopeNested1] +def f(a: int) -> int: ... +f( + f( + "IGNORE" + ) # type: ignore +) + +[case testIgnoreScopeNested2] +[ + "IGNORE" # type: ignore + & + "IGNORE", +] +[builtins fixtures/list.pyi] + +[case testIgnoreScopeNested3] +{ + "IGNORE" + | # type: ignore + "IGNORE", +} +[builtins fixtures/set.pyi] + +[case testIgnoreScopeNested4] +{ + "IGNORE" + : + "IGNORE", # type: ignore +} +[builtins fixtures/dict.pyi] + +[case testIgnoreScopeNestedNonOverlapping] +def f(x: int): ... +def g(x: int): ... +( + f("ERROR"), # E: Argument 1 to "f" has incompatible type "str"; expected "int" + g("IGNORE"), # type: ignore + f("ERROR"), # E: Argument 1 to "f" has incompatible type "str"; expected "int" +) + +[case testIgnoreScopeNestedOverlapping] +def f(x: int): ... +def g(x: int): ... +( + f("ERROR"), g( # E: Argument 1 to "f" has incompatible type "str"; expected "int" + "IGNORE" # type: ignore + ), f("ERROR"), # E: Argument 1 to "f" has incompatible type "str"; expected "int" +) + +[case testIgnoreScopeUnused1] # flags: --warn-unused-ignores ( # type: ignore # N: unused 'type: ignore' comment - 'BAD' # type: ignore + "IGNORE" # type: ignore + # type: ignore # N: unused 'type: ignore' comment 0 # type: ignore # N: unused 'type: ignore' comment ) # type: ignore # N: unused 'type: ignore' comment -[case testMultipleScopedIgnores2] +[case testIgnoreScopeUnused2] # flags: --warn-unused-ignores ( # type: ignore # N: unused 'type: ignore' comment - 'BAD' + "IGNORE" - # type: ignore 0 # type: ignore # N: unused 'type: ignore' comment ) # type: ignore # N: unused 'type: ignore' comment -[case testMultipleScopedIgnores3] +[case testIgnoreScopeUnused3] # flags: --warn-unused-ignores ( # type: ignore # N: unused 'type: ignore' comment - 'BAD' + "IGNORE" / 0 # type: ignore ) # type: ignore # N: unused 'type: ignore' comment - -[case testMultipleScopedIgnoresList] -# flags: --warn-unused-ignores -[ # type: ignore # type: ignore # N: unused 'type: ignore' comment - 'BAD' # type: ignore - & # type: ignore # N: unused 'type: ignore' comment - 0, # type: ignore # N: unused 'type: ignore' comment -] # type: ignore # N: unused 'type: ignore' comment -[builtins fixtures/list.pyi] - -[case testMultipleScopedIgnoresSet] -# flags: --warn-unused-ignores -{ # type: ignore # N: unused 'type: ignore' comment - 'BAD' - | # type: ignore - 0, # type: ignore # N: unused 'type: ignore' comment -} # type: ignore # N: unused 'type: ignore' comment -[builtins fixtures/set.pyi] - -[case testMultipleScopedIgnoresTuple] -# flags: --warn-unused-ignores -( # type: ignore # N: unused 'type: ignore' comment - 'BAD' - ^ - 0, # type: ignore -) # type: ignore # N: unused 'type: ignore' comment - -[case testNestedScopedIgnores1] -# flags: --warn-unused-ignores -def f(a: int) -> int: ... -f( # type: ignore # N: unused 'type: ignore' comment - f( # type: ignore - 'BAD' # type: ignore # N: unused 'type: ignore' comment - ) # type: ignore # N: unused 'type: ignore' comment -) # type: ignore # N: unused 'type: ignore' comment - -[case testNestedScopedIgnores2] -# flags: --warn-unused-ignores -def f(a: int) -> int: ... -f( # type: ignore # N: unused 'type: ignore' comment - f( - 'BAD' # type: ignore - ) # type: ignore # N: unused 'type: ignore' comment -) # type: ignore # N: unused 'type: ignore' comment - -[case testNestedScopedIgnores3] -# flags: --warn-unused-ignores -def f(a: int) -> int: ... -f( # type: ignore # N: unused 'type: ignore' comment - f( - 'BAD' - ) # type: ignore -) # type: ignore # N: unused 'type: ignore' comment From b32317ee4f800b50f4ce63e9f8884996939b2b09 Mon Sep 17 00:00:00 2001 From: Brandt Bucher Date: Wed, 10 Apr 2019 16:46:46 -0700 Subject: [PATCH 10/12] Fix broken tests. --- test-data/unit/check-38.test | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test-data/unit/check-38.test b/test-data/unit/check-38.test index 445bc0c3bc02..c4c9a154f661 100644 --- a/test-data/unit/check-38.test +++ b/test-data/unit/check-38.test @@ -4,7 +4,7 @@ f( "IGNORE" ) # type: ignore -case testIgnoreScopeNested1] +[case testIgnoreScopeNested1] def f(a: int) -> int: ... f( f( @@ -30,8 +30,8 @@ f( [case testIgnoreScopeNested4] { - "IGNORE" - : + None: "IGNORE" + ^ "IGNORE", # type: ignore } [builtins fixtures/dict.pyi] From 8436257b40ee16f46b9673174ab0bf76c62de39f Mon Sep 17 00:00:00 2001 From: Brandt Bucher Date: Wed, 10 Apr 2019 17:11:58 -0700 Subject: [PATCH 11/12] New implementation of expression-scoped ignores. --- mypy/errors.py | 37 ++++++++++++++++++++++++++----------- mypy/fastparse.py | 1 + mypy/messages.py | 9 ++++++++- mypy/nodes.py | 4 +++- 4 files changed, 38 insertions(+), 13 deletions(-) diff --git a/mypy/errors.py b/mypy/errors.py index 0053e3ec08c4..d2b482b33ef9 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -54,8 +54,9 @@ class ErrorInfo: # Only report this particular messages once per program. only_once = False - # Actual origin of the error message as tuple (path, line number) - origin = None # type: Tuple[str, int] + # Actual origin of the error message as tuple (path, line number, end line number) + # If end line number is unknown, use line number. + origin = None # type: Tuple[str, int, int] # Fine-grained incremental target where this was reported target = None # type: Optional[str] @@ -72,7 +73,7 @@ def __init__(self, message: str, blocker: bool, only_once: bool, - origin: Optional[Tuple[str, int]] = None, + origin: Optional[Tuple[str, int, int]] = None, target: Optional[str] = None) -> None: self.import_ctx = import_ctx self.file = file @@ -85,7 +86,7 @@ def __init__(self, self.message = message self.blocker = blocker self.only_once = only_once - self.origin = origin or (file, line) + self.origin = origin or (file, line, line) self.target = target @@ -233,7 +234,8 @@ def report(self, file: Optional[str] = None, only_once: bool = False, origin_line: Optional[int] = None, - offset: int = 0) -> None: + offset: int = 0, + end_line: Optional[int] = None) -> None: """Report message at the given line using the current error context. Args: @@ -244,6 +246,7 @@ def report(self, file: if non-None, override current file as context only_once: if True, only report this exact message once per build origin_line: if non-None, override current context as origin + end_line: if non-None, override current context as end """ if self.scope: type = self.scope.current_type_name() @@ -260,10 +263,17 @@ def report(self, file = self.file if offset: message = " " * offset + message + + if origin_line is None: + origin_line = line + + if end_line is None: + end_line = origin_line + info = ErrorInfo(self.import_context(), file, self.current_module(), type, function, line, column, severity, message, blocker, only_once, - origin=(self.file, origin_line) if origin_line else None, + origin=(self.file, origin_line, end_line), target=self.current_target()) self.add_error_info(info) @@ -274,12 +284,17 @@ def _add_error_info(self, file: str, info: ErrorInfo) -> None: self.error_info_map[file].append(info) def add_error_info(self, info: ErrorInfo) -> None: - file, line = info.origin + file, line, end_line = info.origin if not info.blocker: # Blockers cannot be ignored - if file in self.ignored_lines and line in self.ignored_lines[file]: - # Annotation requests us to ignore all errors on this line. - self.used_ignored_lines[file].add(line) - return + if file in self.ignored_lines: + # Check each line in this context for "type: ignore" comments. + # For anything other than Python 3.8 expressions, line == end_line, + # so we only loop once. + for scope_line in range(line, end_line + 1): + if scope_line in self.ignored_lines[file]: + # Annotation requests us to ignore all errors on this line. + self.used_ignored_lines[file].add(scope_line) + return if file in self.ignored_files: return if info.only_once: diff --git a/mypy/fastparse.py b/mypy/fastparse.py index d47e1a97148d..c788ab8932fa 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -276,6 +276,7 @@ def visit(self, node: Optional[AST]) -> Any: def set_line(self, node: N, n: Union[ast3.expr, ast3.stmt]) -> N: node.line = n.lineno node.column = n.col_offset + node.end_line = getattr(n, "end_lineno", None) return node def translate_expr_list(self, l: Sequence[AST]) -> List[Expression]: diff --git a/mypy/messages.py b/mypy/messages.py index 80c5eaa665ec..8698154eba0c 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -111,11 +111,18 @@ def report(self, msg: str, context: Optional[Context], severity: str, file: Optional[str] = None, origin: Optional[Context] = None, offset: int = 0) -> None: """Report an error or note (unless disabled).""" + if origin is not None: + end_line = origin.end_line + elif context is not None: + end_line = context.end_line + else: + end_line = None if self.disable_count <= 0: self.errors.report(context.get_line() if context else -1, context.get_column() if context else -1, msg, severity=severity, file=file, offset=offset, - origin_line=origin.get_line() if origin else None) + 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, origin: Optional[Context] = None) -> None: diff --git a/mypy/nodes.py b/mypy/nodes.py index 06be60b0f6fe..e6d217a8f014 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -22,11 +22,12 @@ class Context: """Base type for objects that are valid as error message locations.""" - __slots__ = ('line', 'column') + __slots__ = ('line', 'column', 'end_line') def __init__(self, line: int = -1, column: int = -1) -> None: self.line = line self.column = column + self.end_line = None # type: Optional[int] def set_line(self, target: Union['Context', int], column: Optional[int] = None) -> None: """If target is a node, pull line (and column) information @@ -38,6 +39,7 @@ def set_line(self, target: Union['Context', int], column: Optional[int] = None) else: self.line = target.line self.column = target.column + self.end_line = target.end_line if column is not None: self.column = column From 08ab70492fc442bab163473044ccb3341a924d5e Mon Sep 17 00:00:00 2001 From: Brandt Bucher Date: Wed, 10 Apr 2019 18:06:53 -0700 Subject: [PATCH 12/12] Don't copy end_lineno info for statements. --- mypy/fastparse.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/fastparse.py b/mypy/fastparse.py index c788ab8932fa..c2366dd71fbc 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -276,7 +276,7 @@ def visit(self, node: Optional[AST]) -> Any: def set_line(self, node: N, n: Union[ast3.expr, ast3.stmt]) -> N: node.line = n.lineno node.column = n.col_offset - node.end_line = getattr(n, "end_lineno", None) + node.end_line = getattr(n, "end_lineno", None) if isinstance(n, ast3.expr) else None return node def translate_expr_list(self, l: Sequence[AST]) -> List[Expression]: