From 5fcceae3a865b893462f5ba596d26b0daa987a12 Mon Sep 17 00:00:00 2001 From: Thomas Mattone Date: Thu, 17 Jul 2025 14:17:05 +0200 Subject: [PATCH 1/6] fix: preserve inline comment --- .../rules/ellipsis_in_non_empty_class_body.rs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) 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 aef6f5639bb490..4389591d2f21bb 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,25 @@ 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()) + { + // Only delete up to the comment start + Edit::range_deletion(stmt.range().add_end(index)) + } else { + // Default full-statement deletion + 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(), ))); From 35e1ed16ceac3f867bce8f72c0e49ac6eb8e34a0 Mon Sep 17 00:00:00 2001 From: Thomas Mattone Date: Thu, 17 Jul 2025 15:28:19 +0200 Subject: [PATCH 2/6] test: ensure inline comment is preserved --- .../test/fixtures/flake8_pyi/PYI013.py | 5 ++ .../test/fixtures/flake8_pyi/PYI013.pyi | 18 +++-- ...__flake8_pyi__tests__PYI013_PYI013.py.snap | 19 ++++++ ..._flake8_pyi__tests__PYI013_PYI013.pyi.snap | 65 ++++++++++++------- 4 files changed, 78 insertions(+), 29 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 9b3635962e29c2..0b21f59453673a 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 @@ def __init__(): 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 aaf2cb0f794f2a..95847f63da989c 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI013.pyi +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI013.pyi @@ -2,10 +2,10 @@ class OneAttributeClass: value: int - ... # Error + ... class OneAttributeClass2: - ... # Error + ... value: int class MyClass: @@ -14,30 +14,34 @@ class MyClass: class TwoEllipsesClass: ... - ... # Error + ... class DocstringClass: """ My body only contains an ellipsis. """ - ... # Error + ... class NonEmptyChild(Exception): value: int - ... # Error + ... class NonEmptyChild2(Exception): - ... # Error + ... value: int class NonEmptyWithInit: value: int - ... # Error + ... def __init__(): pass +class NonEmptyChildWithInlineComment: + value: int + ... # preserve me + # Not violations class EmptyClass: ... 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 d76266722eb85c..67c3e98e372ac0 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 3fc5a71eb42f3d..a22fd6acf44f61 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 @@ -5,7 +5,7 @@ PYI013.pyi:5:5: PYI013 [*] Non-empty class body must not contain `...` | 3 | class OneAttributeClass: 4 | value: int -5 | ... # Error +5 | ... | ^^^ PYI013 6 | 7 | class OneAttributeClass2: @@ -16,25 +16,25 @@ PYI013.pyi:5:5: PYI013 [*] Non-empty class body must not contain `...` 2 2 | 3 3 | class OneAttributeClass: 4 4 | value: int -5 |- ... # Error +5 |- ... 6 5 | 7 6 | class OneAttributeClass2: -8 7 | ... # Error +8 7 | ... PYI013.pyi:8:5: PYI013 [*] Non-empty class body must not contain `...` | 7 | class OneAttributeClass2: -8 | ... # Error +8 | ... | ^^^ PYI013 9 | value: int | = help: Remove unnecessary `...` ℹ Safe fix -5 5 | ... # Error +5 5 | ... 6 6 | 7 7 | class OneAttributeClass2: -8 |- ... # Error +8 |- ... 9 8 | value: int 10 9 | 11 10 | class MyClass: @@ -62,24 +62,24 @@ PYI013.pyi:16:5: PYI013 [*] Non-empty class body must not contain `...` 15 | class TwoEllipsesClass: 16 | ... | ^^^ PYI013 -17 | ... # Error +17 | ... | = help: Remove unnecessary `...` ℹ Safe fix -13 13 | value: int 14 14 | 15 15 | class TwoEllipsesClass: -16 |- ... -17 16 | ... # Error +16 16 | ... +17 |- ... 18 17 | 19 18 | class DocstringClass: +20 19 | """ PYI013.pyi:17:5: PYI013 [*] Non-empty class body must not contain `...` | 15 | class TwoEllipsesClass: 16 | ... -17 | ... # Error +17 | ... | ^^^ PYI013 18 | 19 | class DocstringClass: @@ -90,7 +90,7 @@ PYI013.pyi:17:5: PYI013 [*] Non-empty class body must not contain `...` 14 14 | 15 15 | class TwoEllipsesClass: 16 16 | ... -17 |- ... # Error +17 |- ... 18 17 | 19 18 | class DocstringClass: 20 19 | """ @@ -99,7 +99,7 @@ PYI013.pyi:24:5: PYI013 [*] Non-empty class body must not contain `...` | 22 | """ 23 | -24 | ... # Error +24 | ... | ^^^ PYI013 25 | 26 | class NonEmptyChild(Exception): @@ -110,7 +110,7 @@ PYI013.pyi:24:5: PYI013 [*] Non-empty class body must not contain `...` 21 21 | My body only contains an ellipsis. 22 22 | """ 23 23 | -24 |- ... # Error +24 |- ... 25 24 | 26 25 | class NonEmptyChild(Exception): 27 26 | value: int @@ -119,7 +119,7 @@ PYI013.pyi:28:5: PYI013 [*] Non-empty class body must not contain `...` | 26 | class NonEmptyChild(Exception): 27 | value: int -28 | ... # Error +28 | ... | ^^^ PYI013 29 | 30 | class NonEmptyChild2(Exception): @@ -130,25 +130,25 @@ PYI013.pyi:28:5: PYI013 [*] Non-empty class body must not contain `...` 25 25 | 26 26 | class NonEmptyChild(Exception): 27 27 | value: int -28 |- ... # Error +28 |- ... 29 28 | 30 29 | class NonEmptyChild2(Exception): -31 30 | ... # Error +31 30 | ... PYI013.pyi:31:5: PYI013 [*] Non-empty class body must not contain `...` | 30 | class NonEmptyChild2(Exception): -31 | ... # Error +31 | ... | ^^^ PYI013 32 | value: int | = help: Remove unnecessary `...` ℹ Safe fix -28 28 | ... # Error +28 28 | ... 29 29 | 30 30 | class NonEmptyChild2(Exception): -31 |- ... # Error +31 |- ... 32 31 | value: int 33 32 | 34 33 | class NonEmptyWithInit: @@ -157,7 +157,7 @@ PYI013.pyi:36:5: PYI013 [*] Non-empty class body must not contain `...` | 34 | class NonEmptyWithInit: 35 | value: int -36 | ... # Error +36 | ... | ^^^ PYI013 37 | 38 | def __init__(): @@ -168,7 +168,28 @@ PYI013.pyi:36:5: PYI013 [*] Non-empty class body must not contain `...` 33 33 | 34 34 | class NonEmptyWithInit: 35 35 | value: int -36 |- ... # Error +36 |- ... 37 36 | 38 37 | def __init__(): 39 38 | 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 | From 4f4e17f6d62e9f6c4001bb1e13af4f9714091d8f Mon Sep 17 00:00:00 2001 From: Thomas Mattone Date: Thu, 17 Jul 2025 15:34:01 +0200 Subject: [PATCH 3/6] refactor: fmt --- .../flake8_pyi/rules/ellipsis_in_non_empty_class_body.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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 4389591d2f21bb..3f3d215a257e3f 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 @@ -60,9 +60,7 @@ pub(crate) fn ellipsis_in_non_empty_class_body(checker: &Checker, body: &[Stmt]) checker.report_diagnostic(EllipsisInNonEmptyClassBody, stmt.range()); // Try to preserve trailing comment if it exists - let edit = if let Some(index) = - trailing_comment_start_offset(stmt, checker.source()) - { + let edit = if let Some(index) = trailing_comment_start_offset(stmt, checker.source()) { // Only delete up to the comment start Edit::range_deletion(stmt.range().add_end(index)) } else { From f55046edb7618032e58c16fd8c831a96e6ab09d9 Mon Sep 17 00:00:00 2001 From: Thomas Mattone Date: Tue, 29 Jul 2025 01:10:03 +0200 Subject: [PATCH 4/6] refactor: remove useless comments --- .../rules/flake8_pyi/rules/ellipsis_in_non_empty_class_body.rs | 2 -- 1 file changed, 2 deletions(-) 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 3f3d215a257e3f..4c1497fe93e60f 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 @@ -61,10 +61,8 @@ pub(crate) fn ellipsis_in_non_empty_class_body(checker: &Checker, body: &[Stmt]) // Try to preserve trailing comment if it exists let edit = if let Some(index) = trailing_comment_start_offset(stmt, checker.source()) { - // Only delete up to the comment start Edit::range_deletion(stmt.range().add_end(index)) } else { - // Default full-statement deletion fix::edits::delete_stmt(stmt, Some(stmt), checker.locator(), checker.indexer()) }; From b68f2db583fe8d894e2f64bf89f0474222a6a0cd Mon Sep 17 00:00:00 2001 From: Thomas Mattone Date: Tue, 29 Jul 2025 01:34:05 +0200 Subject: [PATCH 5/6] refactor: revert error comment removal --- .../resources/test/fixtures/flake8_pyi/PYI013.pyi | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) 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 95847f63da989c..3386c2f0a4ad74 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI013.pyi +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI013.pyi @@ -2,10 +2,10 @@ class OneAttributeClass: value: int - ... + ... # Error class OneAttributeClass2: - ... + ... # Error value: int class MyClass: @@ -14,26 +14,26 @@ class MyClass: class TwoEllipsesClass: ... - ... + ... # Error class DocstringClass: """ My body only contains an ellipsis. """ - ... + ... # Error class NonEmptyChild(Exception): value: int - ... + ... # Error class NonEmptyChild2(Exception): - ... + ... # Error value: int class NonEmptyWithInit: value: int - ... + ... # Error def __init__(): pass From 44e9074c36a68a159d69e170b7d4465cae9f4dac Mon Sep 17 00:00:00 2001 From: Thomas Mattone Date: Tue, 29 Jul 2025 20:37:06 +0200 Subject: [PATCH 6/6] test: revert snapshot comment removal --- ..._flake8_pyi__tests__PYI013_PYI013.pyi.snap | 89 ++++++++++--------- 1 file changed, 48 insertions(+), 41 deletions(-) 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 a22fd6acf44f61..b897ba3d202553 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 @@ -5,7 +5,7 @@ PYI013.pyi:5:5: PYI013 [*] Non-empty class body must not contain `...` | 3 | class OneAttributeClass: 4 | value: int -5 | ... +5 | ... # Error | ^^^ PYI013 6 | 7 | class OneAttributeClass2: @@ -16,28 +16,30 @@ PYI013.pyi:5:5: PYI013 [*] Non-empty class body must not contain `...` 2 2 | 3 3 | class OneAttributeClass: 4 4 | value: int -5 |- ... -6 5 | -7 6 | class OneAttributeClass2: -8 7 | ... +5 |- ... # Error + 5 |+ # Error +6 6 | +7 7 | class OneAttributeClass2: +8 8 | ... # Error PYI013.pyi:8:5: PYI013 [*] Non-empty class body must not contain `...` | 7 | class OneAttributeClass2: -8 | ... +8 | ... # Error | ^^^ PYI013 9 | value: int | = help: Remove unnecessary `...` ℹ Safe fix -5 5 | ... +5 5 | ... # Error 6 6 | 7 7 | class OneAttributeClass2: -8 |- ... -9 8 | value: int -10 9 | -11 10 | class MyClass: +8 |- ... # Error + 8 |+ # Error +9 9 | value: int +10 10 | +11 11 | class MyClass: PYI013.pyi:12:5: PYI013 [*] Non-empty class body must not contain `...` | @@ -62,24 +64,24 @@ PYI013.pyi:16:5: PYI013 [*] Non-empty class body must not contain `...` 15 | class TwoEllipsesClass: 16 | ... | ^^^ PYI013 -17 | ... +17 | ... # Error | = help: Remove unnecessary `...` ℹ Safe fix +13 13 | value: int 14 14 | 15 15 | class TwoEllipsesClass: -16 16 | ... -17 |- ... +16 |- ... +17 16 | ... # Error 18 17 | 19 18 | class DocstringClass: -20 19 | """ PYI013.pyi:17:5: PYI013 [*] Non-empty class body must not contain `...` | 15 | class TwoEllipsesClass: 16 | ... -17 | ... +17 | ... # Error | ^^^ PYI013 18 | 19 | class DocstringClass: @@ -90,16 +92,17 @@ PYI013.pyi:17:5: PYI013 [*] Non-empty class body must not contain `...` 14 14 | 15 15 | class TwoEllipsesClass: 16 16 | ... -17 |- ... -18 17 | -19 18 | class DocstringClass: -20 19 | """ +17 |- ... # Error + 17 |+ # Error +18 18 | +19 19 | class DocstringClass: +20 20 | """ PYI013.pyi:24:5: PYI013 [*] Non-empty class body must not contain `...` | 22 | """ 23 | -24 | ... +24 | ... # Error | ^^^ PYI013 25 | 26 | class NonEmptyChild(Exception): @@ -110,16 +113,17 @@ PYI013.pyi:24:5: PYI013 [*] Non-empty class body must not contain `...` 21 21 | My body only contains an ellipsis. 22 22 | """ 23 23 | -24 |- ... -25 24 | -26 25 | class NonEmptyChild(Exception): -27 26 | value: int +24 |- ... # Error + 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 `...` | 26 | class NonEmptyChild(Exception): 27 | value: int -28 | ... +28 | ... # Error | ^^^ PYI013 29 | 30 | class NonEmptyChild2(Exception): @@ -130,34 +134,36 @@ PYI013.pyi:28:5: PYI013 [*] Non-empty class body must not contain `...` 25 25 | 26 26 | class NonEmptyChild(Exception): 27 27 | value: int -28 |- ... -29 28 | -30 29 | class NonEmptyChild2(Exception): -31 30 | ... +28 |- ... # 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 `...` | 30 | class NonEmptyChild2(Exception): -31 | ... +31 | ... # Error | ^^^ PYI013 32 | value: int | = help: Remove unnecessary `...` ℹ Safe fix -28 28 | ... +28 28 | ... # Error 29 29 | 30 30 | class NonEmptyChild2(Exception): -31 |- ... -32 31 | value: int -33 32 | -34 33 | class NonEmptyWithInit: +31 |- ... # Error + 31 |+ # Error +32 32 | value: int +33 33 | +34 34 | class NonEmptyWithInit: PYI013.pyi:36:5: PYI013 [*] Non-empty class body must not contain `...` | 34 | class NonEmptyWithInit: 35 | value: int -36 | ... +36 | ... # Error | ^^^ PYI013 37 | 38 | def __init__(): @@ -168,10 +174,11 @@ PYI013.pyi:36:5: PYI013 [*] Non-empty class body must not contain `...` 33 33 | 34 34 | class NonEmptyWithInit: 35 35 | value: int -36 |- ... -37 36 | -38 37 | def __init__(): -39 38 | pass +36 |- ... # Error + 36 |+ # Error +37 37 | +38 38 | def __init__(): +39 39 | pass PYI013.pyi:43:5: PYI013 [*] Non-empty class body must not contain `...` |