Allow blank line before sticky-comment functions in docstrings (#2597)

This commit is contained in:
Charlie Marsh 2023-02-05 18:48:29 -05:00 committed by GitHub
parent 7fa5ce8b63
commit 79776c12e2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 227 additions and 70 deletions

View file

@ -0,0 +1,87 @@
"""Test: no-blank-line-after-function special-cases around comment handling."""
# OK
def outer():
"""This is a docstring."""
def inner():
return
return inner
# OK
def outer():
"""This is a docstring."""
def inner():
return
return inner
# OK
def outer():
"""This is a docstring."""
# This is a comment.
def inner():
return
return inner
# OK
def outer():
"""This is a docstring."""
# This is a comment.
def inner():
return
return inner
# OK
def outer():
"""This is a docstring."""
# This is a comment.
# Followed by another comment.
def inner():
return
return inner
# D202
def outer():
"""This is a docstring."""
def inner():
return
return inner
# D202
def outer():
"""This is a docstring."""
# This is a comment.
def inner():
return
return inner
# D202
def outer():
"""This is a docstring."""
# This is a comment.
def inner():
return
return inner

View file

@ -15,57 +15,58 @@ mod tests {
use crate::test::test_path;
use crate::{assert_yaml_snapshot, settings};
#[test_case(Rule::PublicModule, Path::new("D.py"); "D100")]
#[test_case(Rule::PublicClass, Path::new("D.py"); "D101")]
#[test_case(Rule::PublicMethod, Path::new("D.py"); "D102_0")]
#[test_case(Rule::PublicMethod, Path::new("setter.py"); "D102_1")]
#[test_case(Rule::PublicFunction, Path::new("D.py"); "D103")]
#[test_case(Rule::PublicPackage, Path::new("D.py"); "D104")]
#[test_case(Rule::MagicMethod, Path::new("D.py"); "D105")]
#[test_case(Rule::PublicNestedClass, Path::new("D.py"); "D106")]
#[test_case(Rule::PublicInit, Path::new("D.py"); "D107")]
#[test_case(Rule::FitsOnOneLine, Path::new("D.py"); "D200")]
#[test_case(Rule::NoBlankLineBeforeFunction, Path::new("D.py"); "D201")]
#[test_case(Rule::NoBlankLineAfterFunction, Path::new("D.py"); "D202")]
#[test_case(Rule::OneBlankLineBeforeClass, Path::new("D.py"); "D203")]
#[test_case(Rule::OneBlankLineAfterClass, Path::new("D.py"); "D204")]
#[test_case(Rule::BlankLineAfterSummary, Path::new("D.py"); "D205")]
#[test_case(Rule::IndentWithSpaces, Path::new("D.py"); "D206")]
#[test_case(Rule::NoUnderIndentation, Path::new("D.py"); "D207")]
#[test_case(Rule::NoOverIndentation, Path::new("D.py"); "D208")]
#[test_case(Rule::NewLineAfterLastParagraph, Path::new("D.py"); "D209")]
#[test_case(Rule::NoSurroundingWhitespace, Path::new("D.py"); "D210")]
#[test_case(Rule::NoBlankLineBeforeClass, Path::new("D.py"); "D211")]
#[test_case(Rule::MultiLineSummaryFirstLine, Path::new("D.py"); "D212")]
#[test_case(Rule::MultiLineSummarySecondLine, Path::new("D.py"); "D213")]
#[test_case(Rule::SectionNotOverIndented, Path::new("sections.py"); "D214")]
#[test_case(Rule::SectionUnderlineNotOverIndented, Path::new("sections.py"); "D215")]
#[test_case(Rule::UsesTripleQuotes, Path::new("D.py"); "D300")]
#[test_case(Rule::UsesRPrefixForBackslashedContent, Path::new("D.py"); "D301")]
#[test_case(Rule::EndsInPeriod, Path::new("D.py"); "D400_0")]
#[test_case(Rule::EndsInPeriod, Path::new("D400.py"); "D400_1")]
#[test_case(Rule::NonImperativeMood, Path::new("D401.py"); "D401")]
#[test_case(Rule::NoSignature, Path::new("D.py"); "D402")]
#[test_case(Rule::FirstLineCapitalized, Path::new("D.py"); "D403")]
#[test_case(Rule::NoThisPrefix, Path::new("D.py"); "D404")]
#[test_case(Rule::CapitalizeSectionName, Path::new("sections.py"); "D405")]
#[test_case(Rule::NewLineAfterSectionName, Path::new("sections.py"); "D406")]
#[test_case(Rule::DashedUnderlineAfterSection, Path::new("sections.py"); "D407")]
#[test_case(Rule::SectionUnderlineAfterName, Path::new("sections.py"); "D408")]
#[test_case(Rule::SectionUnderlineMatchesSectionLength, Path::new("sections.py"); "D409")]
#[test_case(Rule::BlankLineAfterSection, Path::new("sections.py"); "D410")]
#[test_case(Rule::BlankLineBeforeSection, Path::new("sections.py"); "D411")]
#[test_case(Rule::NoBlankLinesBetweenHeaderAndContent, Path::new("sections.py"); "D412")]
#[test_case(Rule::BlankLineAfterLastSection, Path::new("sections.py"); "D413")]
#[test_case(Rule::NonEmptySection, Path::new("sections.py"); "D414")]
#[test_case(Rule::EndsInPunctuation, Path::new("D.py"); "D415")]
#[test_case(Rule::SectionNameEndsInColon, Path::new("D.py"); "D416")]
#[test_case(Rule::BlankLineAfterSection, Path::new("sections.py"); "D410")]
#[test_case(Rule::BlankLineAfterSummary, Path::new("D.py"); "D205")]
#[test_case(Rule::BlankLineBeforeSection, Path::new("sections.py"); "D411")]
#[test_case(Rule::CapitalizeSectionName, Path::new("sections.py"); "D405")]
#[test_case(Rule::DashedUnderlineAfterSection, Path::new("sections.py"); "D407")]
#[test_case(Rule::DocumentAllArguments, Path::new("canonical_google_examples.py"); "D417_2")]
#[test_case(Rule::DocumentAllArguments, Path::new("canonical_numpy_examples.py"); "D417_1")]
#[test_case(Rule::DocumentAllArguments, Path::new("sections.py"); "D417_0")]
#[test_case(Rule::SkipDocstring, Path::new("D.py"); "D418")]
#[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::FitsOnOneLine, Path::new("D.py"); "D200")]
#[test_case(Rule::IndentWithSpaces, Path::new("D.py"); "D206")]
#[test_case(Rule::MagicMethod, Path::new("D.py"); "D105")]
#[test_case(Rule::MultiLineSummaryFirstLine, Path::new("D.py"); "D212")]
#[test_case(Rule::MultiLineSummarySecondLine, Path::new("D.py"); "D213")]
#[test_case(Rule::NewLineAfterLastParagraph, Path::new("D.py"); "D209")]
#[test_case(Rule::NewLineAfterSectionName, Path::new("sections.py"); "D406")]
#[test_case(Rule::NoBlankLineAfterFunction, Path::new("D.py"); "D202_0")]
#[test_case(Rule::NoBlankLineAfterFunction, Path::new("D202.py"); "D202_1")]
#[test_case(Rule::NoBlankLineBeforeClass, Path::new("D.py"); "D211")]
#[test_case(Rule::NoBlankLineBeforeFunction, Path::new("D.py"); "D201")]
#[test_case(Rule::NoBlankLinesBetweenHeaderAndContent, Path::new("sections.py"); "D412")]
#[test_case(Rule::NoOverIndentation, Path::new("D.py"); "D208")]
#[test_case(Rule::NoSignature, Path::new("D.py"); "D402")]
#[test_case(Rule::NoSurroundingWhitespace, Path::new("D.py"); "D210")]
#[test_case(Rule::NoThisPrefix, Path::new("D.py"); "D404")]
#[test_case(Rule::NoUnderIndentation, Path::new("D.py"); "D207")]
#[test_case(Rule::NonEmpty, Path::new("D.py"); "D419")]
#[test_case(Rule::NonEmptySection, Path::new("sections.py"); "D414")]
#[test_case(Rule::NonImperativeMood, Path::new("D401.py"); "D401")]
#[test_case(Rule::OneBlankLineAfterClass, Path::new("D.py"); "D204")]
#[test_case(Rule::OneBlankLineBeforeClass, Path::new("D.py"); "D203")]
#[test_case(Rule::PublicClass, Path::new("D.py"); "D101")]
#[test_case(Rule::PublicFunction, Path::new("D.py"); "D103")]
#[test_case(Rule::PublicInit, Path::new("D.py"); "D107")]
#[test_case(Rule::PublicMethod, Path::new("D.py"); "D102_0")]
#[test_case(Rule::PublicMethod, Path::new("setter.py"); "D102_1")]
#[test_case(Rule::PublicModule, Path::new("D.py"); "D100")]
#[test_case(Rule::PublicNestedClass, Path::new("D.py"); "D106")]
#[test_case(Rule::PublicPackage, Path::new("D.py"); "D104")]
#[test_case(Rule::PublicPackage, Path::new("D104/__init__.py"); "D104_1")]
#[test_case(Rule::SectionNameEndsInColon, Path::new("D.py"); "D416")]
#[test_case(Rule::SectionNotOverIndented, Path::new("sections.py"); "D214")]
#[test_case(Rule::SectionUnderlineAfterName, Path::new("sections.py"); "D408")]
#[test_case(Rule::SectionUnderlineMatchesSectionLength, Path::new("sections.py"); "D409")]
#[test_case(Rule::SectionUnderlineNotOverIndented, Path::new("sections.py"); "D215")]
#[test_case(Rule::SkipDocstring, Path::new("D.py"); "D418")]
#[test_case(Rule::UsesRPrefixForBackslashedContent, Path::new("D.py"); "D301")]
#[test_case(Rule::UsesTripleQuotes, Path::new("D.py"); "D300")]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.code(), path.to_string_lossy());
let diagnostics = test_path(

View file

@ -1,13 +1,15 @@
use once_cell::sync::Lazy;
use regex::Regex;
use ruff_macros::derive_message_formats;
use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::define_violation;
use crate::docstrings::definition::Docstring;
use crate::registry::Diagnostic;
use crate::rules::pydocstyle::rules::regexes::BACKSLASH_REGEX;
use crate::violation::Violation;
use crate::define_violation;
use ruff_macros::derive_message_formats;
define_violation!(
pub struct UsesRPrefixForBackslashedContent;
);
@ -18,6 +20,8 @@ impl Violation for UsesRPrefixForBackslashedContent {
}
}
static BACKSLASH_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"\\[^(\r\n|\n)uN]").unwrap());
/// D301
pub fn backslashes(checker: &mut Checker, docstring: &Docstring) {
let contents = docstring.contents;

View file

@ -1,15 +1,14 @@
use ruff_macros::derive_message_formats;
use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::define_violation;
use crate::docstrings::definition::{DefinitionKind, Docstring};
use crate::fix::Fix;
use crate::message::Location;
use crate::registry::{Diagnostic, Rule};
use crate::rules::pydocstyle::rules::regexes::COMMENT_REGEX;
use crate::violation::AlwaysAutofixableViolation;
use crate::define_violation;
use ruff_macros::derive_message_formats;
define_violation!(
pub struct OneBlankLineBeforeClass {
pub lines: usize,
@ -144,7 +143,7 @@ pub fn blank_before_after_class(checker: &mut Checker, docstring: &Docstring) {
let all_blank_after = after
.lines()
.skip(1)
.all(|line| line.trim().is_empty() || COMMENT_REGEX.is_match(line));
.all(|line| line.trim().is_empty() || line.trim_start().starts_with('#'));
if all_blank_after {
return;
}

View file

@ -1,15 +1,17 @@
use once_cell::sync::Lazy;
use regex::Regex;
use ruff_macros::derive_message_formats;
use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::define_violation;
use crate::docstrings::definition::{DefinitionKind, Docstring};
use crate::fix::Fix;
use crate::message::Location;
use crate::registry::{Diagnostic, Rule};
use crate::rules::pydocstyle::rules::regexes::{COMMENT_REGEX, INNER_FUNCTION_OR_CLASS_REGEX};
use crate::violation::AlwaysAutofixableViolation;
use crate::define_violation;
use ruff_macros::derive_message_formats;
define_violation!(
pub struct NoBlankLineBeforeFunction {
pub num_lines: usize,
@ -44,6 +46,9 @@ impl AlwaysAutofixableViolation for NoBlankLineAfterFunction {
}
}
static INNER_FUNCTION_OR_CLASS_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^\s+(?:(?:class|def|async def)\s|@)").unwrap());
/// D201, D202
pub fn blank_before_after_function(checker: &mut Checker, docstring: &Docstring) {
let (
@ -98,22 +103,30 @@ pub fn blank_before_after_function(checker: &mut Checker, docstring: &Docstring)
&Range::from_located(docstring.expr),
);
// If the docstring is only followed by blank and commented lines, abort.
let all_blank_after = after
.lines()
.skip(1)
.all(|line| line.trim().is_empty() || COMMENT_REGEX.is_match(line));
.all(|line| line.trim().is_empty() || line.trim_start().starts_with('#'));
if all_blank_after {
return;
}
// Count the number of blank lines after the docstring.
let blank_lines_after = after
.lines()
.skip(1)
.take_while(|line| line.trim().is_empty())
.count();
// Avoid D202 violations for blank lines followed by inner functions or classes.
if blank_lines_after == 1 && INNER_FUNCTION_OR_CLASS_REGEX.is_match(after) {
// Avoid violations for blank lines followed by inner functions or classes.
if blank_lines_after == 1
&& after
.lines()
.skip(1 + blank_lines_after)
.find(|line| !line.trim_start().starts_with('#'))
.map_or(false, |line| INNER_FUNCTION_OR_CLASS_REGEX.is_match(line))
{
return;
}

View file

@ -52,7 +52,6 @@ mod non_imperative_mood;
mod not_empty;
mod not_missing;
mod one_liner;
mod regexes;
mod sections;
mod starts_with_this;
mod triple_quotes;

View file

@ -1,8 +0,0 @@
use once_cell::sync::Lazy;
use regex::Regex;
pub static BACKSLASH_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"\\[^(\r\n|\n)uN]").unwrap());
pub static COMMENT_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^\s*#").unwrap());
pub static INNER_FUNCTION_OR_CLASS_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^\s+(?:(?:class|def|async def)\s|@)").unwrap());

View file

@ -0,0 +1,62 @@
---
source: crates/ruff/src/rules/pydocstyle/mod.rs
expression: diagnostics
---
- kind:
NoBlankLineAfterFunction:
num_lines: 2
location:
row: 57
column: 4
end_location:
row: 57
column: 30
fix:
content:
- ""
location:
row: 58
column: 0
end_location:
row: 60
column: 0
parent: ~
- kind:
NoBlankLineAfterFunction:
num_lines: 2
location:
row: 68
column: 4
end_location:
row: 68
column: 30
fix:
content:
- ""
location:
row: 69
column: 0
end_location:
row: 71
column: 0
parent: ~
- kind:
NoBlankLineAfterFunction:
num_lines: 1
location:
row: 80
column: 4
end_location:
row: 80
column: 30
fix:
content:
- ""
location:
row: 81
column: 0
end_location:
row: 82
column: 0
parent: ~