Repository: psf/black

Issue:
Crash on concatenated string + comment
Another crash on master
```
$ cat xxx.py 
xxxxxxx_xxxxxx_xxxxxxx = xxx(
    [
        xxxxxxxxxxxx(
            xxxxxx_xxxxxxx=(
                '((x.xxxxxxxxx = "xxxxxx.xxxxxxxxxxxxxxxxxxxxx") || (x.xxxxxxxxx = "xxxxxxxxxxxx")) && '
                # xxxxx xxxxxxxxxxxx xxxx xxx (xxxxxxxxxxxxxxxx) xx x xxxxxxxxx xx xxxxxx.
                "(x.xxxxxxxxxxxx.xxx != "
                '"xxx:xxx:xxx::xxxxxxxxxxxx:xxxxxxx-xxxx/xxxxxxxxxxx/xxxxxxxxxxxxxxxxx") && '
            )
        )
    ]
)
$ black -v xxx.py 
Using configuration from /Users/jelle/py/black/pyproject.toml.
Traceback (most recent call last):
  File "/Users/jelle/py/black/src/black/__init__.py", line 648, in reformat_one
    if changed is not Changed.CACHED and format_file_in_place(
  File "/Users/jelle/py/black/src/black/__init__.py", line 791, in format_file_in_place
    dst_contents = format_file_contents(src_contents, fast=fast, mode=mode)
  File "/Users/jelle/py/black/src/black/__init__.py", line 918, in format_file_contents
    assert_stable(src_contents, dst_contents, mode=mode)
  File "/Users/jelle/py/black/src/black/__init__.py", line 6076, in assert_stable
    newdst = format_str(dst, mode=mode)
  File "/Users/jelle/py/black/src/black/__init__.py", line 977, in format_str
    for line in transform_line(
  File "/Users/jelle/py/black/src/black/__init__.py", line 2731, in transform_line
    result.extend(
  File "/Users/jelle/py/black/src/black/__init__.py", line 2725, in transform_line
    for transformed_line in transform(line, features):
  File "/Users/jelle/py/black/src/black/__init__.py", line 2834, in __call__
    for line_result in self.do_transform(line, string_idx):
  File "/Users/jelle/py/black/src/black/__init__.py", line 2978, in do_transform
    msg_result = self.__merge_string_group(new_line, string_idx)
  File "/Users/jelle/py/black/src/black/__init__.py", line 3164, in __merge_string_group
    new_line.append(string_leaf)
  File "/Users/jelle/py/black/src/black/__init__.py", line 1421, in append
    leaf.prefix += whitespace(
  File "/Users/jelle/py/black/src/black/__init__.py", line 2152, in whitespace
    assert p is not None, f"INTERNAL ERROR: hand-made leaf without parent: {leaf!r}"
AssertionError: INTERNAL ERROR: hand-made leaf without parent: Leaf(STRING, '\'(x.xxxxxxxxxxxx.xxx != "xxx:xxx:xxx::xxxxxxxxxxxx:xxxxxxx-xxxx/xxxxxxxxxxx/xxxxxxxxxxxxxxxxx") && \'')
error: cannot format xxx.py: INTERNAL ERROR: hand-made leaf without parent: Leaf(STRING, '\'(x.xxxxxxxxxxxx.xxx != "xxx:xxx:xxx::xxxxxxxxxxxx:xxxxxxx-xxxx/xxxxxxxxxxx/xxxxxxxxxxxxxxxxx") && \'')
Oh no! 💥 💔 💥
1 file failed to reformat.
```


Discussion:
`git bisect` also points to 544ea9c217cad9459d7b60665db787e94b52f93d as the first bad commit, from PR #1132 "Improve String Handling".
Now fine on `master`

```console
$ black --check xxx.py
All done! ✨ 🍰 ✨
1 file would be left unchanged.
```
Yes, #1609 disables the feature that caused the crash. I left the issue open because we need to fix it in order to get rid of the experimental flag.

