mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-26 20:10:09 +00:00
Port pydocstyle code 401 (ImperativeMood) (#1999)
This adds support for pydocstyle code D401 using the `imperative` crate.
This commit is contained in:
parent
81db00a3c4
commit
bea6deb0c3
13 changed files with 208 additions and 6 deletions
39
Cargo.lock
generated
39
Cargo.lock
generated
|
@ -926,6 +926,16 @@ dependencies = [
|
||||||
"winapi-util",
|
"winapi-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "imperative"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9f92123bf2fe0d9f1b5df1964727b970ca3b2d0203d47cf97fb1f36d856b6398"
|
||||||
|
dependencies = [
|
||||||
|
"phf 0.11.1",
|
||||||
|
"rust-stemmers",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "1.9.2"
|
version = "1.9.2"
|
||||||
|
@ -1511,6 +1521,15 @@ dependencies = [
|
||||||
"phf_shared 0.10.0",
|
"phf_shared 0.10.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf"
|
||||||
|
version = "0.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "928c6535de93548188ef63bb7c4036bd415cd8f36ad25af44b9789b2ee72a48c"
|
||||||
|
dependencies = [
|
||||||
|
"phf_shared 0.11.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "phf_codegen"
|
name = "phf_codegen"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
|
@ -1569,6 +1588,15 @@ dependencies = [
|
||||||
"siphasher",
|
"siphasher",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf_shared"
|
||||||
|
version = "0.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e1fb5f6f826b772a8d4c0394209441e7d37cbbb967ae9c7e0e8134365c9ee676"
|
||||||
|
dependencies = [
|
||||||
|
"siphasher",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pico-args"
|
name = "pico-args"
|
||||||
version = "0.4.2"
|
version = "0.4.2"
|
||||||
|
@ -1923,6 +1951,7 @@ dependencies = [
|
||||||
"glob",
|
"glob",
|
||||||
"globset",
|
"globset",
|
||||||
"ignore",
|
"ignore",
|
||||||
|
"imperative",
|
||||||
"insta",
|
"insta",
|
||||||
"itertools",
|
"itertools",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
|
@ -2027,6 +2056,16 @@ dependencies = [
|
||||||
"textwrap",
|
"textwrap",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rust-stemmers"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e46a2036019fdb888131db7a4c847a1063a7493f971ed94ea82c67eada63ca54"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-hash"
|
name = "rustc-hash"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
|
|
|
@ -35,6 +35,7 @@ fern = { version = "0.6.1" }
|
||||||
glob = { version = "0.3.0" }
|
glob = { version = "0.3.0" }
|
||||||
globset = { version = "0.4.9" }
|
globset = { version = "0.4.9" }
|
||||||
ignore = { version = "0.4.18" }
|
ignore = { version = "0.4.18" }
|
||||||
|
imperative = { version = "1.0.3" }
|
||||||
itertools = { version = "0.10.5" }
|
itertools = { version = "0.10.5" }
|
||||||
libcst = { git = "https://github.com/charliermarsh/LibCST", rev = "f2f0b7a487a8725d161fe8b3ed73a6758b21e177" }
|
libcst = { git = "https://github.com/charliermarsh/LibCST", rev = "f2f0b7a487a8725d161fe8b3ed73a6758b21e177" }
|
||||||
log = { version = "0.4.17" }
|
log = { version = "0.4.17" }
|
||||||
|
@ -59,9 +60,9 @@ smallvec = { version = "1.10.0" }
|
||||||
strum = { version = "0.24.1", features = ["strum_macros"] }
|
strum = { version = "0.24.1", features = ["strum_macros"] }
|
||||||
strum_macros = { version = "0.24.3" }
|
strum_macros = { version = "0.24.3" }
|
||||||
textwrap = { version = "0.16.0" }
|
textwrap = { version = "0.16.0" }
|
||||||
|
thiserror = { version = "1.0" }
|
||||||
titlecase = { version = "2.2.1" }
|
titlecase = { version = "2.2.1" }
|
||||||
toml_edit = { version = "0.17.1", features = ["easy"] }
|
toml_edit = { version = "0.17.1", features = ["easy"] }
|
||||||
thiserror = { version = "1.0" }
|
|
||||||
|
|
||||||
# https://docs.rs/getrandom/0.2.7/getrandom/#webassembly-support
|
# https://docs.rs/getrandom/0.2.7/getrandom/#webassembly-support
|
||||||
# For (future) wasm-pack support
|
# For (future) wasm-pack support
|
||||||
|
|
|
@ -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""" | |
|
| D300 | uses-triple-quotes | Use """triple double quotes""" | |
|
||||||
| D301 | uses-r-prefix-for-backslashed-content | Use r""" if any backslashes in a docstring | |
|
| 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 | 🛠 |
|
| 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 | |
|
| 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 | |
|
| 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" | |
|
| D404 | no-this-prefix | First word of the docstring should not be "This" | |
|
||||||
|
|
43
resources/test/fixtures/pydocstyle/D401.py
vendored
Normal file
43
resources/test/fixtures/pydocstyle/D401.py
vendored
Normal file
|
@ -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.
|
||||||
|
"""
|
|
@ -1277,6 +1277,7 @@
|
||||||
"D4",
|
"D4",
|
||||||
"D40",
|
"D40",
|
||||||
"D400",
|
"D400",
|
||||||
|
"D401",
|
||||||
"D402",
|
"D402",
|
||||||
"D403",
|
"D403",
|
||||||
"D404",
|
"D404",
|
||||||
|
|
|
@ -4497,6 +4497,7 @@ impl<'a> Checker<'a> {
|
||||||
.rules
|
.rules
|
||||||
.enabled(&Rule::UsesRPrefixForBackslashedContent)
|
.enabled(&Rule::UsesRPrefixForBackslashedContent)
|
||||||
|| self.settings.rules.enabled(&Rule::EndsInPeriod)
|
|| self.settings.rules.enabled(&Rule::EndsInPeriod)
|
||||||
|
|| self.settings.rules.enabled(&Rule::NonImperativeMood)
|
||||||
|| self.settings.rules.enabled(&Rule::NoSignature)
|
|| self.settings.rules.enabled(&Rule::NoSignature)
|
||||||
|| self.settings.rules.enabled(&Rule::FirstLineCapitalized)
|
|| self.settings.rules.enabled(&Rule::FirstLineCapitalized)
|
||||||
|| self.settings.rules.enabled(&Rule::NoThisPrefix)
|
|| self.settings.rules.enabled(&Rule::NoThisPrefix)
|
||||||
|
@ -4645,6 +4646,9 @@ impl<'a> Checker<'a> {
|
||||||
if self.settings.rules.enabled(&Rule::EndsInPeriod) {
|
if self.settings.rules.enabled(&Rule::EndsInPeriod) {
|
||||||
pydocstyle::rules::ends_with_period(self, &docstring);
|
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) {
|
if self.settings.rules.enabled(&Rule::NoSignature) {
|
||||||
pydocstyle::rules::no_signature(self, &docstring);
|
pydocstyle::rules::no_signature(self, &docstring);
|
||||||
}
|
}
|
||||||
|
|
|
@ -281,6 +281,7 @@ ruff_macros::define_rule_mapping!(
|
||||||
D300 => violations::UsesTripleQuotes,
|
D300 => violations::UsesTripleQuotes,
|
||||||
D301 => violations::UsesRPrefixForBackslashedContent,
|
D301 => violations::UsesRPrefixForBackslashedContent,
|
||||||
D400 => violations::EndsInPeriod,
|
D400 => violations::EndsInPeriod,
|
||||||
|
D401 => crate::rules::pydocstyle::rules::non_imperative_mood::NonImperativeMood,
|
||||||
D402 => violations::NoSignature,
|
D402 => violations::NoSignature,
|
||||||
D403 => violations::FirstLineCapitalized,
|
D403 => violations::FirstLineCapitalized,
|
||||||
D404 => violations::NoThisPrefix,
|
D404 => violations::NoThisPrefix,
|
||||||
|
|
|
@ -55,3 +55,11 @@ pub fn logical_line(content: &str) -> Option<usize> {
|
||||||
}
|
}
|
||||||
logical_line
|
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()
|
||||||
|
}
|
||||||
|
|
|
@ -44,6 +44,7 @@ mod tests {
|
||||||
#[test_case(Rule::UsesRPrefixForBackslashedContent, Path::new("D.py"); "D301")]
|
#[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("D.py"); "D400_0")]
|
||||||
#[test_case(Rule::EndsInPeriod, Path::new("D400.py"); "D400_1")]
|
#[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::NoSignature, Path::new("D.py"); "D402")]
|
||||||
#[test_case(Rule::FirstLineCapitalized, Path::new("D.py"); "D403")]
|
#[test_case(Rule::FirstLineCapitalized, Path::new("D.py"); "D403")]
|
||||||
#[test_case(Rule::NoThisPrefix, Path::new("D.py"); "D404")]
|
#[test_case(Rule::NoThisPrefix, Path::new("D.py"); "D404")]
|
||||||
|
|
|
@ -31,6 +31,7 @@ mod multi_line_summary_start;
|
||||||
mod newline_after_last_paragraph;
|
mod newline_after_last_paragraph;
|
||||||
mod no_signature;
|
mod no_signature;
|
||||||
mod no_surrounding_whitespace;
|
mod no_surrounding_whitespace;
|
||||||
|
pub mod non_imperative_mood;
|
||||||
mod not_empty;
|
mod not_empty;
|
||||||
mod not_missing;
|
mod not_missing;
|
||||||
mod one_liner;
|
mod one_liner;
|
||||||
|
|
50
src/rules/pydocstyle/rules/non_imperative_mood.rs
Normal file
50
src/rules/pydocstyle/rules/non_imperative_mood.rs
Normal file
|
@ -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}\"")
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ use crate::ast::types::Range;
|
||||||
use crate::checkers::ast::Checker;
|
use crate::checkers::ast::Checker;
|
||||||
use crate::docstrings::definition::Docstring;
|
use crate::docstrings::definition::Docstring;
|
||||||
use crate::registry::Diagnostic;
|
use crate::registry::Diagnostic;
|
||||||
|
use crate::rules::pydocstyle::helpers::normalize_word;
|
||||||
use crate::violations;
|
use crate::violations;
|
||||||
|
|
||||||
/// D404
|
/// D404
|
||||||
|
@ -16,11 +17,7 @@ pub fn starts_with_this(checker: &mut Checker, docstring: &Docstring) {
|
||||||
let Some(first_word) = body.split(' ').next() else {
|
let Some(first_word) = body.split(' ').next() else {
|
||||||
return
|
return
|
||||||
};
|
};
|
||||||
if first_word
|
if normalize_word(first_word) != "this" {
|
||||||
.replace(|c: char| !c.is_alphanumeric(), "")
|
|
||||||
.to_lowercase()
|
|
||||||
!= "this"
|
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
checker.diagnostics.push(Diagnostic::new(
|
checker.diagnostics.push(Diagnostic::new(
|
||||||
|
|
|
@ -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: ~
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue