diff --git a/README.md b/README.md index 2133d8708e..ac8623cb36 100644 --- a/README.md +++ b/README.md @@ -303,15 +303,15 @@ The 🛠 emoji indicates that a rule is automatically fixable by the `--fix` com | D211 | NoBlankLineBeforeClass | No blank lines allowed before class docstring | 🛠 | | D212 | MultiLineSummaryFirstLine | Multi-line docstring summary should start at the first line | | | D213 | MultiLineSummarySecondLine | Multi-line docstring summary should start at the second line | | -| D214 | SectionNotOverIndented | Section is over-indented ("Returns") | | +| D214 | SectionNotOverIndented | Section is over-indented ("Returns") | 🛠 | | D215 | SectionUnderlineNotOverIndented | Section underline is over-indented ("Returns") | 🛠 | | D300 | UsesTripleQuotes | Use """triple double quotes""" | | | D400 | EndsInPeriod | First line should end with a period | | | D402 | NoSignature | First line should not be the function's 'signature' | | | D403 | FirstLineCapitalized | First word of the first line should be properly capitalized | | | D404 | NoThisPrefix | First word of the docstring should not be `This` | | -| D405 | CapitalizeSectionName | Section name should be properly capitalized ("returns") | | -| D406 | NewLineAfterSectionName | Section name should end with a newline ("Returns") | | +| D405 | CapitalizeSectionName | Section name should be properly capitalized ("returns") | 🛠 | +| D406 | NewLineAfterSectionName | Section name should end with a newline ("Returns") | 🛠 | | D407 | DashedUnderlineAfterSection | Missing dashed underline after section ("Returns") | 🛠 | | D408 | SectionUnderlineAfterName | Section underline should be in the line following the section's name ("Returns") | | | D409 | SectionUnderlineMatchesSectionLength | Section underline should match the length of its name ("Returns") | 🛠 | @@ -321,7 +321,7 @@ The 🛠 emoji indicates that a rule is automatically fixable by the `--fix` com | D413 | BlankLineAfterLastSection | Missing blank line after last section ("Returns") | 🛠 | | D414 | NonEmptySection | Section has no content ("Returns") | | | D415 | EndsInPunctuation | First line should end with a period, question mark, or exclamation point | | -| D416 | SectionNameEndsInColon | Section name should end with a colon ("Returns") | | +| D416 | SectionNameEndsInColon | Section name should end with a colon ("Returns") | 🛠 | | D417 | DocumentAllArguments | Missing argument descriptions in the docstring: `x`, `y` | | | D418 | SkipDocstring | Function decorated with @overload shouldn't contain a docstring | | | D419 | NonEmpty | Docstring is empty | | @@ -399,6 +399,7 @@ The 🛠 emoji indicates that a rule is automatically fixable by the `--fix` com | ---- | ---- | ------- | --- | | M001 | UnusedNOQA | Unused `noqa` directive | 🛠 | + ## Editor Integrations ### PyCharm diff --git a/src/checks.rs b/src/checks.rs index 05720d2858..c133071e4d 100644 --- a/src/checks.rs +++ b/src/checks.rs @@ -1208,11 +1208,13 @@ impl CheckKind { | CheckKind::BlankLineAfterSection(_) | CheckKind::BlankLineAfterSummary | CheckKind::BlankLineBeforeSection(_) + | CheckKind::CapitalizeSectionName(_) | CheckKind::DashedUnderlineAfterSection(_) | CheckKind::DeprecatedUnittestAlias(_, _) | CheckKind::DoNotAssertFalse | CheckKind::DuplicateHandlerException(_) | CheckKind::NewLineAfterLastParagraph + | CheckKind::NewLineAfterSectionName(_) | CheckKind::NoBlankLineAfterFunction(_) | CheckKind::NoBlankLineBeforeClass(_) | CheckKind::NoBlankLineBeforeFunction(_) @@ -1222,6 +1224,8 @@ impl CheckKind { | CheckKind::OneBlankLineBeforeClass(_) | CheckKind::PPrintFound | CheckKind::PrintFound + | CheckKind::SectionNameEndsInColon(_) + | CheckKind::SectionNotOverIndented(_) | CheckKind::SectionUnderlineMatchesSectionLength(_) | CheckKind::SectionUnderlineNotOverIndented(_) | CheckKind::SuperCallWithParameters diff --git a/src/pydocstyle/plugins.rs b/src/pydocstyle/plugins.rs index d49b8c3e51..b55814de11 100644 --- a/src/pydocstyle/plugins.rs +++ b/src/pydocstyle/plugins.rs @@ -4,7 +4,6 @@ use itertools::Itertools; use once_cell::sync::Lazy; use regex::Regex; use rustpython_ast::{Arg, Constant, ExprKind, Location, StmtKind}; -use titlecase::titlecase; use crate::ast::types::Range; use crate::autofix::fixer; @@ -1099,25 +1098,61 @@ fn common_section( if !style .section_names() .contains(&context.section_name.as_str()) - && style - .section_names() - .contains(titlecase(&context.section_name).as_str()) { - checker.add_check(Check::new( - CheckKind::CapitalizeSectionName(context.section_name.to_string()), - Range::from_located(docstring), - )) + let capitalized_section_name = titlecase::titlecase(&context.section_name); + if style + .section_names() + .contains(capitalized_section_name.as_str()) + { + let mut check = Check::new( + CheckKind::CapitalizeSectionName(context.section_name.to_string()), + Range::from_located(docstring), + ); + if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) { + // Replace the section title with the capitalized variant. This requires + // locating the start and end of the section name. + if let Some(index) = context.line.find(&context.section_name) { + // Map from bytes to characters. + let section_name_start = &context.line[..index].chars().count(); + let section_name_length = &context.section_name.chars().count(); + check.amend(Fix::replacement( + capitalized_section_name, + Location::new( + docstring.location.row() + context.original_index, + 1 + section_name_start, + ), + Location::new( + docstring.location.row() + context.original_index, + 1 + section_name_start + section_name_length, + ), + )) + } + } + checker.add_check(check); + } } } if checker.settings.enabled.contains(&CheckCode::D214) { - if helpers::leading_space(context.line).len() - > helpers::indentation(checker, docstring).len() - { - checker.add_check(Check::new( + let leading_space = helpers::leading_space(context.line); + let indentation = helpers::indentation(checker, docstring).to_string(); + if leading_space.len() > indentation.len() { + let mut check = Check::new( CheckKind::SectionNotOverIndented(context.section_name.to_string()), Range::from_located(docstring), - )) + ); + if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) { + // Replace the existing indentation with whitespace of the appropriate length. + check.amend(Fix::replacement( + indentation, + Location::new(docstring.location.row() + context.original_index, 1), + Location::new( + docstring.location.row() + context.original_index, + 1 + leading_space.len(), + ), + )); + }; + checker.add_check(check); } } @@ -1144,7 +1179,7 @@ fn common_section( + context.following_lines.len(), 1, ), - )) + )); } checker.add_check(check); } @@ -1334,10 +1369,31 @@ fn numpy_section(checker: &mut Checker, definition: &Definition, context: &Secti let docstring = definition .docstring .expect("Sections are only available for docstrings."); - checker.add_check(Check::new( + let mut check = Check::new( CheckKind::NewLineAfterSectionName(context.section_name.to_string()), Range::from_located(docstring), - )) + ); + if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) { + // Delete the suffix. This requires locating the end of the section name. + if let Some(index) = context.line.find(&context.section_name) { + // Map from bytes to characters. + let suffix_start = &context.line[..index + context.section_name.len()] + .chars() + .count(); + let suffix_length = suffix.chars().count(); + check.amend(Fix::deletion( + Location::new( + docstring.location.row() + context.original_index, + 1 + suffix_start, + ), + Location::new( + docstring.location.row() + context.original_index, + 1 + suffix_start + suffix_length, + ), + )); + } + } + checker.add_check(check) } } @@ -1362,10 +1418,32 @@ fn google_section(checker: &mut Checker, definition: &Definition, context: &Sect let docstring = definition .docstring .expect("Sections are only available for docstrings."); - checker.add_check(Check::new( + let mut check = Check::new( CheckKind::SectionNameEndsInColon(context.section_name.to_string()), Range::from_located(docstring), - )) + ); + if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) { + // Replace the suffix. This requires locating the end of the section name. + if let Some(index) = context.line.find(&context.section_name) { + // Map from bytes to characters. + let suffix_start = &context.line[..index + context.section_name.len()] + .chars() + .count(); + let suffix_length = suffix.chars().count(); + check.amend(Fix::replacement( + ":".to_string(), + Location::new( + docstring.location.row() + context.original_index, + 1 + suffix_start, + ), + Location::new( + docstring.location.row() + context.original_index, + 1 + suffix_start + suffix_length, + ), + )); + } + } + checker.add_check(check); } } diff --git a/src/snapshots/ruff__linter__tests__D214_sections.py.snap b/src/snapshots/ruff__linter__tests__D214_sections.py.snap index 91faef696d..09c12d2eb4 100644 --- a/src/snapshots/ruff__linter__tests__D214_sections.py.snap +++ b/src/snapshots/ruff__linter__tests__D214_sections.py.snap @@ -10,5 +10,14 @@ expression: checks end_location: row: 141 column: 8 - fix: ~ + fix: + patch: + content: " " + location: + row: 137 + column: 1 + end_location: + row: 137 + column: 9 + applied: false diff --git a/src/snapshots/ruff__linter__tests__D405_sections.py.snap b/src/snapshots/ruff__linter__tests__D405_sections.py.snap index b5ad30460a..abbd5ff29e 100644 --- a/src/snapshots/ruff__linter__tests__D405_sections.py.snap +++ b/src/snapshots/ruff__linter__tests__D405_sections.py.snap @@ -10,7 +10,16 @@ expression: checks end_location: row: 23 column: 8 - fix: ~ + fix: + patch: + content: Returns + location: + row: 19 + column: 5 + end_location: + row: 19 + column: 12 + applied: false - kind: CapitalizeSectionName: Short summary location: @@ -19,5 +28,14 @@ expression: checks end_location: row: 221 column: 8 - fix: ~ + fix: + patch: + content: Short Summary + location: + row: 209 + column: 5 + end_location: + row: 209 + column: 18 + applied: false diff --git a/src/snapshots/ruff__linter__tests__D406_sections.py.snap b/src/snapshots/ruff__linter__tests__D406_sections.py.snap index fda71c0dfa..ed42ec8cf2 100644 --- a/src/snapshots/ruff__linter__tests__D406_sections.py.snap +++ b/src/snapshots/ruff__linter__tests__D406_sections.py.snap @@ -10,7 +10,16 @@ expression: checks end_location: row: 36 column: 8 - fix: ~ + fix: + patch: + content: "" + location: + row: 32 + column: 12 + end_location: + row: 32 + column: 13 + applied: false - kind: NewLineAfterSectionName: Raises location: @@ -19,7 +28,16 @@ expression: checks end_location: row: 221 column: 8 - fix: ~ + fix: + patch: + content: "" + location: + row: 218 + column: 11 + end_location: + row: 218 + column: 12 + applied: false - kind: NewLineAfterSectionName: Returns location: @@ -28,7 +46,16 @@ expression: checks end_location: row: 262 column: 8 - fix: ~ + fix: + patch: + content: "" + location: + row: 257 + column: 12 + end_location: + row: 257 + column: 13 + applied: false - kind: NewLineAfterSectionName: Raises location: @@ -37,5 +64,14 @@ expression: checks end_location: row: 262 column: 8 - fix: ~ + fix: + patch: + content: "" + location: + row: 259 + column: 11 + end_location: + row: 259 + column: 12 + applied: false