Skip to content

Commit d586070

Browse files
authored
Give arguments a more reasonable location (#14562)
Modifies arguments parsed from the AST to use the AST's row and column information. Modifies function definitions to not overwrite their arguments' locations. Modifies incorrect override messages to use the locations of arguments instead of the method itself. Modifies tests to expect the new locations. I'm not sure whether this handles bound arguments correctly; it passes tests but I don't know whether there's some edge case I'm missing. Fixes #8298.
1 parent 7237831 commit d586070

File tree

11 files changed

+135
-43
lines changed

11 files changed

+135
-43
lines changed

mypy/checker.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2067,15 +2067,22 @@ def erase_override(t: Type) -> Type:
20672067
if not is_subtype(
20682068
original.arg_types[i], erase_override(override.arg_types[i])
20692069
):
2070+
20702071
arg_type_in_super = original.arg_types[i]
2072+
2073+
if isinstance(node, FuncDef):
2074+
context: Context = node.arguments[i + len(override.bound_args)]
2075+
else:
2076+
context = node
20712077
self.msg.argument_incompatible_with_supertype(
20722078
i + 1,
20732079
name,
20742080
type_name,
20752081
name_in_super,
20762082
arg_type_in_super,
20772083
supertype,
2078-
node,
2084+
context,
2085+
secondary_context=node,
20792086
)
20802087
emitted_msg = True
20812088

mypy/errors.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import sys
55
import traceback
66
from collections import defaultdict
7-
from typing import Callable, NoReturn, Optional, TextIO, Tuple, TypeVar
7+
from typing import Callable, Iterable, NoReturn, Optional, TextIO, Tuple, TypeVar
88
from typing_extensions import Final, Literal, TypeAlias as _TypeAlias
99

1010
from mypy import errorcodes as codes
@@ -78,7 +78,7 @@ class ErrorInfo:
7878

7979
# Actual origin of the error message as tuple (path, line number, end line number)
8080
# If end line number is unknown, use line number.
81-
origin: tuple[str, int, int]
81+
origin: tuple[str, Iterable[int]]
8282

8383
# Fine-grained incremental target where this was reported
8484
target: str | None = None
@@ -104,7 +104,7 @@ def __init__(
104104
blocker: bool,
105105
only_once: bool,
106106
allow_dups: bool,
107-
origin: tuple[str, int, int] | None = None,
107+
origin: tuple[str, Iterable[int]] | None = None,
108108
target: str | None = None,
109109
) -> None:
110110
self.import_ctx = import_ctx
@@ -122,7 +122,7 @@ def __init__(
122122
self.blocker = blocker
123123
self.only_once = only_once
124124
self.allow_dups = allow_dups
125-
self.origin = origin or (file, line, line)
125+
self.origin = origin or (file, [line])
126126
self.target = target
127127

128128

@@ -367,7 +367,7 @@ def report(
367367
file: str | None = None,
368368
only_once: bool = False,
369369
allow_dups: bool = False,
370-
origin_span: tuple[int, int] | None = None,
370+
origin_span: Iterable[int] | None = None,
371371
offset: int = 0,
372372
end_line: int | None = None,
373373
end_column: int | None = None,
@@ -411,7 +411,7 @@ def report(
411411
message = " " * offset + message
412412

413413
if origin_span is None:
414-
origin_span = (line, line)
414+
origin_span = [line]
415415

416416
if end_line is None:
417417
end_line = line
@@ -434,7 +434,7 @@ def report(
434434
blocker,
435435
only_once,
436436
allow_dups,
437-
origin=(self.file, *origin_span),
437+
origin=(self.file, origin_span),
438438
target=self.current_target(),
439439
)
440440
self.add_error_info(info)
@@ -467,7 +467,7 @@ def _filter_error(self, file: str, info: ErrorInfo) -> bool:
467467
return False
468468

469469
def add_error_info(self, info: ErrorInfo) -> None:
470-
file, line, end_line = info.origin
470+
file, lines = info.origin
471471
# process the stack of ErrorWatchers before modifying any internal state
472472
# in case we need to filter out the error entirely
473473
# NB: we need to do this both here and in _add_error_info, otherwise we
@@ -478,7 +478,7 @@ def add_error_info(self, info: ErrorInfo) -> None:
478478
if file in self.ignored_lines:
479479
# Check each line in this context for "type: ignore" comments.
480480
# line == end_line for most nodes, so we only loop once.
481-
for scope_line in range(line, end_line + 1):
481+
for scope_line in lines:
482482
if self.is_ignored_error(scope_line, info, self.ignored_lines[file]):
483483
# Annotation requests us to ignore all errors on this line.
484484
self.used_ignored_lines[file][scope_line].append(

mypy/fastparse.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1081,7 +1081,14 @@ def make_argument(
10811081
if argument_elide_name(arg.arg):
10821082
pos_only = True
10831083

1084-
return Argument(Var(arg.arg), arg_type, self.visit(default), kind, pos_only)
1084+
argument = Argument(Var(arg.arg), arg_type, self.visit(default), kind, pos_only)
1085+
argument.set_line(
1086+
arg.lineno,
1087+
arg.col_offset,
1088+
getattr(arg, "end_lineno", None),
1089+
getattr(arg, "end_col_offset", None),
1090+
)
1091+
return argument
10851092

10861093
def fail_arg(self, msg: str, arg: ast3.arg) -> None:
10871094
self.fail(msg, arg.lineno, arg.col_offset)

mypy/messages.py

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from __future__ import annotations
1313

1414
import difflib
15+
import itertools
1516
import re
1617
from contextlib import contextmanager
1718
from textwrap import dedent
@@ -208,34 +209,40 @@ def report(
208209
origin: Context | None = None,
209210
offset: int = 0,
210211
allow_dups: bool = False,
212+
secondary_context: Context | None = None,
211213
) -> None:
212214
"""Report an error or note (unless disabled).
213215
214216
Note that context controls where error is reported, while origin controls
215217
where # type: ignore comments have effect.
216218
"""
217219

218-
def span_from_context(ctx: Context) -> tuple[int, int]:
220+
def span_from_context(ctx: Context) -> Iterable[int]:
219221
"""This determines where a type: ignore for a given context has effect.
220222
221223
Current logic is a bit tricky, to keep as much backwards compatibility as
222224
possible. We may reconsider this to always be a single line (or otherwise
223225
simplify it) when we drop Python 3.7.
224226
"""
225227
if isinstance(ctx, (ClassDef, FuncDef)):
226-
return ctx.deco_line or ctx.line, ctx.line
228+
return range(ctx.deco_line or ctx.line, ctx.line + 1)
227229
elif not isinstance(ctx, Expression):
228-
return ctx.line, ctx.line
230+
return [ctx.line]
229231
else:
230-
return ctx.line, ctx.end_line or ctx.line
232+
return range(ctx.line, (ctx.end_line or ctx.line) + 1)
231233

232-
origin_span: tuple[int, int] | None
234+
origin_span: Iterable[int] | None
233235
if origin is not None:
234236
origin_span = span_from_context(origin)
235237
elif context is not None:
236238
origin_span = span_from_context(context)
237239
else:
238240
origin_span = None
241+
242+
if secondary_context is not None:
243+
assert origin_span is not None
244+
origin_span = itertools.chain(origin_span, span_from_context(secondary_context))
245+
239246
self.errors.report(
240247
context.line if context else -1,
241248
context.column if context else -1,
@@ -258,9 +265,18 @@ def fail(
258265
code: ErrorCode | None = None,
259266
file: str | None = None,
260267
allow_dups: bool = False,
268+
secondary_context: Context | None = None,
261269
) -> None:
262270
"""Report an error message (unless disabled)."""
263-
self.report(msg, context, "error", code=code, file=file, allow_dups=allow_dups)
271+
self.report(
272+
msg,
273+
context,
274+
"error",
275+
code=code,
276+
file=file,
277+
allow_dups=allow_dups,
278+
secondary_context=secondary_context,
279+
)
264280

265281
def note(
266282
self,
@@ -272,6 +288,7 @@ def note(
272288
allow_dups: bool = False,
273289
*,
274290
code: ErrorCode | None = None,
291+
secondary_context: Context | None = None,
275292
) -> None:
276293
"""Report a note (unless disabled)."""
277294
self.report(
@@ -283,6 +300,7 @@ def note(
283300
offset=offset,
284301
allow_dups=allow_dups,
285302
code=code,
303+
secondary_context=secondary_context,
286304
)
287305

288306
def note_multiline(
@@ -293,11 +311,20 @@ def note_multiline(
293311
offset: int = 0,
294312
allow_dups: bool = False,
295313
code: ErrorCode | None = None,
314+
*,
315+
secondary_context: Context | None = None,
296316
) -> None:
297317
"""Report as many notes as lines in the message (unless disabled)."""
298318
for msg in messages.splitlines():
299319
self.report(
300-
msg, context, "note", file=file, offset=offset, allow_dups=allow_dups, code=code
320+
msg,
321+
context,
322+
"note",
323+
file=file,
324+
offset=offset,
325+
allow_dups=allow_dups,
326+
code=code,
327+
secondary_context=secondary_context,
301328
)
302329

303330
#
@@ -1151,6 +1178,7 @@ def argument_incompatible_with_supertype(
11511178
arg_type_in_supertype: Type,
11521179
supertype: str,
11531180
context: Context,
1181+
secondary_context: Context,
11541182
) -> None:
11551183
target = self.override_target(name, name_in_supertype, supertype)
11561184
arg_type_in_supertype_f = format_type_bare(arg_type_in_supertype)
@@ -1161,17 +1189,26 @@ def argument_incompatible_with_supertype(
11611189
),
11621190
context,
11631191
code=codes.OVERRIDE,
1192+
secondary_context=secondary_context,
1193+
)
1194+
self.note(
1195+
"This violates the Liskov substitution principle",
1196+
context,
1197+
code=codes.OVERRIDE,
1198+
secondary_context=secondary_context,
11641199
)
1165-
self.note("This violates the Liskov substitution principle", context, code=codes.OVERRIDE)
11661200
self.note(
11671201
"See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides",
11681202
context,
11691203
code=codes.OVERRIDE,
1204+
secondary_context=secondary_context,
11701205
)
11711206

11721207
if name == "__eq__" and type_name:
11731208
multiline_msg = self.comparison_method_example_msg(class_name=type_name)
1174-
self.note_multiline(multiline_msg, context, code=codes.OVERRIDE)
1209+
self.note_multiline(
1210+
multiline_msg, context, code=codes.OVERRIDE, secondary_context=secondary_context
1211+
)
11751212

11761213
def comparison_method_example_msg(self, class_name: str) -> str:
11771214
return dedent(

mypy/nodes.py

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -701,18 +701,6 @@ def __init__(
701701
def max_fixed_argc(self) -> int:
702702
return self.max_pos
703703

704-
def set_line(
705-
self,
706-
target: Context | int,
707-
column: int | None = None,
708-
end_line: int | None = None,
709-
end_column: int | None = None,
710-
) -> None:
711-
super().set_line(target, column, end_line, end_column)
712-
for arg in self.arguments:
713-
# TODO: set arguments line/column to their precise locations.
714-
arg.set_line(self.line, self.column, self.end_line, end_column)
715-
716704
def is_dynamic(self) -> bool:
717705
return self.type is None
718706

mypy/test/data.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -676,8 +676,8 @@ class DataFileCollector(pytest.Collector):
676676
parent: DataSuiteCollector
677677

678678
@classmethod # We have to fight with pytest here:
679-
def from_parent( # type: ignore[override]
680-
cls, parent: DataSuiteCollector, *, name: str
679+
def from_parent(
680+
cls, parent: DataSuiteCollector, *, name: str # type: ignore[override]
681681
) -> DataFileCollector:
682682
collector = super().from_parent(parent, name=name)
683683
assert isinstance(collector, DataFileCollector)

test-data/unit/check-abstract.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -382,10 +382,10 @@ class A(I):
382382
def g(self, a: 'A') -> 'A':
383383
return A()
384384
[out]
385+
main:11: error: Return type "I" of "h" incompatible with return type "A" in supertype "I"
385386
main:11: error: Argument 1 of "h" is incompatible with supertype "I"; supertype defines the argument type as "I"
386387
main:11: note: This violates the Liskov substitution principle
387388
main:11: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
388-
main:11: error: Return type "I" of "h" incompatible with return type "A" in supertype "I"
389389

390390

391391
-- Accessing abstract members

test-data/unit/check-classes.test

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,59 @@ main:7: note: This violates the Liskov substitution principle
331331
main:7: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
332332
main:9: error: Return type "object" of "h" incompatible with return type "A" in supertype "A"
333333

334+
[case testMethodOverridingWithIncompatibleTypesOnMultipleLines]
335+
class A:
336+
def f(self, x: int, y: str) -> None: pass
337+
class B(A):
338+
def f(
339+
self,
340+
x: int,
341+
y: bool,
342+
) -> None:
343+
pass
344+
[out]
345+
main:7: error: Argument 2 of "f" is incompatible with supertype "A"; supertype defines the argument type as "str"
346+
main:7: note: This violates the Liskov substitution principle
347+
main:7: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
348+
349+
[case testMultiLineMethodOverridingWithIncompatibleTypesIgnorableAtArgument]
350+
class A:
351+
def f(self, x: int, y: str) -> None: pass
352+
353+
class B(A):
354+
def f(
355+
self,
356+
x: int,
357+
y: bool, # type: ignore[override]
358+
) -> None:
359+
pass
360+
361+
[case testMultiLineMethodOverridingWithIncompatibleTypesIgnorableAtDefinition]
362+
class A:
363+
def f(self, x: int, y: str) -> None: pass
364+
class B(A):
365+
def f( # type: ignore[override]
366+
self,
367+
x: int,
368+
y: bool,
369+
) -> None:
370+
pass
371+
372+
[case testMultiLineMethodOverridingWithIncompatibleTypesWrongIgnore]
373+
class A:
374+
def f(self, x: int, y: str) -> None: pass
375+
class B(A):
376+
def f( # type: ignore[return-type]
377+
self,
378+
x: int,
379+
y: bool,
380+
) -> None:
381+
pass
382+
[out]
383+
main:7: error: Argument 2 of "f" is incompatible with supertype "A"; supertype defines the argument type as "str"
384+
main:7: note: This violates the Liskov substitution principle
385+
main:7: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
386+
334387
[case testEqMethodsOverridingWithNonObjects]
335388
class A:
336389
def __eq__(self, other: A) -> bool: pass # Fail
@@ -2626,10 +2679,10 @@ class D(A):
26262679
def __iadd__(self, x: 'A') -> 'B': pass
26272680
[out]
26282681
main:6: error: Return type "A" of "__iadd__" incompatible with return type "B" in "__add__" of supertype "A"
2682+
main:8: error: Signatures of "__iadd__" and "__add__" are incompatible
26292683
main:8: error: Argument 1 of "__iadd__" is incompatible with "__add__" of supertype "A"; supertype defines the argument type as "A"
26302684
main:8: note: This violates the Liskov substitution principle
26312685
main:8: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
2632-
main:8: error: Signatures of "__iadd__" and "__add__" are incompatible
26332686

26342687
[case testGetattribute]
26352688

test-data/unit/check-columns.test

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -238,9 +238,9 @@ if int():
238238
class A:
239239
def f(self, x: int) -> None: pass
240240
class B(A):
241-
def f(self, x: str) -> None: pass # E:5: Argument 1 of "f" is incompatible with supertype "A"; supertype defines the argument type as "int" \
242-
# N:5: This violates the Liskov substitution principle \
243-
# N:5: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
241+
def f(self, x: str) -> None: pass # E:17: Argument 1 of "f" is incompatible with supertype "A"; supertype defines the argument type as "int" \
242+
# N:17: This violates the Liskov substitution principle \
243+
# N:17: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
244244
class C(A):
245245
def f(self, x: int) -> int: pass # E:5: Return type "int" of "f" incompatible with return type "None" in supertype "A"
246246
class D(A):

0 commit comments

Comments
 (0)