Skip to content

Commit 6e325ee

Browse files
committed
Add a secondary context for this error message
Add another context to this message reporting. This means that we'll now check both contexts when looking at what errors to ignore.
1 parent 7a3d4ab commit 6e325ee

File tree

4 files changed

+109
-18
lines changed

4 files changed

+109
-18
lines changed

mypy/checker.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2082,6 +2082,7 @@ def erase_override(t: Type) -> Type:
20822082
arg_type_in_super,
20832083
supertype,
20842084
context,
2085+
secondary_context=node,
20852086
)
20862087
emitted_msg = True
20872088

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/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
@@ -210,34 +211,40 @@ def report(
210211
origin: Context | None = None,
211212
offset: int = 0,
212213
allow_dups: bool = False,
214+
secondary_context: Context | None = None,
213215
) -> None:
214216
"""Report an error or note (unless disabled).
215217
216218
Note that context controls where error is reported, while origin controls
217219
where # type: ignore comments have effect.
218220
"""
219221

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

234-
origin_span: tuple[int, int] | None
236+
origin_span: Iterable[int] | None
235237
if origin is not None:
236238
origin_span = span_from_context(origin)
237239
elif context is not None:
238240
origin_span = span_from_context(context)
239241
else:
240242
origin_span = None
243+
244+
if secondary_context is not None:
245+
assert origin_span is not None
246+
origin_span = itertools.chain(origin_span, span_from_context(secondary_context))
247+
241248
self.errors.report(
242249
context.line if context else -1,
243250
context.column if context else -1,
@@ -260,9 +267,18 @@ def fail(
260267
code: ErrorCode | None = None,
261268
file: str | None = None,
262269
allow_dups: bool = False,
270+
secondary_context: Context | None = None,
263271
) -> None:
264272
"""Report an error message (unless disabled)."""
265-
self.report(msg, context, "error", code=code, file=file, allow_dups=allow_dups)
273+
self.report(
274+
msg,
275+
context,
276+
"error",
277+
code=code,
278+
file=file,
279+
allow_dups=allow_dups,
280+
secondary_context=secondary_context,
281+
)
266282

267283
def note(
268284
self,
@@ -274,6 +290,7 @@ def note(
274290
allow_dups: bool = False,
275291
*,
276292
code: ErrorCode | None = None,
293+
secondary_context: Context | None = None,
277294
) -> None:
278295
"""Report a note (unless disabled)."""
279296
self.report(
@@ -285,6 +302,7 @@ def note(
285302
offset=offset,
286303
allow_dups=allow_dups,
287304
code=code,
305+
secondary_context=secondary_context,
288306
)
289307

290308
def note_multiline(
@@ -295,11 +313,20 @@ def note_multiline(
295313
offset: int = 0,
296314
allow_dups: bool = False,
297315
code: ErrorCode | None = None,
316+
*,
317+
secondary_context: Context | None = None,
298318
) -> None:
299319
"""Report as many notes as lines in the message (unless disabled)."""
300320
for msg in messages.splitlines():
301321
self.report(
302-
msg, context, "note", file=file, offset=offset, allow_dups=allow_dups, code=code
322+
msg,
323+
context,
324+
"note",
325+
file=file,
326+
offset=offset,
327+
allow_dups=allow_dups,
328+
code=code,
329+
secondary_context=secondary_context,
303330
)
304331

305332
#
@@ -1153,6 +1180,7 @@ def argument_incompatible_with_supertype(
11531180
arg_type_in_supertype: Type,
11541181
supertype: str,
11551182
context: Context,
1183+
secondary_context: Context,
11561184
) -> None:
11571185
target = self.override_target(name, name_in_supertype, supertype)
11581186
arg_type_in_supertype_f = format_type_bare(arg_type_in_supertype)
@@ -1163,17 +1191,26 @@ def argument_incompatible_with_supertype(
11631191
),
11641192
context,
11651193
code=codes.OVERRIDE,
1194+
secondary_context=secondary_context,
1195+
)
1196+
self.note(
1197+
"This violates the Liskov substitution principle",
1198+
context,
1199+
code=codes.OVERRIDE,
1200+
secondary_context=secondary_context,
11661201
)
1167-
self.note("This violates the Liskov substitution principle", context, code=codes.OVERRIDE)
11681202
self.note(
11691203
"See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides",
11701204
context,
11711205
code=codes.OVERRIDE,
1206+
secondary_context=secondary_context,
11721207
)
11731208

11741209
if name == "__eq__" and type_name:
11751210
multiline_msg = self.comparison_method_example_msg(class_name=type_name)
1176-
self.note_multiline(multiline_msg, context, code=codes.OVERRIDE)
1211+
self.note_multiline(
1212+
multiline_msg, context, code=codes.OVERRIDE, secondary_context=secondary_context
1213+
)
11771214

11781215
def comparison_method_example_msg(self, class_name: str) -> str:
11791216
return dedent(

test-data/unit/check-classes.test

Lines changed: 53 additions & 0 deletions
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

0 commit comments

Comments
 (0)