From 5f88e5ddf5601ddc263a739ea93ed2bf2c5430b6 Mon Sep 17 00:00:00 2001 From: Matthias Schoettle Date: Sat, 9 Mar 2024 10:57:12 -0500 Subject: [PATCH 1/8] Add test for line and column end for missing function type annotation --- test-data/unit/check-columns.test | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test-data/unit/check-columns.test b/test-data/unit/check-columns.test index 44524b9df943..612a4d01a93b 100644 --- a/test-data/unit/check-columns.test +++ b/test-data/unit/check-columns.test @@ -178,6 +178,18 @@ if int(): def g(x): # E:5: Function is missing a type annotation pass +[case testColumnEndFunctionMissingTypeAnnotation] +# flags: --disallow-untyped-defs --show-error-end +if int(): + def f(x: int): + pass + + def g(x): + pass +[out] +main:3:5:3:15: error: Function is missing a return type annotation +main:6:5:6:10: error: Function is missing a type annotation + [case testColumnNameIsNotDefined] ((x)) # E:3: Name "x" is not defined From c780ac1e8766ed84b665efe59aa530a11a9dcccd Mon Sep 17 00:00:00 2001 From: Matthias Schoettle Date: Sat, 9 Mar 2024 14:48:36 -0500 Subject: [PATCH 2/8] Change end of line and column to be same as start of node --- mypy/checker.py | 3 +++ mypy/messages.py | 18 +++++++++++++++--- test-data/unit/check-columns.test | 4 ++-- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 941dc06f1c71..689e545a055d 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1493,6 +1493,9 @@ def is_unannotated_any(t: Type) -> bool: show_untyped = not self.is_typeshed_stub or self.options.warn_incomplete_stub check_incomplete_defs = self.options.disallow_incomplete_defs and has_explicit_annotation if show_untyped and (self.options.disallow_untyped_defs or check_incomplete_defs): + # TODO: changing end line and column here changes this information for the node itself + # fdef.end_line = fdef.line + # fdef.end_column = fdef.column + 1 if fdef.type is None and self.options.disallow_untyped_defs: if not fdef.arguments or ( len(fdef.arguments) == 1 diff --git a/mypy/messages.py b/mypy/messages.py index 92b57ef781a2..cbc8bd0475c9 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -256,16 +256,28 @@ def span_from_context(ctx: Context) -> Iterable[int]: assert origin_span is not None origin_span = itertools.chain(origin_span, span_from_context(secondary_context)) + column = context.column if context else -1 + end_line = context.end_line if context else -1 + end_column = context.end_column if context else -1 + + # set end line and column to same as start of context for function definitions + # this avoids errors being reported in IDEs for the whole function + # TODO: figure out if it's possible to find the end of the function definition line + if isinstance(context, FuncDef): + end_line = context.line + # column is 1-based, see also format_messages in errors.py + end_column = column + 1 + self.errors.report( context.line if context else -1, - context.column if context else -1, + column, msg, severity=severity, file=file, offset=offset, origin_span=origin_span, - end_line=context.end_line if context else -1, - end_column=context.end_column if context else -1, + end_line=end_line, + end_column=end_column, code=code, allow_dups=allow_dups, ) diff --git a/test-data/unit/check-columns.test b/test-data/unit/check-columns.test index 612a4d01a93b..0a376426fafb 100644 --- a/test-data/unit/check-columns.test +++ b/test-data/unit/check-columns.test @@ -187,8 +187,8 @@ if int(): def g(x): pass [out] -main:3:5:3:15: error: Function is missing a return type annotation -main:6:5:6:10: error: Function is missing a type annotation +main:3:5:3:5: error: Function is missing a return type annotation +main:6:5:6:5: error: Function is missing a type annotation [case testColumnNameIsNotDefined] ((x)) # E:3: Name "x" is not defined From 8b7802ca49ea347e86e0cf21f63fcb999c351e8c Mon Sep 17 00:00:00 2001 From: Matthias Schoettle Date: Mon, 6 May 2024 13:35:17 -0400 Subject: [PATCH 3/8] Add end line and column for the function definition itself --- mypy/nodes.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/mypy/nodes.py b/mypy/nodes.py index bb278d92392d..2a762abc09bb 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -750,6 +750,8 @@ class FuncDef(FuncItem, SymbolNode, Statement): # Present only when a function is decorated with @typing.datasclass_transform or similar "dataclass_transform_spec", "docstring", + "def_end_line", + "def_end_column", ) __match_args__ = ("name", "arguments", "type", "body") @@ -778,6 +780,9 @@ def __init__( self.is_mypy_only = False self.dataclass_transform_spec: DataclassTransformSpec | None = None self.docstring: str | None = None + # track the end of the function definition itself + self.def_end_line: int | None = None + self.def_end_column: int | None = None @property def name(self) -> str: From 6dee499a5d849f96ae6585247a859302e7ff4f79 Mon Sep 17 00:00:00 2001 From: Matthias Schoettle Date: Mon, 6 May 2024 13:43:09 -0400 Subject: [PATCH 4/8] Determine end line and column of function itself --- mypy/fastparse.py | 21 +++++++++++++++++++++ mypy/messages.py | 7 +++---- test-data/unit/check-columns.test | 13 +++++++++---- 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/mypy/fastparse.py b/mypy/fastparse.py index e208e4d0b7d9..2e0d976a54e1 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -983,10 +983,31 @@ def do_func_def( end_line = getattr(n, "end_lineno", None) end_column = getattr(n, "end_col_offset", None) + # Determine end of the function definition itself + # Fall back to the end of the function definition including its body + def_end_line: int | None = n.lineno + def_end_column: int | None = n.col_offset + + returns = n.returns + # use the return type hint if defined + if returns is not None: + def_end_line = returns.end_lineno + def_end_column = returns.end_col_offset + # otherwise use the last argument in the function definition + elif len(args) > 0: + last_arg = args[-1] + initializer = last_arg.initializer + + def_end_line = initializer.end_line if initializer else last_arg.end_line + def_end_column = initializer.end_column if initializer else last_arg.end_column + self.class_and_function_stack.pop() self.class_and_function_stack.append("F") body = self.as_required_block(n.body, can_strip=True, is_coroutine=is_coroutine) func_def = FuncDef(n.name, args, body, func_type) + func_def.def_end_line = def_end_line + func_def.def_end_column = def_end_column + if isinstance(func_def.type, CallableType): # semanal.py does some in-place modifications we want to avoid func_def.unanalyzed_type = func_def.type.copy_modified() diff --git a/mypy/messages.py b/mypy/messages.py index 424ae1546e9e..64c312612365 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -256,7 +256,6 @@ def span_from_context(ctx: Context) -> Iterable[int]: assert origin_span is not None origin_span = itertools.chain(origin_span, span_from_context(secondary_context)) - column = context.column if context else -1 end_line = context.end_line if context else -1 end_column = context.end_column if context else -1 @@ -264,13 +263,13 @@ def span_from_context(ctx: Context) -> Iterable[int]: # this avoids errors being reported in IDEs for the whole function # TODO: figure out if it's possible to find the end of the function definition line if isinstance(context, FuncDef): - end_line = context.line + end_line = context.def_end_line # column is 1-based, see also format_messages in errors.py - end_column = column + 1 + end_column = context.def_end_column + 1 if context.def_end_column else end_column self.errors.report( context.line if context else -1, - column, + context.column if context else -1, msg, severity=severity, file=file, diff --git a/test-data/unit/check-columns.test b/test-data/unit/check-columns.test index 0a376426fafb..600cabcce9b8 100644 --- a/test-data/unit/check-columns.test +++ b/test-data/unit/check-columns.test @@ -180,15 +180,20 @@ if int(): [case testColumnEndFunctionMissingTypeAnnotation] # flags: --disallow-untyped-defs --show-error-end +from typing import Any, Optional if int(): - def f(x: int): + def f(x: int, foo: Optional[str]): pass - def g(x): + def g(x: int, foo: Optional[str] = None): + pass + + def h(x, foo = None): pass [out] -main:3:5:3:5: error: Function is missing a return type annotation -main:6:5:6:5: error: Function is missing a type annotation +main:4:5:4:37: error: Function is missing a return type annotation +main:7:5:7:44: error: Function is missing a return type annotation +main:10:5:10:24: error: Function is missing a type annotation [case testColumnNameIsNotDefined] ((x)) # E:3: Name "x" is not defined From 37ee83368c60e56302c0a4f9f0ec7091db41b4a9 Mon Sep 17 00:00:00 2001 From: Matthias Schoettle Date: Tue, 7 May 2024 15:00:35 -0400 Subject: [PATCH 5/8] Add more tests --- mypy/fastparse.py | 1 + mypy/messages.py | 4 +++- test-data/unit/check-columns.test | 36 +++++++++++++++++++++++++------ 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/mypy/fastparse.py b/mypy/fastparse.py index 2e0d976a54e1..94b5718fded2 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -986,6 +986,7 @@ def do_func_def( # Determine end of the function definition itself # Fall back to the end of the function definition including its body def_end_line: int | None = n.lineno + # TODO: to go to the end of the function name: n.col_offset + 4 + len(n.name) def_end_column: int | None = n.col_offset returns = n.returns diff --git a/mypy/messages.py b/mypy/messages.py index 64c312612365..2f4fda54d2ee 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -265,7 +265,9 @@ def span_from_context(ctx: Context) -> Iterable[int]: if isinstance(context, FuncDef): end_line = context.def_end_line # column is 1-based, see also format_messages in errors.py - end_column = context.def_end_column + 1 if context.def_end_column else end_column + end_column = ( + context.def_end_column + 1 if context.def_end_column is not None else end_column + ) self.errors.report( context.line if context else -1, diff --git a/test-data/unit/check-columns.test b/test-data/unit/check-columns.test index 600cabcce9b8..e0be30a02a35 100644 --- a/test-data/unit/check-columns.test +++ b/test-data/unit/check-columns.test @@ -178,22 +178,46 @@ if int(): def g(x): # E:5: Function is missing a type annotation pass +[case testPrettyFunctionMissingTypeAnnotation] +# flags: --disallow-untyped-defs --pretty +def function_with_long_name(): + pass +[out] +main:2:1: error: Function is missing a return type annotation + def function_with_long_name(): + ^ +main:2:1: note: Use "-> None" if function does not return a value + [case testColumnEndFunctionMissingTypeAnnotation] # flags: --disallow-untyped-defs --show-error-end from typing import Any, Optional if int(): - def f(x: int, foo: Optional[str]): + def f(): + pass + + def f_no_return(x: int, foo: Optional[str]): + pass + + def f_default(x: int, foo: Optional[str] = None): + pass + + def f_default_untyped(x, foo = None): pass - def g(x: int, foo: Optional[str] = None): + def f_args(x, *args): pass - def h(x, foo = None): + def f_kwargs(x, *args, **kwargs): pass +[builtins fixtures/tuple.pyi] [out] -main:4:5:4:37: error: Function is missing a return type annotation -main:7:5:7:44: error: Function is missing a return type annotation -main:10:5:10:24: error: Function is missing a type annotation +main:4:5:4:5: error: Function is missing a return type annotation +main:4:5:4:5: note: Use "-> None" if function does not return a value +main:7:5:7:47: error: Function is missing a return type annotation +main:10:5:10:52: error: Function is missing a return type annotation +main:13:5:13:40: error: Function is missing a type annotation +main:16:5:16:24: error: Function is missing a type annotation +main:19:5:19:36: error: Function is missing a type annotation [case testColumnNameIsNotDefined] ((x)) # E:3: Name "x" is not defined From 081c8cb661671453a08934addab799d8e25797c9 Mon Sep 17 00:00:00 2001 From: Matthias Schoettle Date: Tue, 7 May 2024 15:06:32 -0400 Subject: [PATCH 6/8] Add extra test case --- test-data/unit/check-columns.test | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test-data/unit/check-columns.test b/test-data/unit/check-columns.test index e0be30a02a35..e2e251a5e6c4 100644 --- a/test-data/unit/check-columns.test +++ b/test-data/unit/check-columns.test @@ -219,6 +219,21 @@ main:13:5:13:40: error: Function is missing a type annotation main:16:5:16:24: error: Function is missing a type annotation main:19:5:19:36: error: Function is missing a type annotation +[case testColumnEndFunctionMissingTypeAnnotationWithReturnType] +# flags: --disallow-untyped-defs --show-error-end +def f() -> None: + pass + +def f_partially_typed(x: int, foo) -> None: + pass + +def f_untyped(x, foo, *args, **kwargs) -> None: + pass +[builtins fixtures/tuple.pyi] +[out] +main:5:1:5:43: error: Function is missing a type annotation for one or more arguments +main:8:1:8:47: error: Function is missing a type annotation for one or more arguments + [case testColumnNameIsNotDefined] ((x)) # E:3: Name "x" is not defined From b83549404431ebf271ffb25d2e303330dcdb28b2 Mon Sep 17 00:00:00 2001 From: Matthias Schoettle Date: Wed, 8 May 2024 10:47:46 -0400 Subject: [PATCH 7/8] Cleanup --- mypy/checker.py | 3 --- mypy/fastparse.py | 11 +++++------ mypy/messages.py | 4 +--- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 7a5f2f9810e4..9c10cd2fc30d 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1494,9 +1494,6 @@ def is_unannotated_any(t: Type) -> bool: show_untyped = not self.is_typeshed_stub or self.options.warn_incomplete_stub check_incomplete_defs = self.options.disallow_incomplete_defs and has_explicit_annotation if show_untyped and (self.options.disallow_untyped_defs or check_incomplete_defs): - # TODO: changing end line and column here changes this information for the node itself - # fdef.end_line = fdef.line - # fdef.end_column = fdef.column + 1 if fdef.type is None and self.options.disallow_untyped_defs: if not fdef.arguments or ( len(fdef.arguments) == 1 diff --git a/mypy/fastparse.py b/mypy/fastparse.py index 94b5718fded2..8e482c7c84f2 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -979,22 +979,21 @@ def do_func_def( _dummy_fallback, ) - # End position is always the same. end_line = getattr(n, "end_lineno", None) end_column = getattr(n, "end_col_offset", None) - # Determine end of the function definition itself - # Fall back to the end of the function definition including its body + # End line and column span the whole function including its body. + # Determine end of the function definition itself. + # Fall back to the end of the function definition including its body. def_end_line: int | None = n.lineno - # TODO: to go to the end of the function name: n.col_offset + 4 + len(n.name) def_end_column: int | None = n.col_offset returns = n.returns - # use the return type hint if defined + # Use the return type hint if defined. if returns is not None: def_end_line = returns.end_lineno def_end_column = returns.end_col_offset - # otherwise use the last argument in the function definition + # Otherwise use the last argument in the function definition. elif len(args) > 0: last_arg = args[-1] initializer = last_arg.initializer diff --git a/mypy/messages.py b/mypy/messages.py index 2f4fda54d2ee..2532467b86bf 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -259,9 +259,7 @@ def span_from_context(ctx: Context) -> Iterable[int]: end_line = context.end_line if context else -1 end_column = context.end_column if context else -1 - # set end line and column to same as start of context for function definitions - # this avoids errors being reported in IDEs for the whole function - # TODO: figure out if it's possible to find the end of the function definition line + # FuncDef's end includes the body, use the def's end information if available. if isinstance(context, FuncDef): end_line = context.def_end_line # column is 1-based, see also format_messages in errors.py From 5a7c0ec5e9a410642521c646d97746ce857c7fff Mon Sep 17 00:00:00 2001 From: Matthias Schoettle Date: Thu, 9 May 2024 16:24:41 -0400 Subject: [PATCH 8/8] Add multiline test --- test-data/unit/check-columns.test | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/test-data/unit/check-columns.test b/test-data/unit/check-columns.test index e2e251a5e6c4..6fe5c1498699 100644 --- a/test-data/unit/check-columns.test +++ b/test-data/unit/check-columns.test @@ -234,6 +234,23 @@ def f_untyped(x, foo, *args, **kwargs) -> None: main:5:1:5:43: error: Function is missing a type annotation for one or more arguments main:8:1:8:47: error: Function is missing a type annotation for one or more arguments +[case testColumnEndMultiline] +# flags: --disallow-untyped-defs --warn-no-return --show-error-end +def f( + x: int, + y: int, +): + pass + +def g( + x: int, + y: int, +) -> int: + x = 1 +[out] +main:2:1:4:11: error: Function is missing a return type annotation +main:8:1:11:9: error: Missing return statement + [case testColumnNameIsNotDefined] ((x)) # E:3: Name "x" is not defined