Repository: psf/black

Issue:
Parser fails due to empty body `...` on same line as definition. Discrepancy in whole file and `--line-ranges` parsing
**Describe the bug**

Originally I realized that in VSCode the black formatter does not format my code anymore, i.e. the `--line-ranges` is broken and raises a parser error.

**Observation: Difference between `black file.py` and `black --line-range #-# file.py`**

If there is a `def foo(self): ...` with `...` on the same line `black` fails to format anything after when used with `--line-range`.

**To Reproduce**

<!--
Minimal steps to reproduce the behavior with source code and Black's configuration.
-->

For example, take this code:

```python
class Foo:
    
    @overload
    def foo(self, *, a: int) -> None: ...
    
    def fox(self):
        print('This line is', 'way',  'way',  'way',  'way', 'too long to fit in a single line, so it is split into multiple lines for better readability.')

```

And run it with these arguments:

**Fails:**
```shell
$ black -c --line-ranges 6-7 "class Foo:
    
    @overload
    def foo(self, *, a: int) -> None: ...
    
    def fox(self):
        print('This line is', 'way',  'way',  'way',  'way', 'too long to fit in a single line, so it is split into multiple lines for better readability.')
"
error: cannot format <string>: Cannot parse for target version Python 3.11: 3:17:     @overload    def foo(self, *, a: int) -> None: ..
```

**Formatting only the `...` line changes nothing**
```shell
$ black -c --line-ranges 1-5 "class Foo:
    
    @overload
    def foo(self, *, a: int) -> None: ...
    
    def fox(self):
        print('This line is', 'way',  'way',  'way',  'way', 'too long to fit in a single line, so it is split into multiple lines for better readability.')
"

# Trimmed output
    @overload
    def foo(self, *, a: int) -> None: ...
```

**Works**:
- Either formatting everything without `--line-ranges` works correctly and does change the long line; also including the seemingly "broken" line seems to work fine.
- First putting `...` on the next line and then format it is fine:

```shell
$black -c "class Foo:
    
    @overload
    def foo(self, *, a: int) -> None:
        ...
    
    def fox(self):
        print('This line is', 'way',  'way',  'way',  'way', 'too long to fit in a single line, so it is split into multiple lines for better readability.')
" --line-ranges 7-9
```

In the end, if I format everything `...` is again put on the same line and `--line-ranges` breaks for further editing.

**Expected behavior**

Parser should not be bothered if `...` is on the same line or below anywhere, to have `--line-ranges` work everywhere independent of style used in above code.

**Environment**

Ubuntu 22.04
```
black, 25.1.0 (compiled: no)
Python (CPython) 3.10.16

2025-05-28 09:39:57.081 [info] SUPPORTED black>=22.3.0
FOUND black==25.1.0
```



Discussion:

Patch:
diff --git a/CHANGES.md b/CHANGES.md
index fda755cd3d9..2712e1c6e4f 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -22,6 +22,8 @@
 - Remove support for pre-python 3.7 `await/async` as soft keywords/variable names
   (#4676)
 - Fix crash on parenthesized expression inside a type parameter bound (#4684)
+- Fix crash when using line ranges excluding indented single line decorated items
+  (#4670)
 
 ### Preview style
 
diff --git a/src/black/ranges.py b/src/black/ranges.py
index 92e1c93aaf7..26407cc7cfd 100644
--- a/src/black/ranges.py
+++ b/src/black/ranges.py
@@ -330,6 +330,19 @@ def _convert_node_to_standalone_comment(node: LN) -> None:
     first.prefix = ""
     index = node.remove()
     if index is not None:
+        # Because of the special handling of multiple decorators, if the decorated
+        # item is a single line then there will be a missing newline between the
+        # decorator and item, so add it back. This doesn't affect any other case
+        # since a decorated item with a newline would hit the earlier suite case
+        # in _convert_unchanged_line_by_line that correctly handles the newlines.
+        if node.type == syms.decorated:
+            # A leaf of type decorated wouldn't make sense, since it should always
+            # have at least the decorator + the decorated item, so if this assert
+            # hits that means there's a problem in the parser.
+            assert isinstance(node, Node)
+            # 1 will always be the correct index since before this function is
+            # called all the decorators are collapsed into a single leaf
+            node.insert_child(1, Leaf(NEWLINE, "\n"))
         # Remove the '\n', as STANDALONE_COMMENT will have '\n' appended when
         # generating the formatted code.
         value = str(node)[:-1]


Test patch:
diff --git a/tests/data/cases/line_ranges_decorator_edge_case.py b/tests/data/cases/line_ranges_decorator_edge_case.py
new file mode 100644
index 00000000000..483fbe8c57d
--- /dev/null
+++ b/tests/data/cases/line_ranges_decorator_edge_case.py
@@ -0,0 +1,8 @@
+# flags: --line-ranges=6-7
+class Foo:
+
+    @overload
+    def foo(): ...
+
+    def fox(self):
+        print()