diff --git a/crates/ruff/resources/test/fixtures/ruff/RUF010.py b/crates/ruff/resources/test/fixtures/ruff/RUF010.py new file mode 100644 index 0000000000000..de268b2cf432b --- /dev/null +++ b/crates/ruff/resources/test/fixtures/ruff/RUF010.py @@ -0,0 +1,19 @@ +# Multi-line comment +# with a blank line +# +# is valid + +print("Hello, world!") # + + +def func_1(): + # This is a valid comment + print("Hello, world!") # + + +def func_2(): + # Indented multi-line comment + # with a blank line + # + # is valid + print("Hello, world!") diff --git a/crates/ruff/src/checkers/physical_lines.rs b/crates/ruff/src/checkers/physical_lines.rs index b4437cad5208f..389343c7cff79 100644 --- a/crates/ruff/src/checkers/physical_lines.rs +++ b/crates/ruff/src/checkers/physical_lines.rs @@ -19,6 +19,7 @@ use crate::rules::pycodestyle::rules::{ use crate::rules::pygrep_hooks::rules::{blanket_noqa, blanket_type_ignore}; use crate::rules::pylint; use crate::rules::pyupgrade::rules::unnecessary_coding_comment; +use crate::rules::ruff::rules::blank_comment; use crate::settings::{flags, Settings}; pub fn check_physical_lines( @@ -34,6 +35,7 @@ pub fn check_physical_lines( let mut has_any_shebang = false; let enforce_blanket_noqa = settings.rules.enabled(Rule::BlanketNOQA); + let enforce_blank_comment = settings.rules.enabled(Rule::BlankComment); let enforce_shebang_not_executable = settings.rules.enabled(Rule::ShebangNotExecutable); let enforce_shebang_missing = settings.rules.enabled(Rule::ShebangMissingExecutableFile); let enforce_shebang_whitespace = settings.rules.enabled(Rule::ShebangLeadingWhitespace); @@ -83,6 +85,10 @@ pub fn check_physical_lines( blanket_noqa(&mut diagnostics, &line); } + if enforce_blank_comment { + blank_comment(&mut diagnostics, &line, settings, autofix); + } + if enforce_shebang_missing || enforce_shebang_not_executable || enforce_shebang_whitespace diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index bbb33c41d45cd..791c7df3912cb 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -720,6 +720,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option { (Ruff, "007") => Rule::PairwiseOverZipped, (Ruff, "008") => Rule::MutableDataclassDefault, (Ruff, "009") => Rule::FunctionCallInDataclassDefaultArgument, + (Ruff, "010") => Rule::BlankComment, (Ruff, "100") => Rule::UnusedNOQA, // flake8-django diff --git a/crates/ruff/src/registry.rs b/crates/ruff/src/registry.rs index 22eabf0636658..164d9d9cabb2c 100644 --- a/crates/ruff/src/registry.rs +++ b/crates/ruff/src/registry.rs @@ -651,6 +651,7 @@ ruff_macros::register_rules!( rules::ruff::rules::AmbiguousUnicodeCharacterString, rules::ruff::rules::AmbiguousUnicodeCharacterDocstring, rules::ruff::rules::AmbiguousUnicodeCharacterComment, + rules::ruff::rules::BlankComment, rules::ruff::rules::CollectionLiteralConcatenation, rules::ruff::rules::AsyncioDanglingTask, rules::ruff::rules::UnusedNOQA, @@ -902,6 +903,7 @@ impl Rule { | Rule::ShebangLeadingWhitespace | Rule::TrailingWhitespace | Rule::TabIndentation + | Rule::BlankComment | Rule::BlankLineWithWhitespace => LintSource::PhysicalLines, Rule::AmbiguousUnicodeCharacterComment | Rule::AmbiguousUnicodeCharacterDocstring diff --git a/crates/ruff/src/rules/ruff/mod.rs b/crates/ruff/src/rules/ruff/mod.rs index 0be811b724722..d86d5f943df71 100644 --- a/crates/ruff/src/rules/ruff/mod.rs +++ b/crates/ruff/src/rules/ruff/mod.rs @@ -19,6 +19,7 @@ mod tests { #[test_case(Rule::CollectionLiteralConcatenation, Path::new("RUF005.py"); "RUF005")] #[test_case(Rule::AsyncioDanglingTask, Path::new("RUF006.py"); "RUF006")] + #[test_case(Rule::BlankComment, Path::new("RUF010.py"); "RUF010")] fn rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); let diagnostics = test_path( diff --git a/crates/ruff/src/rules/ruff/rules/blank_comment.rs b/crates/ruff/src/rules/ruff/rules/blank_comment.rs new file mode 100644 index 0000000000000..8d392752ec60f --- /dev/null +++ b/crates/ruff/src/rules/ruff/rules/blank_comment.rs @@ -0,0 +1,63 @@ +use crate::registry::Rule; +use crate::settings::{flags, Settings}; +use once_cell::sync::Lazy; +use regex::Regex; +use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::newlines::Line; +use ruff_text_size::{TextLen, TextRange, TextSize}; + +/// ## What it does +/// Check for blank comments. +/// +/// ## Why is this bad? +/// Blank comments are useless and should be removed. +/// +/// ## Example +/// ```python +/// print("Hello, World!") # +/// ``` +/// +/// Use instead: +/// ```python +/// print("Hello, World!") +/// ``` +/// +/// ## References +/// - [Ruff documentation](https://beta.ruff.rs/docs/configuration/#error-suppression) +#[violation] +pub struct BlankComment; + +impl AlwaysAutofixableViolation for BlankComment { + #[derive_message_formats] + fn message(&self) -> String { + format!("Blank comments are useless and should be removed") + } + + fn autofix_title(&self) -> String { + "Remove blank comment".to_string() + } +} + +static BLACK_COMMENT_REGEX: Lazy = Lazy::new(|| Regex::new(r"\S(\s*#\s*)$").unwrap()); + +/// RUF010 +pub fn blank_comment( + diagnostics: &mut Vec, + line: &Line, + settings: &Settings, + autofix: flags::Autofix, +) { + if let Some(captures) = BLACK_COMMENT_REGEX.captures(line.as_str()) { + let match_ = captures.get(1).unwrap(); + let range = TextRange::at( + line.start() + TextSize::try_from(match_.start()).unwrap(), + match_.as_str().text_len(), + ); + let mut diagnostic = Diagnostic::new(BlankComment, range); + if autofix.into() && settings.rules.should_fix(Rule::BlankComment) { + diagnostic.set_fix(Edit::deletion(range.start(), range.end())); + } + diagnostics.push(diagnostic); + } +} diff --git a/crates/ruff/src/rules/ruff/rules/mod.rs b/crates/ruff/src/rules/ruff/rules/mod.rs index 6a063c18dc1f8..be70eb60a7a37 100644 --- a/crates/ruff/src/rules/ruff/rules/mod.rs +++ b/crates/ruff/src/rules/ruff/rules/mod.rs @@ -1,5 +1,6 @@ mod ambiguous_unicode_character; mod asyncio_dangling_task; +mod blank_comment; mod collection_literal_concatenation; mod mutable_defaults_in_dataclass_fields; mod pairwise_over_zipped; @@ -10,6 +11,7 @@ pub use ambiguous_unicode_character::{ AmbiguousUnicodeCharacterDocstring, AmbiguousUnicodeCharacterString, }; pub use asyncio_dangling_task::{asyncio_dangling_task, AsyncioDanglingTask}; +pub use blank_comment::{blank_comment, BlankComment}; pub use collection_literal_concatenation::{ collection_literal_concatenation, CollectionLiteralConcatenation, }; diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF010_RUF010.py.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF010_RUF010.py.snap new file mode 100644 index 0000000000000..863d61e84d732 --- /dev/null +++ b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF010_RUF010.py.snap @@ -0,0 +1,42 @@ +--- +source: crates/ruff/src/rules/ruff/mod.rs +--- +RUF010.py:6:23: RUF010 [*] Blank comments are useless and should be removed + | +6 | # is valid +7 | +8 | print("Hello, world!") # + | ^^^ RUF010 + | + = help: Remove blank comment + +ℹ Suggested fix +3 3 | # +4 4 | # is valid +5 5 | +6 |-print("Hello, world!") # + 6 |+print("Hello, world!") +7 7 | +8 8 | +9 9 | def func_1(): + +RUF010.py:11:27: RUF010 [*] Blank comments are useless and should be removed + | +11 | def func_1(): +12 | # This is a valid comment +13 | print("Hello, world!") # + | ^^^ RUF010 + | + = help: Remove blank comment + +ℹ Suggested fix +8 8 | +9 9 | def func_1(): +10 10 | # This is a valid comment +11 |- print("Hello, world!") # + 11 |+ print("Hello, world!") +12 12 | +13 13 | +14 14 | def func_2(): + + diff --git a/ruff.schema.json b/ruff.schema.json index 6e9f2d2ef9840..199debca1d3e0 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2160,6 +2160,8 @@ "RUF007", "RUF008", "RUF009", + "RUF01", + "RUF010", "RUF1", "RUF10", "RUF100",