Repository: psf/black

Issue:
Black --preview (ESP) crashes on an implicitly concatenated f-string
<!--
Please make sure that the bug is not already fixed either in newer versions or the
current development version. To confirm this, you have three options:

1. Update Black's version if a newer release exists: `pip install -U black`
2. Use the online formatter at <https://black.vercel.app/?version=main>, which will use
   the latest main branch.
3. Or run _Black_ on your machine:
   - create a new virtualenv (make sure it's the same Python version);
   - clone this repository;
   - run `pip install -e .[d]`;
   - run `pip install -r test_requirements.txt`
   - make sure it's sane by running `python -m pytest`; and
   - run `black` like you did last time.
-->

**Describe the bug**

<!-- A clear and concise description of what the bug is. -->

**To Reproduce**

<!--
Minimal steps to reproduce the behavior with source code and Black's configuration.
-->

For example, take this code:

```python
xxxx(
    xxxxxxxxxxxxx=''
    f'xxxxxxx {xxxxxxxxxx} xxx xxxxxxxxxx xxxxx xxx xxx xx '
    'xxxxx xxxxxxxxx xxxxxxx, xxx xxxxxxxxxxx xxx xxxxx. '
    f'xxxxxxxxxxxxx xxxx xx xxxxxxxxxx. xxxxx: {x.xxx}')
```

And run it with these arguments:

```sh
$ black --preview t.py
```

The resulting error is:

```
error: cannot format /Users/yileiyang/Downloads/t.py: 'f"' is missing a starting quote character (' or ").

Oh no! 💥 💔 💥
1 file would fail to reformat.
```

**Expected behavior**

<!-- A clear and concise description of what you expected to happen. -->
No crash

**Environment**

<!-- Please complete the following information: -->

- Black's version: this bug existed since v22.1.0 from ESP

**Additional context**

<!-- Add any other context about the problem here. -->

Traceback:

```
Traceback (most recent call last):
  File "/Users/yileiyang/github.com/yilei/black/src/black/__init__.py", line 1078, in format_str
    dst_contents = _format_str_once(src_contents, mode=mode)
  File "/Users/yileiyang/github.com/yilei/black/src/black/__init__.py", line 1110, in _format_str_once
    for line in transform_line(
  File "/Users/yileiyang/github.com/yilei/black/src/black/linegen.py", line 577, in transform_line
    result = run_transformer(line, transform, mode, features, line_str=line_str)
  File "/Users/yileiyang/github.com/yilei/black/src/black/linegen.py", line 1420, in run_transformer
    result.extend(transform_line(transformed_line, mode=mode, features=features))
  File "/Users/yileiyang/github.com/yilei/black/src/black/linegen.py", line 577, in transform_line
    result = run_transformer(line, transform, mode, features, line_str=line_str)
  File "/Users/yileiyang/github.com/yilei/black/src/black/linegen.py", line 1420, in run_transformer
    result.extend(transform_line(transformed_line, mode=mode, features=features))
  File "/Users/yileiyang/github.com/yilei/black/src/black/linegen.py", line 577, in transform_line
    result = run_transformer(line, transform, mode, features, line_str=line_str)
  File "/Users/yileiyang/github.com/yilei/black/src/black/linegen.py", line 1420, in run_transformer
    result.extend(transform_line(transformed_line, mode=mode, features=features))
  File "/Users/yileiyang/github.com/yilei/black/src/black/linegen.py", line 577, in transform_line
    result = run_transformer(line, transform, mode, features, line_str=line_str)
  File "/Users/yileiyang/github.com/yilei/black/src/black/linegen.py", line 1416, in run_transformer
    for transformed_line in transform(line, features):
  File "/Users/yileiyang/github.com/yilei/black/src/black/trans.py", line 251, in __call__
    for line_result in self.do_transform(line, string_idx):
  File "/Users/yileiyang/github.com/yilei/black/src/black/trans.py", line 1362, in do_transform
    next_value != self._normalize_f_string(next_value, prefix)
  File "/Users/yileiyang/github.com/yilei/black/src/black/trans.py", line 1592, in _normalize_f_string
    assert_is_leaf_string(string)
  File "/Users/yileiyang/github.com/yilei/black/src/black/strings.py", line 129, in assert_is_leaf_string
    assert (
AssertionError: 'f"' is missing a starting quote character (' or ").
```


Discussion:

Resolving patch:
diff --git a/src/black/trans.py b/src/black/trans.py
--- a/src/black/trans.py
+++ b/src/black/trans.py
@@ -1359,9 +1359,14 @@ def more_splits_should_be_made() -> bool:
             # prefix, and the current custom split did NOT originally use a
             # prefix...
             if (
-                next_value != self._normalize_f_string(next_value, prefix)
-                and use_custom_breakpoints
+                use_custom_breakpoints
                 and not csplit.has_prefix
+                and (
+                    # `next_value == prefix + QUOTE` happens when the custom
+                    # split is an empty string.
+                    next_value == prefix + QUOTE
+                    or next_value != self._normalize_f_string(next_value, prefix)
+                )
             ):
                 # Then `csplit.break_idx` will be off by one after removing
                 # the 'f' prefix.


Test patch:
diff --git a/tests/data/preview/long_strings__regression.py b/tests/data/preview/long_strings__regression.py
--- a/tests/data/preview/long_strings__regression.py
+++ b/tests/data/preview/long_strings__regression.py
@@ -531,6 +531,18 @@ async def foo(self):
         r"signiferumque, duo ea vocibus consetetur scriptorem. Facer \t",
 }
 
+# Regression test for https://github.com/psf/black/issues/3459.
+xxxx(
+    empty_str_as_first_split=''
+    f'xxxxxxx {xxxxxxxxxx} xxx xxxxxxxxxx xxxxx xxx xxx xx '
+    'xxxxx xxxxxxxxx xxxxxxx, xxx xxxxxxxxxxx xxx xxxxx. '
+    f'xxxxxxxxxxxxx xxxx xx xxxxxxxxxx. xxxxx: {x.xxx}',
+    empty_u_str_as_first_split=u''
+    f'xxxxxxx {xxxxxxxxxx} xxx xxxxxxxxxx xxxxx xxx xxx xx '
+    'xxxxx xxxxxxxxx xxxxxxx, xxx xxxxxxxxxxx xxx xxxxx. '
+    f'xxxxxxxxxxxxx xxxx xx xxxxxxxxxx. xxxxx: {x.xxx}',
+)
+
 
 # output
 
@@ -1193,3 +1205,19 @@ async def foo(self):
         r"signiferumque, duo ea vocibus consetetur scriptorem. Facer \t"
     ),
 }
+
+# Regression test for https://github.com/psf/black/issues/3459.
+xxxx(
+    empty_str_as_first_split=(
+        ""
+        f"xxxxxxx {xxxxxxxxxx} xxx xxxxxxxxxx xxxxx xxx xxx xx "
+        "xxxxx xxxxxxxxx xxxxxxx, xxx xxxxxxxxxxx xxx xxxxx. "
+        f"xxxxxxxxxxxxx xxxx xx xxxxxxxxxx. xxxxx: {x.xxx}"
+    ),
+    empty_u_str_as_first_split=(
+        ""
+        f"xxxxxxx {xxxxxxxxxx} xxx xxxxxxxxxx xxxxx xxx xxx xx "
+        "xxxxx xxxxxxxxx xxxxxxx, xxx xxxxxxxxxxx xxx xxxxx. "
+        f"xxxxxxxxxxxxx xxxx xx xxxxxxxxxx. xxxxx: {x.xxx}"
+    ),
+)