diff --git a/README.md b/README.md index 4a23206d11..ab057bc557 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/) (1/47) +- [`flake8-docstrings`](https://pypi.org/project/flake8-docstrings/) (2/47) - [`pyupgrade`](https://pypi.org/project/pyupgrade/) (partial) Beyond rule-set parity, ruff suffers from the following limitations vis-à-vis Flake8: @@ -304,6 +304,7 @@ The 🛠 emoji indicates that a rule is automatically fixable by the `--fix` com | U006 | UsePEP585Annotation | Use `list` instead of `List` for type annotations | | 🛠 | | U007 | UsePEP604Annotation | Use `X \| Y` for type annotations | | 🛠 | | U008 | SuperCallWithParameters | Use `super()` instead of `super(__class__, self)` | | 🛠 | +| D400 | DocstringEndsInNonPeriod | First line should end with a period | | | | D419 | EmptyDocstring | Docstring is empty | | | | M001 | UnusedNOQA | Unused `noqa` directive | | 🛠 | diff --git a/resources/test/fixtures/D400.py b/resources/test/fixtures/D400.py new file mode 100644 index 0000000000..37b8ae9e1c --- /dev/null +++ b/resources/test/fixtures/D400.py @@ -0,0 +1,27 @@ +def f1() -> None: + """Hello, world. + + More content here. + """ + + +def f2() -> None: + """Hello, world! + + More content here. + """ + + +def f3() -> None: + """Hello, world + + More content here. + """ + + +def f4() -> None: + """.""" + + +def f5() -> None: + """""" diff --git a/src/check_ast.rs b/src/check_ast.rs index 345b09511f..36de447568 100644 --- a/src/check_ast.rs +++ b/src/check_ast.rs @@ -1890,7 +1890,12 @@ impl<'a> Checker<'a> { fn check_docstrings(&mut self) { while let Some(docstring) = self.docstrings.pop() { - docstrings::docstring_empty(self, &docstring); + if self.settings.enabled.contains(&CheckCode::D400) { + docstrings::ends_with_period(self, &docstring); + } + if self.settings.enabled.contains(&CheckCode::D419) { + docstrings::not_empty(self, &docstring); + } } } diff --git a/src/checks.rs b/src/checks.rs index 7b5668408f..267ae9313e 100644 --- a/src/checks.rs +++ b/src/checks.rs @@ -151,6 +151,7 @@ pub enum CheckCode { U007, U008, // pydocstyle + D400, D419, // Meta M001, @@ -250,6 +251,7 @@ pub enum CheckKind { SuperCallWithParameters, // pydocstyle EmptyDocstring, + DocstringEndsInNonPeriod, // Meta UnusedNOQA(Option>), } @@ -359,6 +361,7 @@ impl CheckCode { CheckCode::U007 => CheckKind::UsePEP604Annotation, CheckCode::U008 => CheckKind::SuperCallWithParameters, // pydocstyle + CheckCode::D400 => CheckKind::DocstringEndsInNonPeriod, CheckCode::D419 => CheckKind::EmptyDocstring, // Meta CheckCode::M001 => CheckKind::UnusedNOQA(None), @@ -448,6 +451,7 @@ impl CheckKind { CheckKind::UselessObjectInheritance(_) => &CheckCode::U004, CheckKind::SuperCallWithParameters => &CheckCode::U008, // pydocstyle + CheckKind::DocstringEndsInNonPeriod => &CheckCode::D400, CheckKind::EmptyDocstring => &CheckCode::D419, // Meta CheckKind::UnusedNOQA(_) => &CheckCode::M001, @@ -693,6 +697,9 @@ impl CheckKind { "Use `super()` instead of `super(__class__, self)`".to_string() } // pydocstyle + CheckKind::DocstringEndsInNonPeriod => { + "First line should end with a period".to_string() + } CheckKind::EmptyDocstring => "Docstring is empty".to_string(), // Meta CheckKind::UnusedNOQA(codes) => match codes { diff --git a/src/docstrings.rs b/src/docstrings.rs index 40aea47515..72e519a4ee 100644 --- a/src/docstrings.rs +++ b/src/docstrings.rs @@ -61,8 +61,7 @@ pub fn extract<'a, 'b>( None } -pub fn docstring_empty(checker: &mut Checker, docstring: &Docstring) { - // Extract the source. +pub fn not_empty(checker: &mut Checker, docstring: &Docstring) { if let ExprKind::Constant { value: Constant::Str(string), .. @@ -76,3 +75,20 @@ pub fn docstring_empty(checker: &mut Checker, docstring: &Docstring) { } } } + +pub fn ends_with_period(checker: &mut Checker, docstring: &Docstring) { + if let ExprKind::Constant { + value: Constant::Str(string), + .. + } = &docstring.expr.node + { + if let Some(string) = string.lines().next() { + if !string.ends_with('.') { + checker.add_check(Check::new( + CheckKind::DocstringEndsInNonPeriod, + Range::from_located(docstring.expr), + )); + } + } + } +} diff --git a/src/linter.rs b/src/linter.rs index 584c5374f5..e6ee6fe2ad 100644 --- a/src/linter.rs +++ b/src/linter.rs @@ -978,6 +978,18 @@ mod tests { Ok(()) } + #[test] + fn d400() -> Result<()> { + let mut checks = check_path( + Path::new("./resources/test/fixtures/D400.py"), + &settings::Settings::for_rule(CheckCode::D400), + &fixer::Mode::Generate, + )?; + checks.sort_by_key(|check| check.location); + insta::assert_yaml_snapshot!(checks); + Ok(()) + } + #[test] fn d419() -> Result<()> { let mut checks = check_path( diff --git a/src/snapshots/ruff__linter__tests__d400.snap b/src/snapshots/ruff__linter__tests__d400.snap new file mode 100644 index 0000000000..79081a77d1 --- /dev/null +++ b/src/snapshots/ruff__linter__tests__d400.snap @@ -0,0 +1,21 @@ +--- +source: src/linter.rs +expression: checks +--- +- kind: DocstringEndsInPeriod + location: + row: 9 + column: 6 + end_location: + row: 12 + column: 8 + fix: ~ +- kind: DocstringEndsInPeriod + location: + row: 16 + column: 6 + end_location: + row: 19 + column: 8 + fix: ~ + diff --git a/src/snapshots/ruff__linter__tests__d400.snap.new b/src/snapshots/ruff__linter__tests__d400.snap.new new file mode 100644 index 0000000000..79f66a7c34 --- /dev/null +++ b/src/snapshots/ruff__linter__tests__d400.snap.new @@ -0,0 +1,22 @@ +--- +source: src/linter.rs +assertion_line: 989 +expression: checks +--- +- kind: DocstringEndsInNonPeriod + location: + row: 9 + column: 6 + end_location: + row: 12 + column: 8 + fix: ~ +- kind: DocstringEndsInNonPeriod + location: + row: 16 + column: 6 + end_location: + row: 19 + column: 8 + fix: ~ +