Implement D404 and D418 for pydocstyle (#409)

This commit is contained in:
Charlie Marsh 2022-10-12 13:20:55 -04:00 committed by GitHub
parent e08e1caf71
commit 77055faab6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 176 additions and 41 deletions

View file

@ -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-print`](https://pypi.org/project/flake8-print/)
- [`flake8-comprehensions`](https://pypi.org/project/flake8-comprehensions/) (12/16) - [`flake8-comprehensions`](https://pypi.org/project/flake8-comprehensions/) (12/16)
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (3/32) - [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (3/32)
- [`flake8-docstrings`](https://pypi.org/project/flake8-docstrings/) (25/48) - [`flake8-docstrings`](https://pypi.org/project/flake8-docstrings/) (27/48)
- [`pyupgrade`](https://pypi.org/project/pyupgrade/) (8/34) - [`pyupgrade`](https://pypi.org/project/pyupgrade/) (8/34)
Beyond rule-set parity, ruff suffers from the following limitations vis-à-vis Flake8: Beyond rule-set parity, ruff suffers from the following limitations vis-à-vis Flake8:
@ -323,7 +323,9 @@ The 🛠 emoji indicates that a rule is automatically fixable by the `--fix` com
| D400 | EndsInPeriod | First line should end with a period | | | | D400 | EndsInPeriod | First line should end with a period | | |
| D402 | NoSignature | First line should not be the function's 'signature' | | | | D402 | NoSignature | First line should not be the function's 'signature' | | |
| D403 | FirstLineCapitalized | First word of the first line should be properly capitalized | | | | D403 | FirstLineCapitalized | First word of the first line should be properly capitalized | | |
| D404 | NoThisPrefix | First word of the docstring should not be `This` | | |
| D415 | EndsInPunctuation | First line should end with a period, question mark, or exclamation point | | | | D415 | EndsInPunctuation | First line should end with a period, question mark, or exclamation point | | |
| D418 | SkipDocstring | Function decorated with @overload shouldn't contain a docstring | | |
| D419 | NonEmpty | Docstring is empty | | | | D419 | NonEmpty | Docstring is empty | | |
| D201 | NoBlankLineBeforeFunction | No blank lines allowed before function docstring (found 1) | | | | D201 | NoBlankLineBeforeFunction | No blank lines allowed before function docstring (found 1) | | |
| D202 | NoBlankLineAfterFunction | No blank lines allowed after function docstring (found 1) | | | | D202 | NoBlankLineAfterFunction | No blank lines allowed after function docstring (found 1) | | |

View file

@ -1955,9 +1955,15 @@ impl<'a> Checker<'a> {
if self.settings.enabled.contains(&CheckCode::D403) { if self.settings.enabled.contains(&CheckCode::D403) {
docstrings::capitalized(self, &docstring); docstrings::capitalized(self, &docstring);
} }
if self.settings.enabled.contains(&CheckCode::D404) {
docstrings::starts_with_this(self, &docstring);
}
if self.settings.enabled.contains(&CheckCode::D415) { if self.settings.enabled.contains(&CheckCode::D415) {
docstrings::ends_with_punctuation(self, &docstring); docstrings::ends_with_punctuation(self, &docstring);
} }
if self.settings.enabled.contains(&CheckCode::D418) {
docstrings::if_needed(self, &docstring);
}
} }
} }

View file

@ -170,7 +170,9 @@ pub enum CheckCode {
D400, D400,
D402, D402,
D403, D403,
D404,
D415, D415,
D418,
D419, D419,
D201, D201,
D202, D202,
@ -279,27 +281,29 @@ pub enum CheckKind {
EndsInPunctuation, EndsInPunctuation,
FirstLineCapitalized, FirstLineCapitalized,
FitsOnOneLine, FitsOnOneLine,
MagicMethod,
MultiLineSummaryFirstLine, MultiLineSummaryFirstLine,
MultiLineSummarySecondLine, MultiLineSummarySecondLine,
NewLineAfterLastParagraph, NewLineAfterLastParagraph,
NoBlankLineAfterSummary,
NoSurroundingWhitespace,
NonEmpty,
UsesTripleQuotes,
NoSignature,
NoBlankLineBeforeFunction(usize),
NoBlankLineAfterFunction(usize), NoBlankLineAfterFunction(usize),
NoBlankLineAfterSummary,
NoBlankLineBeforeClass(usize), NoBlankLineBeforeClass(usize),
OneBlankLineBeforeClass(usize), NoBlankLineBeforeFunction(usize),
NoSignature,
NoSurroundingWhitespace,
NoThisPrefix,
NonEmpty,
OneBlankLineAfterClass(usize), OneBlankLineAfterClass(usize),
PublicModule, OneBlankLineBeforeClass(usize),
PublicClass, PublicClass,
PublicMethod,
PublicFunction, PublicFunction,
PublicPackage,
MagicMethod,
PublicNestedClass,
PublicInit, PublicInit,
PublicMethod,
PublicModule,
PublicNestedClass,
PublicPackage,
SkipDocstring,
UsesTripleQuotes,
// Meta // Meta
UnusedNOQA(Option<Vec<String>>), UnusedNOQA(Option<Vec<String>>),
} }
@ -422,22 +426,24 @@ impl CheckCode {
CheckCode::D106 => CheckKind::PublicNestedClass, CheckCode::D106 => CheckKind::PublicNestedClass,
CheckCode::D107 => CheckKind::PublicInit, CheckCode::D107 => CheckKind::PublicInit,
CheckCode::D200 => CheckKind::FitsOnOneLine, CheckCode::D200 => CheckKind::FitsOnOneLine,
CheckCode::D201 => CheckKind::NoBlankLineBeforeFunction(1),
CheckCode::D202 => CheckKind::NoBlankLineAfterFunction(1),
CheckCode::D203 => CheckKind::OneBlankLineBeforeClass(0),
CheckCode::D204 => CheckKind::OneBlankLineAfterClass(0),
CheckCode::D205 => CheckKind::NoBlankLineAfterSummary, CheckCode::D205 => CheckKind::NoBlankLineAfterSummary,
CheckCode::D209 => CheckKind::NewLineAfterLastParagraph, CheckCode::D209 => CheckKind::NewLineAfterLastParagraph,
CheckCode::D210 => CheckKind::NoSurroundingWhitespace, CheckCode::D210 => CheckKind::NoSurroundingWhitespace,
CheckCode::D400 => CheckKind::EndsInPeriod, CheckCode::D211 => CheckKind::NoBlankLineBeforeClass(1),
CheckCode::D419 => CheckKind::NonEmpty,
CheckCode::D212 => CheckKind::MultiLineSummaryFirstLine, CheckCode::D212 => CheckKind::MultiLineSummaryFirstLine,
CheckCode::D213 => CheckKind::MultiLineSummarySecondLine, CheckCode::D213 => CheckKind::MultiLineSummarySecondLine,
CheckCode::D300 => CheckKind::UsesTripleQuotes, CheckCode::D300 => CheckKind::UsesTripleQuotes,
CheckCode::D400 => CheckKind::EndsInPeriod,
CheckCode::D402 => CheckKind::NoSignature, CheckCode::D402 => CheckKind::NoSignature,
CheckCode::D403 => CheckKind::FirstLineCapitalized, CheckCode::D403 => CheckKind::FirstLineCapitalized,
CheckCode::D404 => CheckKind::NoThisPrefix,
CheckCode::D415 => CheckKind::EndsInPunctuation, CheckCode::D415 => CheckKind::EndsInPunctuation,
CheckCode::D201 => CheckKind::NoBlankLineBeforeFunction(1), CheckCode::D418 => CheckKind::SkipDocstring,
CheckCode::D202 => CheckKind::NoBlankLineAfterFunction(1), CheckCode::D419 => CheckKind::NonEmpty,
CheckCode::D211 => CheckKind::NoBlankLineBeforeClass(1),
CheckCode::D203 => CheckKind::OneBlankLineBeforeClass(0),
CheckCode::D204 => CheckKind::OneBlankLineAfterClass(0),
// Meta // Meta
CheckCode::M001 => CheckKind::UnusedNOQA(None), CheckCode::M001 => CheckKind::UnusedNOQA(None),
} }
@ -527,31 +533,33 @@ impl CheckKind {
CheckKind::UselessObjectInheritance(_) => &CheckCode::U004, CheckKind::UselessObjectInheritance(_) => &CheckCode::U004,
CheckKind::SuperCallWithParameters => &CheckCode::U008, CheckKind::SuperCallWithParameters => &CheckCode::U008,
// pydocstyle // pydocstyle
CheckKind::PublicModule => &CheckCode::D100,
CheckKind::PublicClass => &CheckCode::D101,
CheckKind::PublicMethod => &CheckCode::D102,
CheckKind::PublicFunction => &CheckCode::D103,
CheckKind::PublicPackage => &CheckCode::D104,
CheckKind::MagicMethod => &CheckCode::D105,
CheckKind::PublicNestedClass => &CheckCode::D106,
CheckKind::PublicInit => &CheckCode::D107,
CheckKind::FitsOnOneLine => &CheckCode::D200,
CheckKind::NoBlankLineAfterSummary => &CheckCode::D205,
CheckKind::NewLineAfterLastParagraph => &CheckCode::D209,
CheckKind::NoSurroundingWhitespace => &CheckCode::D210,
CheckKind::EndsInPeriod => &CheckCode::D400, CheckKind::EndsInPeriod => &CheckCode::D400,
CheckKind::NonEmpty => &CheckCode::D419, CheckKind::EndsInPunctuation => &CheckCode::D415,
CheckKind::FirstLineCapitalized => &CheckCode::D403,
CheckKind::FitsOnOneLine => &CheckCode::D200,
CheckKind::MagicMethod => &CheckCode::D105,
CheckKind::MultiLineSummaryFirstLine => &CheckCode::D212, CheckKind::MultiLineSummaryFirstLine => &CheckCode::D212,
CheckKind::MultiLineSummarySecondLine => &CheckCode::D213, CheckKind::MultiLineSummarySecondLine => &CheckCode::D213,
CheckKind::UsesTripleQuotes => &CheckCode::D300, CheckKind::NewLineAfterLastParagraph => &CheckCode::D209,
CheckKind::NoSignature => &CheckCode::D402,
CheckKind::FirstLineCapitalized => &CheckCode::D403,
CheckKind::EndsInPunctuation => &CheckCode::D415,
CheckKind::NoBlankLineBeforeFunction(_) => &CheckCode::D201,
CheckKind::NoBlankLineAfterFunction(_) => &CheckCode::D202, CheckKind::NoBlankLineAfterFunction(_) => &CheckCode::D202,
CheckKind::NoBlankLineAfterSummary => &CheckCode::D205,
CheckKind::NoBlankLineBeforeClass(_) => &CheckCode::D211, CheckKind::NoBlankLineBeforeClass(_) => &CheckCode::D211,
CheckKind::OneBlankLineBeforeClass(_) => &CheckCode::D203, CheckKind::NoBlankLineBeforeFunction(_) => &CheckCode::D201,
CheckKind::NoSignature => &CheckCode::D402,
CheckKind::NoSurroundingWhitespace => &CheckCode::D210,
CheckKind::NoThisPrefix => &CheckCode::D404,
CheckKind::NonEmpty => &CheckCode::D419,
CheckKind::OneBlankLineAfterClass(_) => &CheckCode::D204, CheckKind::OneBlankLineAfterClass(_) => &CheckCode::D204,
CheckKind::OneBlankLineBeforeClass(_) => &CheckCode::D203,
CheckKind::PublicClass => &CheckCode::D101,
CheckKind::PublicFunction => &CheckCode::D103,
CheckKind::PublicInit => &CheckCode::D107,
CheckKind::PublicMethod => &CheckCode::D102,
CheckKind::PublicModule => &CheckCode::D100,
CheckKind::PublicNestedClass => &CheckCode::D106,
CheckKind::PublicPackage => &CheckCode::D104,
CheckKind::SkipDocstring => &CheckCode::D418,
CheckKind::UsesTripleQuotes => &CheckCode::D300,
// Meta // Meta
CheckKind::UnusedNOQA(_) => &CheckCode::M001, CheckKind::UnusedNOQA(_) => &CheckCode::M001,
} }
@ -851,6 +859,12 @@ impl CheckKind {
CheckKind::MagicMethod => "Missing docstring in magic method".to_string(), CheckKind::MagicMethod => "Missing docstring in magic method".to_string(),
CheckKind::PublicNestedClass => "Missing docstring in public nested class".to_string(), CheckKind::PublicNestedClass => "Missing docstring in public nested class".to_string(),
CheckKind::PublicInit => "Missing docstring in __init__".to_string(), CheckKind::PublicInit => "Missing docstring in __init__".to_string(),
CheckKind::NoThisPrefix => {
"First word of the docstring should not be `This`".to_string()
}
CheckKind::SkipDocstring => {
"Function decorated with @overload shouldn't contain a docstring".to_string()
}
// Meta // Meta
CheckKind::UnusedNOQA(codes) => match codes { CheckKind::UnusedNOQA(codes) => match codes {
None => "Unused `noqa` directive".to_string(), None => "Unused `noqa` directive".to_string(),

View file

@ -1,3 +1,5 @@
//! Abstractions for tracking and validationg docstrings in Python code.
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use regex::Regex; use regex::Regex;
use rustpython_ast::{Constant, Expr, ExprKind, Location, Stmt, StmtKind}; use rustpython_ast::{Constant, Expr, ExprKind, Location, Stmt, StmtKind};
@ -593,6 +595,32 @@ pub fn capitalized(checker: &mut Checker, definition: &Definition) {
} }
} }
/// D404
pub fn starts_with_this(checker: &mut Checker, definition: &Definition) {
if let Some(docstring) = definition.docstring {
if let ExprKind::Constant {
value: Constant::Str(string),
..
} = &docstring.node
{
let trimmed = string.trim();
if trimmed.is_empty() {
return;
}
if let Some(first_word) = string.split(' ').next() {
if first_word
.replace(|c: char| !c.is_alphanumeric(), "")
.to_lowercase()
== "this"
{
checker.add_check(Check::new(CheckKind::NoThisPrefix, range_for(docstring)));
}
}
}
}
}
/// D415 /// D415
pub fn ends_with_punctuation(checker: &mut Checker, definition: &Definition) { pub fn ends_with_punctuation(checker: &mut Checker, definition: &Definition) {
if let Some(docstring) = definition.docstring { if let Some(docstring) = definition.docstring {
@ -613,6 +641,23 @@ pub fn ends_with_punctuation(checker: &mut Checker, definition: &Definition) {
} }
} }
/// D418
pub fn if_needed(checker: &mut Checker, definition: &Definition) {
if definition.docstring.is_some() {
if let DefinitionKind::Function(stmt)
| DefinitionKind::NestedFunction(stmt)
| DefinitionKind::Method(stmt) = definition.kind
{
if is_overload(stmt) {
checker.add_check(Check::new(
CheckKind::SkipDocstring,
Range::from_located(stmt),
));
}
}
}
}
/// D419 /// D419
pub fn not_empty(checker: &mut Checker, definition: &Definition) -> bool { pub fn not_empty(checker: &mut Checker, definition: &Definition) -> bool {
if let Some(docstring) = definition.docstring { if let Some(docstring) = definition.docstring {

View file

@ -1284,6 +1284,18 @@ mod tests {
Ok(()) Ok(())
} }
#[test]
fn d404() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/D.py"),
&settings::Settings::for_rule(CheckCode::D404),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test] #[test]
fn d415() -> Result<()> { fn d415() -> Result<()> {
let mut checks = check_path( let mut checks = check_path(
@ -1296,6 +1308,18 @@ mod tests {
Ok(()) Ok(())
} }
#[test]
fn d418() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/D.py"),
&settings::Settings::for_rule(CheckCode::D418),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test] #[test]
fn d419() -> Result<()> { fn d419() -> Result<()> {
let mut checks = check_path( let mut checks = check_path(

View file

@ -0,0 +1,6 @@
---
source: src/linter.rs
expression: checks
---
[]

View file

@ -0,0 +1,29 @@
---
source: src/linter.rs
expression: checks
---
- kind: SkipDocstring
location:
row: 33
column: 5
end_location:
row: 37
column: 5
fix: ~
- kind: SkipDocstring
location:
row: 85
column: 5
end_location:
row: 89
column: 5
fix: ~
- kind: SkipDocstring
location:
row: 105
column: 1
end_location:
row: 110
column: 1
fix: ~

View file

@ -1,3 +1,5 @@
//! Abstractions for tracking public and private visibility across modules, classes, and functions.
use std::path::Path; use std::path::Path;
use crate::ast::helpers::match_name_or_attr; use crate::ast::helpers::match_name_or_attr;
@ -24,6 +26,7 @@ pub struct VisibleScope {
pub visibility: Visibility, pub visibility: Visibility,
} }
/// Returns `true` if a function definition is an `@overload`.
pub fn is_overload(stmt: &Stmt) -> bool { pub fn is_overload(stmt: &Stmt) -> bool {
match &stmt.node { match &stmt.node {
StmtKind::FunctionDef { decorator_list, .. } StmtKind::FunctionDef { decorator_list, .. }
@ -34,6 +37,7 @@ pub fn is_overload(stmt: &Stmt) -> bool {
} }
} }
/// Returns `true` if a function is a "magic method".
pub fn is_magic(stmt: &Stmt) -> bool { pub fn is_magic(stmt: &Stmt) -> bool {
match &stmt.node { match &stmt.node {
StmtKind::FunctionDef { name, .. } | StmtKind::AsyncFunctionDef { name, .. } => { StmtKind::FunctionDef { name, .. } | StmtKind::AsyncFunctionDef { name, .. } => {
@ -47,6 +51,7 @@ pub fn is_magic(stmt: &Stmt) -> bool {
} }
} }
/// Returns `true` if a function is an `__init__`.
pub fn is_init(stmt: &Stmt) -> bool { pub fn is_init(stmt: &Stmt) -> bool {
match &stmt.node { match &stmt.node {
StmtKind::FunctionDef { name, .. } | StmtKind::AsyncFunctionDef { name, .. } => { StmtKind::FunctionDef { name, .. } | StmtKind::AsyncFunctionDef { name, .. } => {
@ -56,13 +61,14 @@ pub fn is_init(stmt: &Stmt) -> bool {
} }
} }
fn is_private_name(module_name: &str) -> bool { /// Returns `true` if a module name indicates private visibility.
fn is_private_module(module_name: &str) -> bool {
module_name.starts_with('_') || (module_name.starts_with("__") && module_name.ends_with("__")) module_name.starts_with('_') || (module_name.starts_with("__") && module_name.ends_with("__"))
} }
pub fn module_visibility(path: &Path) -> Visibility { pub fn module_visibility(path: &Path) -> Visibility {
for component in path.iter().rev() { for component in path.iter().rev() {
if is_private_name(&component.to_string_lossy()) { if is_private_module(&component.to_string_lossy()) {
return Visibility::Private; return Visibility::Private;
} }
} }
@ -115,6 +121,9 @@ fn class_visibility(stmt: &Stmt) -> Visibility {
} }
/// Transition a `VisibleScope` based on a new `Documentable` definition. /// Transition a `VisibleScope` based on a new `Documentable` definition.
///
/// `scope` is the current `VisibleScope`, while `Documentable` and `Stmt` describe the current
/// node used to modify visibility.
pub fn transition_scope(scope: &VisibleScope, stmt: &Stmt, kind: &Documentable) -> VisibleScope { pub fn transition_scope(scope: &VisibleScope, stmt: &Stmt, kind: &Documentable) -> VisibleScope {
match kind { match kind {
Documentable::Function => VisibleScope { Documentable::Function => VisibleScope {