Repository: psf/black

Issue:
Crash with line-length 6 and multi-line docstring
```python
def x():
    """first line----------------------------------------------------------------------

    second line----------------------------------------------------------------------"""
```
Crashes with Black 22.12.0:
```
% black --preview --diff tests/data/preview/docstring_preview.py -l 6
error: cannot format tests/data/preview/docstring_preview.py: INTERNAL ERROR: Black produced different code on the second pass of the formatter.  Please report a bug on https://github.com/psf/black/issues.  This diff might be helpful: /var/folders/3b/k964cnv10097wzj3h7w07fww0000gp/T/blk_la0pkiyv.log

Oh no! 💥 💔 💥
1 file would fail to reformat.
% cat /var/folders/3b/k964cnv10097wzj3h7w07fww0000gp/T/blk_la0pkiyv.log

Mode(target_versions={<TargetVersion.PY38: 8>, <TargetVersion.PY37: 7>}, line_length=6, string_normalization=True, is_pyi=False, is_ipynb=False, skip_source_first_line=False, magic_trailing_comma=True, experimental_string_processing=False, python_cell_magics=set(), preview=True)
--- source
+++ first pass
@@ -1,4 +1,6 @@
 def x():
     """first line----------------------------------------------------------------------
 
-    second line----------------------------------------------------------------------"""
+    second line----------------------------------------------------------------------
+    
+    """
--- first pass
+++ second pass
@@ -1,6 +1,7 @@
 def x():
     """first line----------------------------------------------------------------------
 
     second line----------------------------------------------------------------------
+
     
     """
```

I guess this stops crashing at line-length 7 because then `    """` (one level of indent plus trailing quotes) fits in a line.


Discussion:
Actually doesn't need long lines, this crashes in the same way with `--preview -l 6`:
```python
def f():
    """
    x
    """
```
This is fixed in 23.1a1, probably by https://github.com/psf/black/pull/3418/commits/d48f8ed854d585ca0fac73c4785bdc93a81471ad (which I should put in a separate PR).

Resolving patch:
diff --git a/src/black/linegen.py b/src/black/linegen.py
--- a/src/black/linegen.py
+++ b/src/black/linegen.py
@@ -401,6 +401,7 @@ def visit_STRING(self, leaf: Leaf) -> Iterator[Line]:
             else:
                 docstring = docstring.strip()
 
+            has_trailing_backslash = False
             if docstring:
                 # Add some padding if the docstring starts / ends with a quote mark.
                 if docstring[0] == quote_char:
@@ -413,6 +414,7 @@ def visit_STRING(self, leaf: Leaf) -> Iterator[Line]:
                         # Odd number of tailing backslashes, add some padding to
                         # avoid escaping the closing string quote.
                         docstring += " "
+                        has_trailing_backslash = True
             elif not docstring_started_empty:
                 docstring = " "
 
@@ -435,6 +437,8 @@ def visit_STRING(self, leaf: Leaf) -> Iterator[Line]:
                 if (
                     len(lines) > 1
                     and last_line_length + quote_len > self.mode.line_length
+                    and len(indent) + quote_len <= self.mode.line_length
+                    and not has_trailing_backslash
                 ):
                     leaf.value = prefix + quote + docstring + "\n" + indent + quote
                 else:


Test patch:
diff --git a/tests/data/miscellaneous/linelength6.py b/tests/data/miscellaneous/linelength6.py
new file mode 100644
--- /dev/null
+++ b/tests/data/miscellaneous/linelength6.py
@@ -0,0 +1,5 @@
+# Regression test for #3427, which reproes only with line length <= 6
+def f():
+    """
+    x
+    """
diff --git a/tests/data/simple_cases/docstring.py b/tests/data/simple_cases/docstring.py
--- a/tests/data/simple_cases/docstring.py
+++ b/tests/data/simple_cases/docstring.py
@@ -173,6 +173,11 @@ def multiline_backslash_2():
   '''
   hey there \ '''
 
+# Regression test for #3425
+def multiline_backslash_really_long_dont_crash():
+    """
+    hey there hello guten tag hi hoow are you ola zdravstvuyte ciao como estas ca va \ """
+
 
 def multiline_backslash_3():
   '''
@@ -391,6 +396,12 @@ def multiline_backslash_2():
     hey there \ """
 
 
+# Regression test for #3425
+def multiline_backslash_really_long_dont_crash():
+    """
+    hey there hello guten tag hi hoow are you ola zdravstvuyte ciao como estas ca va \ """
+
+
 def multiline_backslash_3():
     """
     already escaped \\"""
diff --git a/tests/test_format.py b/tests/test_format.py
--- a/tests/test_format.py
+++ b/tests/test_format.py
@@ -146,6 +146,13 @@ def test_docstring_no_string_normalization() -> None:
     assert_format(source, expected, mode)
 
 
+def test_docstring_line_length_6() -> None:
+    """Like test_docstring but with line length set to 6."""
+    source, expected = read_data("miscellaneous", "linelength6")
+    mode = black.Mode(line_length=6)
+    assert_format(source, expected, mode)
+
+
 def test_preview_docstring_no_string_normalization() -> None:
     """
     Like test_docstring but with string normalization off *and* the preview style