From 6a5eff60171a8b17158ee7b890ef36708e0e1f6a Mon Sep 17 00:00:00 2001 From: Dylan <53534755+dylwil3@users.noreply.github.com> Date: Mon, 16 Dec 2024 09:09:27 -0600 Subject: [PATCH] [`pydocstyle`] Skip leading whitespace for `D403` (#14963) This PR introduces three changes to `D403`, which has to do with capitalizing the first word in a docstring. 1. The diagnostic and fix now skip leading whitespace when determining what counts as "the first word". 2. The name has been changed to `first-word-uncapitalized` from `first-line-capitalized`, for both clarity and compliance with our rule naming policy. 3. The diagnostic message and documentation has been modified slightly to reflect this. Closes #14890 --- .../test/fixtures/pydocstyle/D403.py | 10 +++++ .../src/checkers/ast/analyze/definitions.rs | 4 +- crates/ruff_linter/src/codes.rs | 2 +- .../ruff_linter/src/rules/pydocstyle/mod.rs | 4 +- .../src/rules/pydocstyle/rules/capitalized.rs | 19 ++++++---- ...ules__pydocstyle__tests__D403_D403.py.snap | 38 +++++++++++++++++-- 6 files changed, 60 insertions(+), 17 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/pydocstyle/D403.py b/crates/ruff_linter/resources/test/fixtures/pydocstyle/D403.py index bd90d60bc84c2..62105959cf259 100644 --- a/crates/ruff_linter/resources/test/fixtures/pydocstyle/D403.py +++ b/crates/ruff_linter/resources/test/fixtures/pydocstyle/D403.py @@ -31,3 +31,13 @@ def single_word(): def single_word_no_dot(): """singleword""" + +def first_word_lots_of_whitespace(): + """ + + + + here is the start of my docstring! + + What do you think? + """ diff --git a/crates/ruff_linter/src/checkers/ast/analyze/definitions.rs b/crates/ruff_linter/src/checkers/ast/analyze/definitions.rs index 4580d23a644d8..b9e8be637dd53 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/definitions.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/definitions.rs @@ -46,7 +46,7 @@ pub(crate) fn definitions(checker: &mut Checker) { Rule::EndsInPeriod, Rule::EndsInPunctuation, Rule::EscapeSequenceInDocstring, - Rule::FirstLineCapitalized, + Rule::FirstWordUncapitalized, Rule::FitsOnOneLine, Rule::IndentWithSpaces, Rule::MultiLineSummaryFirstLine, @@ -277,7 +277,7 @@ pub(crate) fn definitions(checker: &mut Checker) { if checker.enabled(Rule::NoSignature) { pydocstyle::rules::no_signature(checker, &docstring); } - if checker.enabled(Rule::FirstLineCapitalized) { + if checker.enabled(Rule::FirstWordUncapitalized) { pydocstyle::rules::capitalized(checker, &docstring); } if checker.enabled(Rule::DocstringStartsWithThis) { diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index 13549898308d6..dcd2ca45d8f82 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -566,7 +566,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Pydocstyle, "400") => (RuleGroup::Stable, rules::pydocstyle::rules::EndsInPeriod), (Pydocstyle, "401") => (RuleGroup::Stable, rules::pydocstyle::rules::NonImperativeMood), (Pydocstyle, "402") => (RuleGroup::Stable, rules::pydocstyle::rules::NoSignature), - (Pydocstyle, "403") => (RuleGroup::Stable, rules::pydocstyle::rules::FirstLineCapitalized), + (Pydocstyle, "403") => (RuleGroup::Stable, rules::pydocstyle::rules::FirstWordUncapitalized), (Pydocstyle, "404") => (RuleGroup::Stable, rules::pydocstyle::rules::DocstringStartsWithThis), (Pydocstyle, "405") => (RuleGroup::Stable, rules::pydocstyle::rules::CapitalizeSectionName), (Pydocstyle, "406") => (RuleGroup::Stable, rules::pydocstyle::rules::NewLineAfterSectionName), diff --git a/crates/ruff_linter/src/rules/pydocstyle/mod.rs b/crates/ruff_linter/src/rules/pydocstyle/mod.rs index 9f387fdebb087..700633ff6955a 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/mod.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/mod.rs @@ -32,8 +32,8 @@ mod tests { #[test_case(Rule::EndsInPeriod, Path::new("D400_415.py"))] #[test_case(Rule::EndsInPunctuation, Path::new("D.py"))] #[test_case(Rule::EndsInPunctuation, Path::new("D400_415.py"))] - #[test_case(Rule::FirstLineCapitalized, Path::new("D.py"))] - #[test_case(Rule::FirstLineCapitalized, Path::new("D403.py"))] + #[test_case(Rule::FirstWordUncapitalized, Path::new("D.py"))] + #[test_case(Rule::FirstWordUncapitalized, Path::new("D403.py"))] #[test_case(Rule::FitsOnOneLine, Path::new("D.py"))] #[test_case(Rule::IndentWithSpaces, Path::new("D.py"))] #[test_case(Rule::UndocumentedMagicMethod, Path::new("D.py"))] diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/capitalized.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/capitalized.rs index dbb861b75b8ab..0c6e7a3154d85 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/capitalized.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/capitalized.rs @@ -10,8 +10,8 @@ use crate::docstrings::Docstring; /// Checks for docstrings that do not start with a capital letter. /// /// ## Why is this bad? -/// The first character in a docstring should be capitalized for, grammatical -/// correctness and consistency. +/// The first non-whitespace character in a docstring should be +/// capitalized for grammatical correctness and consistency. /// /// ## Example /// ```python @@ -30,16 +30,16 @@ use crate::docstrings::Docstring; /// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) /// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) #[derive(ViolationMetadata)] -pub(crate) struct FirstLineCapitalized { +pub(crate) struct FirstWordUncapitalized { first_word: String, capitalized_word: String, } -impl AlwaysFixableViolation for FirstLineCapitalized { +impl AlwaysFixableViolation for FirstWordUncapitalized { #[derive_message_formats] fn message(&self) -> String { format!( - "First word of the first line should be capitalized: `{}` -> `{}`", + "First word of the docstring should be capitalized: `{}` -> `{}`", self.first_word, self.capitalized_word ) } @@ -59,7 +59,8 @@ pub(crate) fn capitalized(checker: &mut Checker, docstring: &Docstring) { } let body = docstring.body(); - let first_word = body.split_once(' ').map_or_else( + let trim_start_body = body.trim_start(); + let first_word = trim_start_body.split_once(' ').map_or_else( || { // If the docstring is a single word, trim the punctuation marks because // it makes the ASCII test below fail. @@ -91,8 +92,10 @@ pub(crate) fn capitalized(checker: &mut Checker, docstring: &Docstring) { let capitalized_word = uppercase_first_char.to_string() + first_word_chars.as_str(); + let leading_whitespace_len = body.text_len() - trim_start_body.text_len(); + let mut diagnostic = Diagnostic::new( - FirstLineCapitalized { + FirstWordUncapitalized { first_word: first_word.to_string(), capitalized_word: capitalized_word.to_string(), }, @@ -101,7 +104,7 @@ pub(crate) fn capitalized(checker: &mut Checker, docstring: &Docstring) { diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( capitalized_word, - TextRange::at(body.start(), first_word.text_len()), + TextRange::at(body.start() + leading_whitespace_len, first_word.text_len()), ))); checker.diagnostics.push(diagnostic); diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D403_D403.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D403_D403.py.snap index 6faa93a9fa136..86fbe97384caf 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D403_D403.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D403_D403.py.snap @@ -1,8 +1,7 @@ --- source: crates/ruff_linter/src/rules/pydocstyle/mod.rs -snapshot_kind: text --- -D403.py:2:5: D403 [*] First word of the first line should be capitalized: `this` -> `This` +D403.py:2:5: D403 [*] First word of the docstring should be capitalized: `this` -> `This` | 1 | def bad_function(): 2 | """this docstring is not capitalized""" @@ -20,7 +19,7 @@ D403.py:2:5: D403 [*] First word of the first line should be capitalized: `this` 4 4 | def good_function(): 5 5 | """This docstring is capitalized.""" -D403.py:30:5: D403 [*] First word of the first line should be capitalized: `singleword` -> `Singleword` +D403.py:30:5: D403 [*] First word of the docstring should be capitalized: `singleword` -> `Singleword` | 29 | def single_word(): 30 | """singleword.""" @@ -40,11 +39,13 @@ D403.py:30:5: D403 [*] First word of the first line should be capitalized: `sing 32 32 | def single_word_no_dot(): 33 33 | """singleword""" -D403.py:33:5: D403 [*] First word of the first line should be capitalized: `singleword` -> `Singleword` +D403.py:33:5: D403 [*] First word of the docstring should be capitalized: `singleword` -> `Singleword` | 32 | def single_word_no_dot(): 33 | """singleword""" | ^^^^^^^^^^^^^^^^ D403 +34 | +35 | def first_word_lots_of_whitespace(): | = help: Capitalize `singleword` to `Singleword` @@ -54,3 +55,32 @@ D403.py:33:5: D403 [*] First word of the first line should be capitalized: `sing 32 32 | def single_word_no_dot(): 33 |- """singleword""" 33 |+ """Singleword""" +34 34 | +35 35 | def first_word_lots_of_whitespace(): +36 36 | """ + +D403.py:36:5: D403 [*] First word of the docstring should be capitalized: `here` -> `Here` + | +35 | def first_word_lots_of_whitespace(): +36 | """ + | _____^ +37 | | +38 | | +39 | | +40 | | here is the start of my docstring! +41 | | +42 | | What do you think? +43 | | """ + | |_______^ D403 + | + = help: Capitalize `here` to `Here` + +ℹ Safe fix +37 37 | +38 38 | +39 39 | +40 |- here is the start of my docstring! + 40 |+ Here is the start of my docstring! +41 41 | +42 42 | What do you think? +43 43 | """