From ae26fa020c0a1fc90f45281fd2b2ebbd86d6c647 Mon Sep 17 00:00:00 2001 From: Thomas Mattone <43917226+Luunynliny@users.noreply.github.com> Date: Tue, 29 Jul 2025 21:06:04 +0200 Subject: [PATCH] [`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 ``` --- .../test/fixtures/flake8_pyi/PYI013.py | 5 ++ .../test/fixtures/flake8_pyi/PYI013.pyi | 4 ++ .../rules/ellipsis_in_non_empty_class_body.rs | 15 ++-- ...__flake8_pyi__tests__PYI013_PYI013.py.snap | 19 +++++ ..._flake8_pyi__tests__PYI013_PYI013.pyi.snap | 70 +++++++++++++------ 5 files changed, 88 insertions(+), 25 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI013.py b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI013.py index 9b3635962e..0b21f59453 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI013.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI013.py @@ -39,6 +39,11 @@ class NonEmptyWithInit: pass +class NonEmptyChildWithInlineComment: + value: int + ... # preserve me + + class EmptyClass: ... diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI013.pyi b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI013.pyi index aaf2cb0f79..3386c2f0a4 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI013.pyi +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI013.pyi @@ -38,6 +38,10 @@ class NonEmptyWithInit: def __init__(): pass +class NonEmptyChildWithInlineComment: + value: int + ... # preserve me + # Not violations class EmptyClass: ... diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/ellipsis_in_non_empty_class_body.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/ellipsis_in_non_empty_class_body.rs index aef6f5639b..4c1497fe93 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/ellipsis_in_non_empty_class_body.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/ellipsis_in_non_empty_class_body.rs @@ -1,10 +1,11 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::whitespace::trailing_comment_start_offset; use ruff_python_ast::{Stmt, StmtExpr}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::fix; -use crate::{Fix, FixAvailability, Violation}; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// 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 { - let Stmt::Expr(StmtExpr { value, .. }) = &stmt else { + let Stmt::Expr(StmtExpr { value, .. }) = stmt else { continue; }; if value.is_ellipsis_literal_expr() { let mut diagnostic = 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( checker.semantic().current_statement_id(), ))); diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI013_PYI013.py.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI013_PYI013.py.snap index d76266722e..67c3e98e37 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI013_PYI013.py.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI013_PYI013.py.snap @@ -145,3 +145,22 @@ PYI013.py:36:5: PYI013 [*] Non-empty class body must not contain `...` 37 36 | 38 37 | def __init__(): 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: diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI013_PYI013.pyi.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI013_PYI013.pyi.snap index 3fc5a71eb4..b897ba3d20 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI013_PYI013.pyi.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI013_PYI013.pyi.snap @@ -17,9 +17,10 @@ PYI013.pyi:5:5: PYI013 [*] Non-empty class body must not contain `...` 3 3 | class OneAttributeClass: 4 4 | value: int 5 |- ... # Error -6 5 | -7 6 | class OneAttributeClass2: -8 7 | ... # Error + 5 |+ # Error +6 6 | +7 7 | class OneAttributeClass2: +8 8 | ... # Error 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 | 7 7 | class OneAttributeClass2: 8 |- ... # Error -9 8 | value: int -10 9 | -11 10 | class MyClass: + 8 |+ # Error +9 9 | value: int +10 10 | +11 11 | class MyClass: 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: 16 16 | ... 17 |- ... # Error -18 17 | -19 18 | class DocstringClass: -20 19 | """ + 17 |+ # Error +18 18 | +19 19 | class DocstringClass: +20 20 | """ 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 | """ 23 23 | 24 |- ... # Error -25 24 | -26 25 | class NonEmptyChild(Exception): -27 26 | value: int + 24 |+ # Error +25 25 | +26 26 | class NonEmptyChild(Exception): +27 27 | value: int 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): 27 27 | value: int 28 |- ... # Error -29 28 | -30 29 | class NonEmptyChild2(Exception): -31 30 | ... # Error + 28 |+ # Error +29 29 | +30 30 | class NonEmptyChild2(Exception): +31 31 | ... # Error 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 | 30 30 | class NonEmptyChild2(Exception): 31 |- ... # Error -32 31 | value: int -33 32 | -34 33 | class NonEmptyWithInit: + 31 |+ # Error +32 32 | value: int +33 33 | +34 34 | class NonEmptyWithInit: 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: 35 35 | value: int 36 |- ... # Error -37 36 | -38 37 | def __init__(): -39 38 | pass + 36 |+ # Error +37 37 | +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 |