diff --git a/README.md b/README.md index 641a58447d..4a4fc478c5 100644 --- a/README.md +++ b/README.md @@ -217,7 +217,7 @@ ruff also implements some of the most popular Flake8 plugins natively, including - [`flake8-print`](https://pypi.org/project/flake8-print/) - [`flake8-comprehensions`](https://pypi.org/project/flake8-comprehensions/) (11/16) - [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (3/32) -- [`flake8-docstrings`](https://pypi.org/project/flake8-docstrings/) (11/47) +- [`flake8-docstrings`](https://pypi.org/project/flake8-docstrings/) (12/47) - [`pyupgrade`](https://pypi.org/project/pyupgrade/) (8/34) Beyond rule-set parity, ruff suffers from the following limitations vis-à-vis Flake8: @@ -312,6 +312,7 @@ The 🛠 emoji indicates that a rule is automatically fixable by the `--fix` com | D213 | MultiLineSummarySecondLine | Multi-line docstring summary should start at the second line | | | | 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 | | | | D415 | EndsInPunctuation | First line should end with a period, question mark, or exclamation point | | | | D419 | NonEmpty | Docstring is empty | | | diff --git a/src/check_ast.rs b/src/check_ast.rs index 95564cf3df..ebc60af751 100644 --- a/src/check_ast.rs +++ b/src/check_ast.rs @@ -1916,6 +1916,9 @@ impl<'a> Checker<'a> { if self.settings.enabled.contains(&CheckCode::D400) { docstrings::ends_with_period(self, &docstring); } + if self.settings.enabled.contains(&CheckCode::D402) { + docstrings::no_signature(self, &docstring); + } if self.settings.enabled.contains(&CheckCode::D403) { docstrings::capitalized(self, &docstring); } diff --git a/src/checks.rs b/src/checks.rs index 2abd2ccd6b..16e31ee061 100644 --- a/src/checks.rs +++ b/src/checks.rs @@ -159,6 +159,7 @@ pub enum CheckCode { D213, D300, D400, + D402, D403, D415, D419, @@ -270,6 +271,7 @@ pub enum CheckKind { NoSurroundingWhitespace, NonEmpty, UsesTripleQuotes, + NoSignature, // Meta UnusedNOQA(Option>), } @@ -388,6 +390,7 @@ impl CheckCode { CheckCode::D212 => CheckKind::MultiLineSummaryFirstLine, CheckCode::D213 => CheckKind::MultiLineSummarySecondLine, CheckCode::D300 => CheckKind::UsesTripleQuotes, + CheckCode::D402 => CheckKind::NoSignature, CheckCode::D403 => CheckKind::FirstLineCapitalized, CheckCode::D415 => CheckKind::EndsInPunctuation, // Meta @@ -487,6 +490,7 @@ impl CheckKind { CheckKind::MultiLineSummaryFirstLine => &CheckCode::D212, CheckKind::MultiLineSummarySecondLine => &CheckCode::D213, CheckKind::UsesTripleQuotes => &CheckCode::D300, + CheckKind::NoSignature => &CheckCode::D402, CheckKind::FirstLineCapitalized => &CheckCode::D403, CheckKind::EndsInPunctuation => &CheckCode::D415, // Meta @@ -759,6 +763,9 @@ impl CheckKind { CheckKind::MultiLineSummarySecondLine => { "Multi-line docstring summary should start at the second line".to_string() } + CheckKind::NoSignature => { + "First line should not be the function's 'signature'".to_string() + } // Meta CheckKind::UnusedNOQA(codes) => match codes { None => "Unused `noqa` directive".to_string(), diff --git a/src/docstrings.rs b/src/docstrings.rs index 96a02c25ec..97b6955ad0 100644 --- a/src/docstrings.rs +++ b/src/docstrings.rs @@ -5,22 +5,21 @@ use crate::check_ast::Checker; use crate::checks::{Check, CheckCode, CheckKind}; #[derive(Debug)] -pub enum DocstringKind { +pub enum DocstringKind<'a> { Module, - Function, - Class, + Function(&'a Stmt), + Class(&'a Stmt), } #[derive(Debug)] pub struct Docstring<'a> { - pub kind: DocstringKind, - pub parent: Option<&'a Stmt>, + pub kind: DocstringKind<'a>, pub expr: &'a Expr, } /// Extract a `Docstring` from an `Expr`. pub fn extract<'a, 'b>( - checker: &'a Checker, + checker: &'a Checker<'b>, stmt: &'b Stmt, expr: &'b Expr, ) -> Option> { @@ -33,7 +32,6 @@ pub fn extract<'a, 'b>( if checker.initial { return Some(Docstring { kind: DocstringKind::Module, - parent: None, expr, }); } @@ -46,11 +44,10 @@ pub fn extract<'a, 'b>( if body.first().map(|node| node == stmt).unwrap_or_default() { return Some(Docstring { kind: if matches!(&parent.node, StmtKind::ClassDef { .. }) { - DocstringKind::Class + DocstringKind::Class(parent) } else { - DocstringKind::Function + DocstringKind::Function(parent) }, - parent: None, expr, }); } @@ -257,9 +254,28 @@ pub fn ends_with_period(checker: &mut Checker, docstring: &Docstring) { } } +/// D402 +pub fn no_signature(checker: &mut Checker, docstring: &Docstring) { + if let DocstringKind::Function(parent) = docstring.kind { + if let StmtKind::FunctionDef { name, .. } = &parent.node { + if let ExprKind::Constant { + value: Constant::Str(string), + .. + } = &docstring.expr.node + { + if let Some(first_line) = string.lines().next() { + if first_line.contains(&format!("{name}(")) { + checker.add_check(Check::new(CheckKind::NoSignature, range_for(docstring))); + } + } + } + } + } +} + /// D403 pub fn capitalized(checker: &mut Checker, docstring: &Docstring) { - if !matches!(docstring.kind, DocstringKind::Function) { + if !matches!(docstring.kind, DocstringKind::Function(_)) { return; } diff --git a/src/linter.rs b/src/linter.rs index 6ac4985c57..db49be661f 100644 --- a/src/linter.rs +++ b/src/linter.rs @@ -1104,6 +1104,18 @@ mod tests { Ok(()) } + #[test] + fn d402() -> Result<()> { + let mut checks = check_path( + Path::new("./resources/test/fixtures/D.py"), + &settings::Settings::for_rule(CheckCode::D402), + &fixer::Mode::Generate, + )?; + checks.sort_by_key(|check| check.location); + insta::assert_yaml_snapshot!(checks); + Ok(()) + } + #[test] fn d403() -> Result<()> { let mut checks = check_path( diff --git a/src/snapshots/ruff__linter__tests__d402.snap b/src/snapshots/ruff__linter__tests__d402.snap new file mode 100644 index 0000000000..17f409cdbd --- /dev/null +++ b/src/snapshots/ruff__linter__tests__d402.snap @@ -0,0 +1,13 @@ +--- +source: src/linter.rs +expression: checks +--- +- kind: NoSignature + location: + row: 373 + column: 5 + end_location: + row: 373 + column: 31 + fix: ~ +