Repository: psf/black

Issue:
Black crashes when moving type: ignore comments
**Describe the bug**

Black crashes when run on some code that includes a multiline statement with `#type: ignore` comments on multiple lines.

**To Reproduce**

For example, take this code:

```python
class Foo:
   def foo(self) -> None:
       if True:
           self.foooooo= (  # type:ignore[error-code]
               Baaaaaaar(  # type:ignore[error-code]
                   self.baz()
               )
           ) 
```

And run it with these arguments:

```sh
$ black file.py
```

The resulting error is:

> cannot format black.py: INTERNAL ERROR: Black produced code that is not equivalent to the source. Please report a bug on https:// github.com/psf/black/issues. This diff might be helpful

Logfile:
```
--- src
+++ dst
@@ -108,8 +108,6 @@
        'Foo',  # str
    )  # /ClassDef
  type_ignores=
    TypeIgnore(
    )  # /TypeIgnore
-    TypeIgnore(
-    )  # /TypeIgnore
)  # /Module
\ No newline at end of file
```

**Expected behavior**
Run without errors, leaving `#type: ignore` comments with the code they are annotating.

**Environment**

- Black's version: main, 23.3.0
- OS and Python version: Windows 10, python 3.11.2

**Additional context**

The `#type: ignore` comments are for mypy.



Discussion:

Resolving patch:
diff --git a/src/black/linegen.py b/src/black/linegen.py
--- a/src/black/linegen.py
+++ b/src/black/linegen.py
@@ -49,6 +49,7 @@
     is_stub_body,
     is_stub_suite,
     is_tuple_containing_walrus,
+    is_type_ignore_comment_string,
     is_vararg,
     is_walrus_assignment,
     is_yield,
@@ -1399,8 +1400,13 @@ def maybe_make_parens_invisible_in_atom(
     if is_lpar_token(first) and is_rpar_token(last):
         middle = node.children[1]
         # make parentheses invisible
-        first.value = ""
-        last.value = ""
+        if (
+            # If the prefix of `middle` includes a type comment with
+            # ignore annotation, then we do not remove the parentheses
+            not is_type_ignore_comment_string(middle.prefix.strip())
+        ):
+            first.value = ""
+            last.value = ""
         maybe_make_parens_invisible_in_atom(
             middle,
             parent=parent,
diff --git a/src/black/lines.py b/src/black/lines.py
--- a/src/black/lines.py
+++ b/src/black/lines.py
@@ -28,6 +28,7 @@
     is_multiline_string,
     is_one_sequence_between,
     is_type_comment,
+    is_type_ignore_comment,
     is_with_or_async_with_stmt,
     replace_child,
     syms,
@@ -251,7 +252,7 @@ def contains_uncollapsable_type_comments(self) -> bool:
             for comment in comments:
                 if is_type_comment(comment):
                     if comment_seen or (
-                        not is_type_comment(comment, " ignore")
+                        not is_type_ignore_comment(comment)
                         and leaf_id not in ignored_ids
                     ):
                         return True
@@ -288,7 +289,7 @@ def contains_unsplittable_type_ignore(self) -> bool:
             # line.
             for node in self.leaves[-2:]:
                 for comment in self.comments.get(id(node), []):
-                    if is_type_comment(comment, " ignore"):
+                    if is_type_ignore_comment(comment):
                         return True
 
         return False
diff --git a/src/black/nodes.py b/src/black/nodes.py
--- a/src/black/nodes.py
+++ b/src/black/nodes.py
@@ -816,12 +816,27 @@ def is_async_stmt_or_funcdef(leaf: Leaf) -> bool:
     )
 
 
-def is_type_comment(leaf: Leaf, suffix: str = "") -> bool:
-    """Return True if the given leaf is a special comment.
-    Only returns true for type comments for now."""
+def is_type_comment(leaf: Leaf) -> bool:
+    """Return True if the given leaf is a type comment. This function should only
+    be used for general type comments (excluding ignore annotations, which should
+    use `is_type_ignore_comment`). Note that general type comments are no longer
+    used in modern version of Python, this function may be deprecated in the future."""
     t = leaf.type
     v = leaf.value
-    return t in {token.COMMENT, STANDALONE_COMMENT} and v.startswith("# type:" + suffix)
+    return t in {token.COMMENT, STANDALONE_COMMENT} and v.startswith("# type:")
+
+
+def is_type_ignore_comment(leaf: Leaf) -> bool:
+    """Return True if the given leaf is a type comment with ignore annotation."""
+    t = leaf.type
+    v = leaf.value
+    return t in {token.COMMENT, STANDALONE_COMMENT} and is_type_ignore_comment_string(v)
+
+
+def is_type_ignore_comment_string(value: str) -> bool:
+    """Return True if the given string match with type comment with
+    ignore annotation."""
+    return value.startswith("# type: ignore")
 
 
 def wrap_in_parentheses(parent: Node, child: LN, *, visible: bool = True) -> None:


Test patch:
diff --git a/tests/data/simple_cases/multiline_consecutive_open_parentheses_ignore.py b/tests/data/simple_cases/multiline_consecutive_open_parentheses_ignore.py
new file mode 100644
--- /dev/null
+++ b/tests/data/simple_cases/multiline_consecutive_open_parentheses_ignore.py
@@ -0,0 +1,41 @@
+# This is a regression test. Issue #3737
+
+a = (  # type: ignore
+    int(  # type: ignore
+        int(  # type: ignore
+            int(  # type: ignore
+                6
+            )
+        )
+    )
+)
+
+b = (
+    int(
+        6
+    )
+)
+
+print(   "111") # type: ignore
+print(   "111"                         ) # type: ignore
+print(   "111"       ) # type: ignore
+
+
+# output
+
+
+# This is a regression test. Issue #3737
+
+a = (  # type: ignore
+    int(  # type: ignore
+        int(  # type: ignore
+            int(6)  # type: ignore
+        )
+    )
+)
+
+b = int(6)
+
+print("111")  # type: ignore
+print("111")  # type: ignore
+print("111")  # type: ignore
\ No newline at end of file