From 50a7916e8433d06efdbe85812a7e7cd60bca2615 Mon Sep 17 00:00:00 2001 From: Jonathan Plasse <13716151+JonathanPlasse@users.noreply.github.com> Date: Sat, 25 Mar 2023 20:21:45 +0100 Subject: [PATCH] [`pydocstyle`] Implement autofix for `D403` (#3731) --- .../test/fixtures/pydocstyle/D403.py | 15 ++++++ crates/ruff/src/rules/pydocstyle/mod.rs | 3 +- .../src/rules/pydocstyle/rules/capitalized.rs | 54 ++++++++++++++++--- ...ules__pydocstyle__tests__D403_D403.py.snap | 25 +++++++++ 4 files changed, 88 insertions(+), 9 deletions(-) create mode 100644 crates/ruff/resources/test/fixtures/pydocstyle/D403.py create mode 100644 crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D403_D403.py.snap diff --git a/crates/ruff/resources/test/fixtures/pydocstyle/D403.py b/crates/ruff/resources/test/fixtures/pydocstyle/D403.py new file mode 100644 index 0000000000..129de7d11a --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pydocstyle/D403.py @@ -0,0 +1,15 @@ +def bad_function(): + """this docstring is not capitalized""" + +def good_function(): + """This docstring is capitalized.""" + +def other_function(): + """ + This docstring is capitalized.""" + +def another_function(): + """ This docstring is capitalized.""" + +def utf8_function(): + """éste docstring is capitalized.""" diff --git a/crates/ruff/src/rules/pydocstyle/mod.rs b/crates/ruff/src/rules/pydocstyle/mod.rs index 37b298c4fe..8ea388fa0b 100644 --- a/crates/ruff/src/rules/pydocstyle/mod.rs +++ b/crates/ruff/src/rules/pydocstyle/mod.rs @@ -30,7 +30,8 @@ mod tests { #[test_case(Rule::EndsInPeriod, Path::new("D.py"); "D400_0")] #[test_case(Rule::EndsInPeriod, Path::new("D400.py"); "D400_1")] #[test_case(Rule::EndsInPunctuation, Path::new("D.py"); "D415")] - #[test_case(Rule::FirstLineCapitalized, Path::new("D.py"); "D403")] + #[test_case(Rule::FirstLineCapitalized, Path::new("D.py"); "D403_0")] + #[test_case(Rule::FirstLineCapitalized, Path::new("D403.py"); "D403_1")] #[test_case(Rule::FitsOnOneLine, Path::new("D.py"); "D200")] #[test_case(Rule::IndentWithSpaces, Path::new("D.py"); "D206")] #[test_case(Rule::UndocumentedMagicMethod, Path::new("D.py"); "D105")] diff --git a/crates/ruff/src/rules/pydocstyle/rules/capitalized.rs b/crates/ruff/src/rules/pydocstyle/rules/capitalized.rs index 39f07659af..fbbed8bfe0 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/capitalized.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/capitalized.rs @@ -1,17 +1,33 @@ -use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::str::leading_quote; use ruff_python_ast::types::Range; +use unicode_width::UnicodeWidthStr; use crate::checkers::ast::Checker; use crate::docstrings::definition::{DefinitionKind, Docstring}; +use crate::registry::AsRule; #[violation] -pub struct FirstLineCapitalized; +pub struct FirstLineCapitalized { + pub first_word: String, + pub capitalized_word: String, +} -impl Violation for FirstLineCapitalized { +impl AlwaysAutofixableViolation for FirstLineCapitalized { #[derive_message_formats] fn message(&self) -> String { - format!("First word of the first line should be properly capitalized") + format!( + "First word of the first line should be capitalized: `{}` -> `{}`", + self.first_word, self.capitalized_word + ) + } + + fn autofix_title(&self) -> String { + format!( + "Capitalize `{}` to `{}`", + self.first_word, self.capitalized_word + ) } } @@ -37,14 +53,36 @@ pub fn capitalized(checker: &mut Checker, docstring: &Docstring) { return; } } - let Some(first_char) = first_word.chars().next() else { + let mut first_word_chars = first_word.chars(); + let Some(first_char) = first_word_chars.next() else { return; }; if first_char.is_uppercase() { return; }; - checker.diagnostics.push(Diagnostic::new( - FirstLineCapitalized, + + let capitalized_word = first_char.to_uppercase().to_string() + first_word_chars.as_str(); + + let mut diagnostic = Diagnostic::new( + FirstLineCapitalized { + first_word: first_word.to_string(), + capitalized_word: capitalized_word.to_string(), + }, Range::from(docstring.expr), - )); + ); + + if checker.patch(diagnostic.kind.rule()) { + if let Some(pattern) = leading_quote(docstring.contents) { + diagnostic.amend(Edit::replacement( + capitalized_word, + docstring.expr.location.with_col_offset(pattern.width()), + docstring + .expr + .location + .with_col_offset(pattern.width() + first_word.width()), + )); + } + } + + checker.diagnostics.push(diagnostic); } diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D403_D403.py.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D403_D403.py.snap new file mode 100644 index 0000000000..10775349f4 --- /dev/null +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D403_D403.py.snap @@ -0,0 +1,25 @@ +--- +source: crates/ruff/src/rules/pydocstyle/mod.rs +expression: diagnostics +--- +- kind: + name: FirstLineCapitalized + body: "First word of the first line should be capitalized: `this` -> `This`" + suggestion: "Capitalize `this` to `This`" + fixable: true + location: + row: 2 + column: 4 + end_location: + row: 2 + column: 43 + fix: + content: This + location: + row: 2 + column: 7 + end_location: + row: 2 + column: 11 + parent: ~ +