Resolving patch:
diff --git a/src/black/__init__.py b/src/black/__init__.py
--- a/src/black/__init__.py
+++ b/src/black/__init__.py
@@ -2889,11 +2889,8 @@ class StringMerger(CustomSplitMapMixin, StringTransformer):
     """StringTransformer that merges strings together.
 
     Requirements:
-        (A) The line contains adjacent strings such that at most one substring
-        has inline comments AND none of those inline comments are pragmas AND
-        the set of all substring prefixes is either of length 1 or equal to
-        {"", "f"} AND none of the substrings are raw strings (i.e. are prefixed
-        with 'r').
+        (A) The line contains adjacent strings such that ALL of the validation checks
+        listed in StringMerger.__validate_msg(...)'s docstring pass.
             OR
         (B) The line contains a string which uses line continuation backslashes.
 
@@ -3142,6 +3139,7 @@ def __validate_msg(line: Line, string_idx: int) -> TResult[None]:
             * Ok(None), if ALL validation checks (listed below) pass.
                 OR
             * Err(CannotTransform), if any of the following are true:
+                - The target string group does not contain ANY stand-alone comments.
                 - The target string is not in a string group (i.e. it has no
                   adjacent strings).
                 - The string group has more than one inline comment.
@@ -3150,6 +3148,26 @@ def __validate_msg(line: Line, string_idx: int) -> TResult[None]:
                   length greater than one and is not equal to {"", "f"}.
                 - The string group consists of raw strings.
         """
+        # We first check for "inner" stand-alone comments (i.e. stand-alone
+        # comments that have a string leaf before them AND after them).
+        for inc in [1, -1]:
+            i = string_idx
+            found_sa_comment = False
+            is_valid_index = is_valid_index_factory(line.leaves)
+            while is_valid_index(i) and line.leaves[i].type in [
+                token.STRING,
+                STANDALONE_COMMENT,
+            ]:
+                if line.leaves[i].type == STANDALONE_COMMENT:
+                    found_sa_comment = True
+                elif found_sa_comment:
+                    return TErr(
+                        "StringMerger does NOT merge string groups which contain "
+                        "stand-alone comments."
+                    )
+
+                i += inc
+
         num_of_inline_string_comments = 0
         set_of_prefixes = set()
         num_of_strings = 0


Test patch:
diff --git a/tests/data/long_strings__regression.py b/tests/data/long_strings__regression.py
--- a/tests/data/long_strings__regression.py
+++ b/tests/data/long_strings__regression.py
@@ -310,6 +310,19 @@ def who(self):
             passenger_association=passenger_association,
         )
 
+xxxxxxx_xxxxxx_xxxxxxx = xxx(
+    [
+        xxxxxxxxxxxx(
+            xxxxxx_xxxxxxx=(
+                '((x.aaaaaaaaa = "xxxxxx.xxxxxxxxxxxxxxxxxxxxx") || (x.xxxxxxxxx = "xxxxxxxxxxxx")) && '
+                # xxxxx xxxxxxxxxxxx xxxx xxx (xxxxxxxxxxxxxxxx) xx x xxxxxxxxx xx xxxxxx.
+                "(x.bbbbbbbbbbbb.xxx != "
+                '"xxx:xxx:xxx::cccccccccccc:xxxxxxx-xxxx/xxxxxxxxxxx/xxxxxxxxxxxxxxxxx") && '
+            )
+        )
+    ]
+)
+
 if __name__ == "__main__":
     for i in range(4, 8):
         cmd = (
@@ -709,6 +722,20 @@ def who(self):
         )
 
 
+xxxxxxx_xxxxxx_xxxxxxx = xxx(
+    [
+        xxxxxxxxxxxx(
+            xxxxxx_xxxxxxx=(
+                '((x.aaaaaaaaa = "xxxxxx.xxxxxxxxxxxxxxxxxxxxx") || (x.xxxxxxxxx ='
+                ' "xxxxxxxxxxxx")) && '
+                # xxxxx xxxxxxxxxxxx xxxx xxx (xxxxxxxxxxxxxxxx) xx x xxxxxxxxx xx xxxxxx.
+                "(x.bbbbbbbbbbbb.xxx != "
+                '"xxx:xxx:xxx::cccccccccccc:xxxxxxx-xxxx/xxxxxxxxxxx/xxxxxxxxxxxxxxxxx") && '
+            )
+        )
+    ]
+)
+
 if __name__ == "__main__":
     for i in range(4, 8):
         cmd = (