Skip to content

Commit

Permalink
Port pydocstyle code 401 (ImperativeMood) (#1999)
Browse files Browse the repository at this point in the history
This adds support for pydocstyle code D401 using the `imperative` crate.
  • Loading branch information
akx committed Jan 20, 2023
1 parent 81db00a commit bea6deb
Show file tree
Hide file tree
Showing 13 changed files with 208 additions and 6 deletions.
39 changes: 39 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ fern = { version = "0.6.1" }
glob = { version = "0.3.0" }
globset = { version = "0.4.9" }
ignore = { version = "0.4.18" }
imperative = { version = "1.0.3" }
itertools = { version = "0.10.5" }
libcst = { git = "https://github.com/charliermarsh/LibCST", rev = "f2f0b7a487a8725d161fe8b3ed73a6758b21e177" }
log = { version = "0.4.17" }
Expand All @@ -59,9 +60,9 @@ smallvec = { version = "1.10.0" }
strum = { version = "0.24.1", features = ["strum_macros"] }
strum_macros = { version = "0.24.3" }
textwrap = { version = "0.16.0" }
thiserror = { version = "1.0" }
titlecase = { version = "2.2.1" }
toml_edit = { version = "0.17.1", features = ["easy"] }
thiserror = { version = "1.0" }

# https://docs.rs/getrandom/0.2.7/getrandom/#webassembly-support
# For (future) wasm-pack support
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,7 @@ For more, see [pydocstyle](https://pypi.org/project/pydocstyle/6.1.1/) on PyPI.
| D300 | uses-triple-quotes | Use """triple double quotes""" | |
| D301 | uses-r-prefix-for-backslashed-content | Use r""" if any backslashes in a docstring | |
| D400 | ends-in-period | First line should end with a period | 🛠 |
| D401 | non-imperative-mood | First line of docstring should be in imperative mood: "{first_line}" | |
| D402 | no-signature | First line should not be the function's signature | |
| D403 | first-line-capitalized | First word of the first line should be properly capitalized | |
| D404 | no-this-prefix | First word of the docstring should not be "This" | |
Expand Down
43 changes: 43 additions & 0 deletions resources/test/fixtures/pydocstyle/D401.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Bad examples

def bad_liouiwnlkjl():
"""Returns foo."""


def bad_sdgfsdg23245():
"""Constructor for a foo."""


def bad_sdgfsdg23245777():
"""
Constructor for a boa.
"""


def bad_run_something():
"""Runs something"""
pass


def multi_line():
"""Writes a logical line that
extends to two physical lines.
"""


# Good examples

def good_run_something():
"""Run away."""


def good_construct():
"""Construct a beautiful house."""


def good_multi_line():
"""Write a logical line that
extends to two physical lines.
"""
1 change: 1 addition & 0 deletions ruff.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1277,6 +1277,7 @@
"D4",
"D40",
"D400",
"D401",
"D402",
"D403",
"D404",
Expand Down
4 changes: 4 additions & 0 deletions src/checkers/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4497,6 +4497,7 @@ impl<'a> Checker<'a> {
.rules
.enabled(&Rule::UsesRPrefixForBackslashedContent)
|| self.settings.rules.enabled(&Rule::EndsInPeriod)
|| self.settings.rules.enabled(&Rule::NonImperativeMood)
|| self.settings.rules.enabled(&Rule::NoSignature)
|| self.settings.rules.enabled(&Rule::FirstLineCapitalized)
|| self.settings.rules.enabled(&Rule::NoThisPrefix)
Expand Down Expand Up @@ -4645,6 +4646,9 @@ impl<'a> Checker<'a> {
if self.settings.rules.enabled(&Rule::EndsInPeriod) {
pydocstyle::rules::ends_with_period(self, &docstring);
}
if self.settings.rules.enabled(&Rule::NonImperativeMood) {
pydocstyle::rules::non_imperative_mood::non_imperative_mood(self, &docstring);
}
if self.settings.rules.enabled(&Rule::NoSignature) {
pydocstyle::rules::no_signature(self, &docstring);
}
Expand Down
1 change: 1 addition & 0 deletions src/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ ruff_macros::define_rule_mapping!(
D300 => violations::UsesTripleQuotes,
D301 => violations::UsesRPrefixForBackslashedContent,
D400 => violations::EndsInPeriod,
D401 => crate::rules::pydocstyle::rules::non_imperative_mood::NonImperativeMood,
D402 => violations::NoSignature,
D403 => violations::FirstLineCapitalized,
D404 => violations::NoThisPrefix,
Expand Down
8 changes: 8 additions & 0 deletions src/rules/pydocstyle/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,11 @@ pub fn logical_line(content: &str) -> Option<usize> {
}
logical_line
}

/// Normalize a word by removing all non-alphanumeric characters
/// and converting it to lowercase.
pub fn normalize_word(first_word: &str) -> String {
first_word
.replace(|c: char| !c.is_alphanumeric(), "")
.to_lowercase()
}
1 change: 1 addition & 0 deletions src/rules/pydocstyle/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ mod tests {
#[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")]
Expand Down
1 change: 1 addition & 0 deletions src/rules/pydocstyle/rules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ mod multi_line_summary_start;
mod newline_after_last_paragraph;
mod no_signature;
mod no_surrounding_whitespace;
pub mod non_imperative_mood;
mod not_empty;
mod not_missing;
mod one_liner;
Expand Down
50 changes: 50 additions & 0 deletions src/rules/pydocstyle/rules/non_imperative_mood.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
use imperative::Mood;
use once_cell::sync::Lazy;
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::helpers::normalize_word;
use crate::violation::Violation;

static MOOD: Lazy<Mood> = Lazy::new(Mood::new);

/// D401
pub fn non_imperative_mood(checker: &mut Checker, docstring: &Docstring) {
let body = docstring.body;

// Find first line, disregarding whitespace.
let line = match body.trim().lines().next() {
Some(line) => line.trim(),
None => return,
};
// Find the first word on that line and normalize it to lower-case.
let first_word_norm = match line.split_whitespace().next() {
Some(word) => normalize_word(word),
None => return,
};
if first_word_norm.is_empty() {
return;
}
if let Some(false) = MOOD.is_imperative(&first_word_norm) {
let diagnostic = Diagnostic::new(
NonImperativeMood(line.to_string()),
Range::from_located(docstring.expr),
);
checker.diagnostics.push(diagnostic);
}
}

define_violation!(
pub struct NonImperativeMood(pub String);
);
impl Violation for NonImperativeMood {
#[derive_message_formats]
fn message(&self) -> String {
let NonImperativeMood(first_line) = self;
format!("First line of docstring should be in imperative mood: \"{first_line}\"")
}
}
7 changes: 2 additions & 5 deletions src/rules/pydocstyle/rules/starts_with_this.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::docstrings::definition::Docstring;
use crate::registry::Diagnostic;
use crate::rules::pydocstyle::helpers::normalize_word;
use crate::violations;

/// D404
Expand All @@ -16,11 +17,7 @@ pub fn starts_with_this(checker: &mut Checker, docstring: &Docstring) {
let Some(first_word) = body.split(' ').next() else {
return
};
if first_word
.replace(|c: char| !c.is_alphanumeric(), "")
.to_lowercase()
!= "this"
{
if normalize_word(first_word) != "this" {
return;
}
checker.diagnostics.push(Diagnostic::new(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
---
source: src/rules/pydocstyle/mod.rs
expression: diagnostics
---
- kind:
NonImperativeMood: Returns foo.
location:
row: 4
column: 4
end_location:
row: 4
column: 22
fix: ~
parent: ~
- kind:
NonImperativeMood: Constructor for a foo.
location:
row: 8
column: 4
end_location:
row: 8
column: 32
fix: ~
parent: ~
- kind:
NonImperativeMood: Constructor for a boa.
location:
row: 12
column: 4
end_location:
row: 16
column: 7
fix: ~
parent: ~
- kind:
NonImperativeMood: Runs something
location:
row: 20
column: 4
end_location:
row: 20
column: 24
fix: ~
parent: ~
- kind:
NonImperativeMood: Writes a logical line that
location:
row: 25
column: 4
end_location:
row: 27
column: 7
fix: ~
parent: ~

0 comments on commit bea6deb

Please sign in to comment.