[flake8-pyi] Preserve inline comment in ellipsis removal (PYI013) (#19399)

## Summary

Fixes #19385.

Based on [unnecessary-placeholder
(PIE790)](https://docs.astral.sh/ruff/rules/unnecessary-placeholder/)
behavior, [ellipsis-in-non-empty-class-body
(PYI013)](https://docs.astral.sh/ruff/rules/ellipsis-in-non-empty-class-body/)
now safely preserve inline comment on ellipsis removal.

## Test Plan

A new test class was added:

```python
class NonEmptyChildWithInlineComment:
    value: int
    ... # preserve me
```

with the following expected fix:

```python
class NonEmptyChildWithInlineComment:
    value: int
    # preserve me
```
This commit is contained in:
Thomas Mattone 2025-07-29 21:06:04 +02:00 committed by GitHub
parent 88a679945c
commit ae26fa020c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 88 additions and 25 deletions

View file

@ -39,6 +39,11 @@ class NonEmptyWithInit:
pass pass
class NonEmptyChildWithInlineComment:
value: int
... # preserve me
class EmptyClass: class EmptyClass:
... ...

View file

@ -38,6 +38,10 @@ class NonEmptyWithInit:
def __init__(): def __init__():
pass pass
class NonEmptyChildWithInlineComment:
value: int
... # preserve me
# Not violations # Not violations
class EmptyClass: ... class EmptyClass: ...

View file

@ -1,10 +1,11 @@
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::whitespace::trailing_comment_start_offset;
use ruff_python_ast::{Stmt, StmtExpr}; use ruff_python_ast::{Stmt, StmtExpr};
use ruff_text_size::Ranged; use ruff_text_size::Ranged;
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
use crate::fix; use crate::fix;
use crate::{Fix, FixAvailability, Violation}; use crate::{Edit, Fix, FixAvailability, Violation};
/// ## What it does /// ## What it does
/// Removes ellipses (`...`) in otherwise non-empty class bodies. /// Removes ellipses (`...`) in otherwise non-empty class bodies.
@ -50,15 +51,21 @@ pub(crate) fn ellipsis_in_non_empty_class_body(checker: &Checker, body: &[Stmt])
} }
for stmt in body { for stmt in body {
let Stmt::Expr(StmtExpr { value, .. }) = &stmt else { let Stmt::Expr(StmtExpr { value, .. }) = stmt else {
continue; continue;
}; };
if value.is_ellipsis_literal_expr() { if value.is_ellipsis_literal_expr() {
let mut diagnostic = let mut diagnostic =
checker.report_diagnostic(EllipsisInNonEmptyClassBody, stmt.range()); checker.report_diagnostic(EllipsisInNonEmptyClassBody, stmt.range());
let edit =
fix::edits::delete_stmt(stmt, Some(stmt), checker.locator(), checker.indexer()); // Try to preserve trailing comment if it exists
let edit = if let Some(index) = trailing_comment_start_offset(stmt, checker.source()) {
Edit::range_deletion(stmt.range().add_end(index))
} else {
fix::edits::delete_stmt(stmt, Some(stmt), checker.locator(), checker.indexer())
};
diagnostic.set_fix(Fix::safe_edit(edit).isolate(Checker::isolation( diagnostic.set_fix(Fix::safe_edit(edit).isolate(Checker::isolation(
checker.semantic().current_statement_id(), checker.semantic().current_statement_id(),
))); )));

View file

@ -145,3 +145,22 @@ PYI013.py:36:5: PYI013 [*] Non-empty class body must not contain `...`
37 36 | 37 36 |
38 37 | def __init__(): 38 37 | def __init__():
39 38 | pass 39 38 | pass
PYI013.py:44:5: PYI013 [*] Non-empty class body must not contain `...`
|
42 | class NonEmptyChildWithInlineComment:
43 | value: int
44 | ... # preserve me
| ^^^ PYI013
|
= help: Remove unnecessary `...`
Safe fix
41 41 |
42 42 | class NonEmptyChildWithInlineComment:
43 43 | value: int
44 |- ... # preserve me
44 |+ # preserve me
45 45 |
46 46 |
47 47 | class EmptyClass:

View file

@ -17,9 +17,10 @@ PYI013.pyi:5:5: PYI013 [*] Non-empty class body must not contain `...`
3 3 | class OneAttributeClass: 3 3 | class OneAttributeClass:
4 4 | value: int 4 4 | value: int
5 |- ... # Error 5 |- ... # Error
6 5 | 5 |+ # Error
7 6 | class OneAttributeClass2: 6 6 |
8 7 | ... # Error 7 7 | class OneAttributeClass2:
8 8 | ... # Error
PYI013.pyi:8:5: PYI013 [*] Non-empty class body must not contain `...` PYI013.pyi:8:5: PYI013 [*] Non-empty class body must not contain `...`
| |
@ -35,9 +36,10 @@ PYI013.pyi:8:5: PYI013 [*] Non-empty class body must not contain `...`
6 6 | 6 6 |
7 7 | class OneAttributeClass2: 7 7 | class OneAttributeClass2:
8 |- ... # Error 8 |- ... # Error
9 8 | value: int 8 |+ # Error
10 9 | 9 9 | value: int
11 10 | class MyClass: 10 10 |
11 11 | class MyClass:
PYI013.pyi:12:5: PYI013 [*] Non-empty class body must not contain `...` PYI013.pyi:12:5: PYI013 [*] Non-empty class body must not contain `...`
| |
@ -91,9 +93,10 @@ PYI013.pyi:17:5: PYI013 [*] Non-empty class body must not contain `...`
15 15 | class TwoEllipsesClass: 15 15 | class TwoEllipsesClass:
16 16 | ... 16 16 | ...
17 |- ... # Error 17 |- ... # Error
18 17 | 17 |+ # Error
19 18 | class DocstringClass: 18 18 |
20 19 | """ 19 19 | class DocstringClass:
20 20 | """
PYI013.pyi:24:5: PYI013 [*] Non-empty class body must not contain `...` PYI013.pyi:24:5: PYI013 [*] Non-empty class body must not contain `...`
| |
@ -111,9 +114,10 @@ PYI013.pyi:24:5: PYI013 [*] Non-empty class body must not contain `...`
22 22 | """ 22 22 | """
23 23 | 23 23 |
24 |- ... # Error 24 |- ... # Error
25 24 | 24 |+ # Error
26 25 | class NonEmptyChild(Exception): 25 25 |
27 26 | value: int 26 26 | class NonEmptyChild(Exception):
27 27 | value: int
PYI013.pyi:28:5: PYI013 [*] Non-empty class body must not contain `...` PYI013.pyi:28:5: PYI013 [*] Non-empty class body must not contain `...`
| |
@ -131,9 +135,10 @@ PYI013.pyi:28:5: PYI013 [*] Non-empty class body must not contain `...`
26 26 | class NonEmptyChild(Exception): 26 26 | class NonEmptyChild(Exception):
27 27 | value: int 27 27 | value: int
28 |- ... # Error 28 |- ... # Error
29 28 | 28 |+ # Error
30 29 | class NonEmptyChild2(Exception): 29 29 |
31 30 | ... # Error 30 30 | class NonEmptyChild2(Exception):
31 31 | ... # Error
PYI013.pyi:31:5: PYI013 [*] Non-empty class body must not contain `...` PYI013.pyi:31:5: PYI013 [*] Non-empty class body must not contain `...`
| |
@ -149,9 +154,10 @@ PYI013.pyi:31:5: PYI013 [*] Non-empty class body must not contain `...`
29 29 | 29 29 |
30 30 | class NonEmptyChild2(Exception): 30 30 | class NonEmptyChild2(Exception):
31 |- ... # Error 31 |- ... # Error
32 31 | value: int 31 |+ # Error
33 32 | 32 32 | value: int
34 33 | class NonEmptyWithInit: 33 33 |
34 34 | class NonEmptyWithInit:
PYI013.pyi:36:5: PYI013 [*] Non-empty class body must not contain `...` PYI013.pyi:36:5: PYI013 [*] Non-empty class body must not contain `...`
| |
@ -169,6 +175,28 @@ PYI013.pyi:36:5: PYI013 [*] Non-empty class body must not contain `...`
34 34 | class NonEmptyWithInit: 34 34 | class NonEmptyWithInit:
35 35 | value: int 35 35 | value: int
36 |- ... # Error 36 |- ... # Error
37 36 | 36 |+ # Error
38 37 | def __init__(): 37 37 |
39 38 | pass 38 38 | def __init__():
39 39 | pass
PYI013.pyi:43:5: PYI013 [*] Non-empty class body must not contain `...`
|
41 | class NonEmptyChildWithInlineComment:
42 | value: int
43 | ... # preserve me
| ^^^ PYI013
44 |
45 | # Not violations
|
= help: Remove unnecessary `...`
Safe fix
40 40 |
41 41 | class NonEmptyChildWithInlineComment:
42 42 | value: int
43 |- ... # preserve me
43 |+ # preserve me
44 44 |
45 45 | # Not violations
46 46 |