Repository: python/mypy
Issue category: Bug Report

Issue:
Crash in `--pretty` mode with `--shadow-file` which shadows a shorter file
**Crash Report**

`--pretty` inserts source file fragments from the file that's being reported. If `--shadow-file` uses a `SHADOW_FILE` which is longer than `SOURCE_FILE`, mypy crashes as it tries to access a line which doesn't exist.

**To Reproduce & Traceback**

`a.py`
```python
b: bytes  # Ensure there is at most 1 newline character in this file
```
`b.py`
```python
a: int = ""
b: bytes = 1
```
```shell
$ mypy a.py --pretty --shadow-file a.py b.py
Traceback (most recent call last):
  ...
  File "<virtual environment>\lib\site-packages\mypy\__main__.py", line 15, in console_entry
    main()
  File "mypy\main.py", line 103, in main
  File "mypy\main.py", line 187, in run_build
  File "mypy\build.py", line 193, in build
  File "mypy\build.py", line 268, in _build
  File "mypy\build.py", line 2950, in dispatch
  File "mypy\build.py", line 3348, in process_graph
  File "mypy\build.py", line 3471, in process_stale_scc
  File "mypy\errors.py", line 923, in file_messages
  File "mypy\errors.py", line 883, in format_messages
IndexError: list index out of range
```

https://github.com/python/mypy/blob/1a2c8e2a4df21532e4952191cad74ae50083f4ad/mypy/errors.py#L883

I think the solution is to insert source file fragments from `SHADOW_FILE` rather than `SOURCE_FILE`. Even if this doesn't crash, the resultant messages don't make sense. For example, inserting extra newlines into `a.py` prevents the crash but you get
```shell
a.py:1:10:1:11: error: Incompatible types in assignment (expression has type "str", variable has type "int")  [assignment]
    b: bytes
            ^
a.py:2:12:2:12: error: Incompatible types in assignment (expression has type "int", variable has type "bytes")  [assignment]
    
       ^
```

**Your Environment**

- Mypy version used: 1.11.2
- Mypy command-line flags: `--pretty --shadow-file`
- Mypy configuration options from `mypy.ini` (and other config files): None
- Python version used: 3.9
- Operating system and version: Windows 11 Home


Hints:
PR welcome!

Patch:
diff --git a/mypy/errors.py b/mypy/errors.py
index 13452b14a237..1b3f485d19c0 100644
--- a/mypy/errors.py
+++ b/mypy/errors.py
@@ -922,9 +922,25 @@ def file_messages(self, path: str, formatter: ErrorFormatter | None = None) -> l
         self.flushed_files.add(path)
         source_lines = None
         if self.options.pretty and self.read_source:
-            source_lines = self.read_source(path)
+            # Find shadow file mapping and read source lines if a shadow file exists for the given path.
+            # If shadow file mapping is not found, read source lines
+            mapped_path = self.find_shadow_file_mapping(path)
+            if mapped_path:
+                source_lines = self.read_source(mapped_path)
+            else:
+                source_lines = self.read_source(path)
         return self.format_messages(error_tuples, source_lines)
 
+    def find_shadow_file_mapping(self, path: str) -> str | None:
+        """Return the shadow file path for a given source file path or None."""
+        if self.options.shadow_file is None:
+            return None
+
+        for i in self.options.shadow_file:
+            if i[0] == path:
+                return i[1]
+        return None
+
     def new_messages(self) -> list[str]:
         """Return a string list of new error messages.
 


Test patch:
diff --git a/test-data/unit/cmdline.test b/test-data/unit/cmdline.test
index 2262b7e7280c..38ea83cdbcf4 100644
--- a/test-data/unit/cmdline.test
+++ b/test-data/unit/cmdline.test
@@ -914,6 +914,23 @@ s4.py:2: error: Incompatible return value type (got "int", expected "str")
 s3.py:2: error: Incompatible return value type (got "List[int]", expected "int")
 s1.py:2: error: Incompatible return value type (got "int", expected "str")
 
+[case testShadowFileWithPretty]
+# cmd: mypy a.py --pretty --shadow-file a.py b.py
+[file a.py]
+b: bytes
+[file b.py]
+a: int = ""
+b: bytes = 1
+[out]
+a.py:1: error: Incompatible types in assignment (expression has type "str",
+variable has type "int")
+    a: int = ""
+             ^~
+a.py:2: error: Incompatible types in assignment (expression has type "int",
+variable has type "bytes")
+    b: bytes = 1
+               ^
+
 [case testConfigWarnUnusedSection1]
 # cmd: mypy foo.py quux.py spam/eggs.py
 [file mypy.ini]