Repository: psf/black

Issue:
Comments removed in pyi files
Hi! I accidentally ran black on my entire venv in a project, and one of the files it spat out "INTERNAL ERROR: Black produced code that is not equivalent to the source." on was typeshed/stdlib/3/configparser.pyi. I'm not sure what version of typeshed that is, but the issue doesn't repro with the current version on GitHub.

Regardless, here's the diff:

```
--- src
+++ dst
@@ -5135,8 +5135,6 @@
   type_ignores=
     TypeIgnore(
     )  # /TypeIgnore
     TypeIgnore(
     )  # /TypeIgnore
-    TypeIgnore(
-    )  # /TypeIgnore
 )  # /Module
```

Discussion:
github-actions[bot]: **diff-shades** reports zero changes comparing this PR (18a64969f093e190aa22b7c7b8f630a50db254c2) to main (d1248ca9beaf0ba526d265f4108836d89cf551b7).

---

[**What is this?**](https://black.readthedocs.io/en/latest/contributing/gauging_changes.html#diff-shades) | [Workflow run](https://github.com/psf/black/actions/runs/5352783385) | [diff-shades documentation](https://github.com/ichard26/diff-shades#readme)

Patch:
diff --git a/CHANGES.md b/CHANGES.md
index 1b0475f..bb30429 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -15,6 +15,8 @@
 
 - Fix a bug where an illegal trailing comma was added to return type annotations using
   PEP 604 unions (#3735)
+- Fix several bugs and crashes where comments in stub files were removed or mishandled
+  under some circumstances. (#3745)
 - Fix a bug where multi-line open parenthesis magic comment like `type: ignore` were not
   correctly parsed (#3740)
 
@@ -55,6 +57,7 @@
 
 <!-- Changes that improve Black's performance. -->
 
+- Speed up _Black_ significantly when the cache is full (#3751)
 - Avoid importing `IPython` in a case where we wouldn't need it (#3748)
 
 ### Output
@@ -84,6 +87,9 @@
 
 ### Documentation
 
+- Add a CITATION.cff file to the root of the repository, containing metadata on how to
+  cite this software (#3723)
+
 <!-- Major changes to documentation and policies. Small docs changes
      don't need a changelog entry. -->
 
diff --git a/CITATION.cff b/CITATION.cff
new file mode 100644
index 0000000..ddf64f6
--- /dev/null
+++ b/CITATION.cff
@@ -0,0 +1,22 @@
+cff-version: 1.2.0
+title: "Black: The uncompromising Python code formatter"
+message: >-
+  If you use this software, please cite it using the metadata from this file.
+type: software
+authors:
+  - family-names: Langa
+    given-names: Łukasz
+  - name: "contributors to Black"
+repository-code: "https://github.com/psf/black"
+url: "https://black.readthedocs.io/en/stable/"
+abstract: >-
+  Black is the uncompromising Python code formatter. By using it, you agree to cede
+  control over minutiae ofhand-formatting. In return, Black gives you speed,
+  determinism, and freedom from pycodestyle nagging about formatting. You will save time
+  and mental energy for more important matters.
+
+  Blackened code looks the same regardless of the project you're reading. Formatting
+  becomes transparent after a while and you can focus on the content instead.
+
+  Black makes code review faster by producing the smallest diffs possible.
+license: MIT
diff --git a/src/black/files.py b/src/black/files.py
index 4e2209e..ef6895e 100644
--- a/src/black/files.py
+++ b/src/black/files.py
@@ -276,15 +276,24 @@ def normalize_path_maybe_ignore(
     return root_relative_path
 
 
-def path_is_ignored(
-    path: Path, gitignore_dict: Dict[Path, PathSpec], report: Report
+def _path_is_ignored(
+    root_relative_path: str,
+    root: Path,
+    gitignore_dict: Dict[Path, PathSpec],
+    report: Report,
 ) -> bool:
+    path = root / root_relative_path
+    # Note that this logic is sensitive to the ordering of gitignore_dict. Callers must
+    # ensure that gitignore_dict is ordered from least specific to most specific.
     for gitignore_path, pattern in gitignore_dict.items():
-        relative_path = normalize_path_maybe_ignore(path, gitignore_path, report)
-        if relative_path is None:
+        try:
+            relative_path = path.relative_to(gitignore_path).as_posix()
+        except ValueError:
             break
         if pattern.match_file(relative_path):
-            report.path_ignored(path, "matches a .gitignore file content")
+            report.path_ignored(
+                path.relative_to(root), "matches a .gitignore file content"
+            )
             return True
     return False
 
@@ -326,7 +335,9 @@ def gen_python_files(
             continue
 
         # First ignore files matching .gitignore, if passed
-        if gitignore_dict and path_is_ignored(child, gitignore_dict, report):
+        if gitignore_dict and _path_is_ignored(
+            normalized_path, root, gitignore_dict, report
+        ):
             continue
 
         # Then ignore with `--exclude` `--extend-exclude` and `--force-exclude` options.
diff --git a/src/black/nodes.py b/src/black/nodes.py
index ef42278..45423b2 100644
--- a/src/black/nodes.py
+++ b/src/black/nodes.py
@@ -714,6 +714,11 @@ def is_multiline_string(leaf: Leaf) -> bool:
 
 def is_stub_suite(node: Node) -> bool:
     """Return True if `node` is a suite with a stub body."""
+
+    # If there is a comment, we want to keep it.
+    if node.prefix.strip():
+        return False
+
     if (
         len(node.children) != 4
         or node.children[0].type != token.NEWLINE
@@ -722,6 +727,9 @@ def is_stub_suite(node: Node) -> bool:
     ):
         return False
 
+    if node.children[3].prefix.strip():
+        return False
+
     return is_stub_body(node.children[2])
 
 
@@ -735,7 +743,8 @@ def is_stub_body(node: LN) -> bool:
 
     child = node.children[0]
     return (
-        child.type == syms.atom
+        not child.prefix.strip()
+        and child.type == syms.atom
         and len(child.children) == 3
         and all(leaf == Leaf(token.DOT, ".") for leaf in child.children)
     )


Test patch:
diff --git a/tests/data/simple_cases/ignore_pyi.py b/tests/data/simple_cases/ignore_pyi.py
new file mode 100644
index 0000000..3ef6107
--- /dev/null
+++ b/tests/data/simple_cases/ignore_pyi.py
@@ -0,0 +1,41 @@
+def f():  # type: ignore
+    ...
+
+class x:  # some comment
+    ...
+
+class y:
+    ...  # comment
+
+# whitespace doesn't matter (note the next line has a trailing space and tab)
+class z:        
+    ...
+
+def g():
+    # hi
+    ...
+
+def h():
+    ...
+    # bye
+
+# output
+
+def f():  # type: ignore
+    ...
+
+class x:  # some comment
+    ...
+
+class y: ...  # comment
+
+# whitespace doesn't matter (note the next line has a trailing space and tab)
+class z: ...
+
+def g():
+    # hi
+    ...
+
+def h():
+    ...
+    # bye
diff --git a/tests/test_black.py b/tests/test_black.py
index dd21d0a..3b3ab72 100644
--- a/tests/test_black.py
+++ b/tests/test_black.py
@@ -508,6 +508,8 @@ def _mocked_calls() -> bool:
             "pathlib.Path.cwd", return_value=working_directory
         ), patch("pathlib.Path.is_dir", side_effect=mock_n_calls([True])):
             ctx = FakeContext()
+            # Note that the root folder (project_root) isn't the folder
+            # named "root" (aka working_directory)
             ctx.obj["root"] = project_root
             report = MagicMock(verbose=True)
             black.get_sources(
@@ -527,7 +529,7 @@ def _mocked_calls() -> bool:
             for _, mock_args, _ in report.path_ignored.mock_calls
         ), "A symbolic link was reported."
         report.path_ignored.assert_called_once_with(
-            Path("child", "b.py"), "matches a .gitignore file content"
+            Path("root", "child", "b.py"), "matches a .gitignore file content"
         )
 
     def test_report_verbose(self) -> None:
diff --git a/tests/test_format.py b/tests/test_format.py
index 8e0ada9..fb4d8eb 100644
--- a/tests/test_format.py
+++ b/tests/test_format.py
@@ -33,9 +33,10 @@ def check_file(
 @pytest.mark.parametrize("filename", all_data_cases("simple_cases"))
 def test_simple_format(filename: str) -> None:
     magic_trailing_comma = filename != "skip_magic_trailing_comma"
-    check_file(
-        "simple_cases", filename, black.Mode(magic_trailing_comma=magic_trailing_comma)
+    mode = black.Mode(
+        magic_trailing_comma=magic_trailing_comma, is_pyi=filename.endswith("_pyi")
     )
+    check_file("simple_cases", filename, mode)
 
 
 @pytest.mark.parametrize("filename", all_data_cases("preview"))