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-comprehensions`](https://pypi.org/project/flake8-comprehensions/) (12/16)
- [`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)
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 | | |
| 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` | | |
| 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 | | |
| D201 | NoBlankLineBeforeFunction | No blank lines allowed before 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) {
docstrings::capitalized(self, &docstring);
}
if self.settings.enabled.contains(&CheckCode::D404) {
docstrings::starts_with_this(self, &docstring);
}
if self.settings.enabled.contains(&CheckCode::D415) {
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,
D402,
D403,
D404,
D415,
D418,
D419,
D201,
D202,
@ -279,27 +281,29 @@ pub enum CheckKind {
EndsInPunctuation,
FirstLineCapitalized,
FitsOnOneLine,
MagicMethod,
MultiLineSummaryFirstLine,
MultiLineSummarySecondLine,
NewLineAfterLastParagraph,
NoBlankLineAfterSummary,
NoSurroundingWhitespace,
NonEmpty,
UsesTripleQuotes,
NoSignature,
NoBlankLineBeforeFunction(usize),
NoBlankLineAfterFunction(usize),
NoBlankLineAfterSummary,
NoBlankLineBeforeClass(usize),
OneBlankLineBeforeClass(usize),
NoBlankLineBeforeFunction(usize),
NoSignature,
NoSurroundingWhitespace,
NoThisPrefix,
NonEmpty,
OneBlankLineAfterClass(usize),
PublicModule,
OneBlankLineBeforeClass(usize),
PublicClass,
PublicMethod,
PublicFunction,
PublicPackage,
MagicMethod,
PublicNestedClass,
PublicInit,
PublicMethod,
PublicModule,
PublicNestedClass,
PublicPackage,
SkipDocstring,
UsesTripleQuotes,
// Meta
UnusedNOQA(Option<Vec<String>>),
}
@ -422,22 +426,24 @@ impl CheckCode {
CheckCode::D106 => CheckKind::PublicNestedClass,
CheckCode::D107 => CheckKind::PublicInit,
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::D209 => CheckKind::NewLineAfterLastParagraph,
CheckCode::D210 => CheckKind::NoSurroundingWhitespace,
CheckCode::D400 => CheckKind::EndsInPeriod,
CheckCode::D419 => CheckKind::NonEmpty,
CheckCode::D211 => CheckKind::NoBlankLineBeforeClass(1),
CheckCode::D212 => CheckKind::MultiLineSummaryFirstLine,
CheckCode::D213 => CheckKind::MultiLineSummarySecondLine,
CheckCode::D300 => CheckKind::UsesTripleQuotes,
CheckCode::D400 => CheckKind::EndsInPeriod,
CheckCode::D402 => CheckKind::NoSignature,
CheckCode::D403 => CheckKind::FirstLineCapitalized,
CheckCode::D404 => CheckKind::NoThisPrefix,
CheckCode::D415 => CheckKind::EndsInPunctuation,
CheckCode::D201 => CheckKind::NoBlankLineBeforeFunction(1),
CheckCode::D202 => CheckKind::NoBlankLineAfterFunction(1),
CheckCode::D211 => CheckKind::NoBlankLineBeforeClass(1),
CheckCode::D203 => CheckKind::OneBlankLineBeforeClass(0),
CheckCode::D204 => CheckKind::OneBlankLineAfterClass(0),
CheckCode::D418 => CheckKind::SkipDocstring,
CheckCode::D419 => CheckKind::NonEmpty,
// Meta
CheckCode::M001 => CheckKind::UnusedNOQA(None),
}
@ -527,31 +533,33 @@ impl CheckKind {
CheckKind::UselessObjectInheritance(_) => &CheckCode::U004,
CheckKind::SuperCallWithParameters => &CheckCode::U008,
// 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::NonEmpty => &CheckCode::D419,
CheckKind::EndsInPunctuation => &CheckCode::D415,
CheckKind::FirstLineCapitalized => &CheckCode::D403,
CheckKind::FitsOnOneLine => &CheckCode::D200,
CheckKind::MagicMethod => &CheckCode::D105,
CheckKind::MultiLineSummaryFirstLine => &CheckCode::D212,
CheckKind::MultiLineSummarySecondLine => &CheckCode::D213,
CheckKind::UsesTripleQuotes => &CheckCode::D300,
CheckKind::NoSignature => &CheckCode::D402,
CheckKind::FirstLineCapitalized => &CheckCode::D403,
CheckKind::EndsInPunctuation => &CheckCode::D415,
CheckKind::NoBlankLineBeforeFunction(_) => &CheckCode::D201,
CheckKind::NewLineAfterLastParagraph => &CheckCode::D209,
CheckKind::NoBlankLineAfterFunction(_) => &CheckCode::D202,
CheckKind::NoBlankLineAfterSummary => &CheckCode::D205,
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::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
CheckKind::UnusedNOQA(_) => &CheckCode::M001,
}
@ -851,6 +859,12 @@ impl CheckKind {
CheckKind::MagicMethod => "Missing docstring in magic method".to_string(),
CheckKind::PublicNestedClass => "Missing docstring in public nested class".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
CheckKind::UnusedNOQA(codes) => match codes {
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 regex::Regex;
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
pub fn ends_with_punctuation(checker: &mut Checker, definition: &Definition) {
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
pub fn not_empty(checker: &mut Checker, definition: &Definition) -> bool {
if let Some(docstring) = definition.docstring {

View file

@ -1284,6 +1284,18 @@ mod tests {
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]
fn d415() -> Result<()> {
let mut checks = check_path(
@ -1296,6 +1308,18 @@ mod tests {
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]
fn d419() -> Result<()> {
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 crate::ast::helpers::match_name_or_attr;
@ -24,6 +26,7 @@ pub struct VisibleScope {
pub visibility: Visibility,
}
/// Returns `true` if a function definition is an `@overload`.
pub fn is_overload(stmt: &Stmt) -> bool {
match &stmt.node {
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 {
match &stmt.node {
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 {
match &stmt.node {
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("__"))
}
pub fn module_visibility(path: &Path) -> Visibility {
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;
}
}
@ -115,6 +121,9 @@ fn class_visibility(stmt: &Stmt) -> Visibility {
}
/// 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 {
match kind {
Documentable::Function => VisibleScope {