Re-arrange some docstring modules (#428)

This commit is contained in:
Charlie Marsh 2022-10-14 12:34:35 -04:00 committed by GitHub
parent b64040cbb2
commit 6407fd5a33
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 519 additions and 447 deletions

View file

@ -21,13 +21,13 @@ use crate::ast::visitor::{walk_excepthandler, Visitor};
use crate::ast::{checkers, helpers, operations, visitor};
use crate::autofix::{fixer, fixes};
use crate::checks::{Check, CheckCode, CheckKind};
use crate::docstrings::docstring_checks;
use crate::docstrings::docstring_plugins;
use crate::docstrings::types::{Definition, DefinitionKind, Documentable};
use crate::plugins;
use crate::python::builtins::{BUILTINS, MAGIC_GLOBALS};
use crate::python::future::ALL_FEATURE_NAMES;
use crate::settings::{PythonVersion, Settings};
use crate::visibility::{module_visibility, transition_scope, Modifier, Visibility, VisibleScope};
use crate::{docstrings, plugins};
pub const GLOBAL_SCOPE_INDEX: usize = 0;
@ -572,7 +572,7 @@ where
let prev_visibile_scope = self.visible_scope.clone();
match &stmt.node {
StmtKind::FunctionDef { body, .. } | StmtKind::AsyncFunctionDef { body, .. } => {
let definition = docstring_checks::extract(
let definition = docstrings::extraction::extract(
&self.visible_scope,
stmt,
body,
@ -590,7 +590,7 @@ where
));
}
StmtKind::ClassDef { body, .. } => {
let definition = docstring_checks::extract(
let definition = docstrings::extraction::extract(
&self.visible_scope,
stmt,
body,
@ -1683,7 +1683,7 @@ impl<'a> Checker<'a> {
where
'b: 'a,
{
let docstring = docstring_checks::docstring_from(python_ast);
let docstring = docstrings::extraction::docstring_from(python_ast);
self.docstrings.push((
Definition {
kind: if self.path.ends_with("__init__.py") {
@ -1946,60 +1946,60 @@ impl<'a> Checker<'a> {
fn check_docstrings(&mut self) {
while let Some((docstring, visibility)) = self.docstrings.pop() {
if !docstring_checks::not_empty(self, &docstring) {
if !docstring_plugins::not_empty(self, &docstring) {
continue;
}
if !docstring_checks::not_missing(self, &docstring, &visibility) {
if !docstring_plugins::not_missing(self, &docstring, &visibility) {
continue;
}
if self.settings.enabled.contains(&CheckCode::D200) {
docstring_checks::one_liner(self, &docstring);
docstring_plugins::one_liner(self, &docstring);
}
if self.settings.enabled.contains(&CheckCode::D201)
|| self.settings.enabled.contains(&CheckCode::D202)
{
docstring_checks::blank_before_after_function(self, &docstring);
docstring_plugins::blank_before_after_function(self, &docstring);
}
if self.settings.enabled.contains(&CheckCode::D203)
|| self.settings.enabled.contains(&CheckCode::D204)
|| self.settings.enabled.contains(&CheckCode::D211)
{
docstring_checks::blank_before_after_class(self, &docstring);
docstring_plugins::blank_before_after_class(self, &docstring);
}
if self.settings.enabled.contains(&CheckCode::D205) {
docstring_checks::blank_after_summary(self, &docstring);
docstring_plugins::blank_after_summary(self, &docstring);
}
if self.settings.enabled.contains(&CheckCode::D209) {
docstring_checks::newline_after_last_paragraph(self, &docstring);
docstring_plugins::newline_after_last_paragraph(self, &docstring);
}
if self.settings.enabled.contains(&CheckCode::D210) {
docstring_checks::no_surrounding_whitespace(self, &docstring);
docstring_plugins::no_surrounding_whitespace(self, &docstring);
}
if self.settings.enabled.contains(&CheckCode::D212)
|| self.settings.enabled.contains(&CheckCode::D213)
{
docstring_checks::multi_line_summary_start(self, &docstring);
docstring_plugins::multi_line_summary_start(self, &docstring);
}
if self.settings.enabled.contains(&CheckCode::D300) {
docstring_checks::triple_quotes(self, &docstring);
docstring_plugins::triple_quotes(self, &docstring);
}
if self.settings.enabled.contains(&CheckCode::D400) {
docstring_checks::ends_with_period(self, &docstring);
docstring_plugins::ends_with_period(self, &docstring);
}
if self.settings.enabled.contains(&CheckCode::D402) {
docstring_checks::no_signature(self, &docstring);
docstring_plugins::no_signature(self, &docstring);
}
if self.settings.enabled.contains(&CheckCode::D403) {
docstring_checks::capitalized(self, &docstring);
docstring_plugins::capitalized(self, &docstring);
}
if self.settings.enabled.contains(&CheckCode::D404) {
docstring_checks::starts_with_this(self, &docstring);
docstring_plugins::starts_with_this(self, &docstring);
}
if self.settings.enabled.contains(&CheckCode::D415) {
docstring_checks::ends_with_punctuation(self, &docstring);
docstring_plugins::ends_with_punctuation(self, &docstring);
}
if self.settings.enabled.contains(&CheckCode::D418) {
docstring_checks::if_needed(self, &docstring);
docstring_plugins::if_needed(self, &docstring);
}
if self.settings.enabled.contains(&CheckCode::D212)
|| self.settings.enabled.contains(&CheckCode::D214)
@ -2007,7 +2007,6 @@ impl<'a> Checker<'a> {
|| self.settings.enabled.contains(&CheckCode::D405)
|| self.settings.enabled.contains(&CheckCode::D406)
|| self.settings.enabled.contains(&CheckCode::D407)
|| self.settings.enabled.contains(&CheckCode::D407)
|| self.settings.enabled.contains(&CheckCode::D408)
|| self.settings.enabled.contains(&CheckCode::D409)
|| self.settings.enabled.contains(&CheckCode::D410)
@ -2015,11 +2014,10 @@ impl<'a> Checker<'a> {
|| self.settings.enabled.contains(&CheckCode::D412)
|| self.settings.enabled.contains(&CheckCode::D413)
|| self.settings.enabled.contains(&CheckCode::D414)
|| self.settings.enabled.contains(&CheckCode::D414)
|| self.settings.enabled.contains(&CheckCode::D414)
|| self.settings.enabled.contains(&CheckCode::D416)
|| self.settings.enabled.contains(&CheckCode::D417)
{
docstring_checks::check_sections(self, &docstring);
docstring_plugins::sections(self, &docstring);
}
}
}

View file

@ -1,3 +1,8 @@
pub mod docstring_checks;
pub mod docstring_plugins;
pub mod extraction;
mod google;
mod helpers;
mod numpy;
pub mod sections;
mod styles;
pub mod types;

View file

@ -2,101 +2,18 @@
use once_cell::sync::Lazy;
use regex::Regex;
use rustpython_ast::{Constant, Expr, ExprKind, Location, Stmt, StmtKind};
use rustpython_ast::{Constant, ExprKind, Location, StmtKind};
use crate::ast::types::Range;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckCode, CheckKind};
use crate::docstrings::sections::{
check_google_section, check_numpy_section, section_contexts, SectionStyle,
};
use crate::docstrings::types::{Definition, DefinitionKind, Documentable};
use crate::visibility::{is_init, is_magic, is_overload, Modifier, Visibility, VisibleScope};
/// Extract a docstring from a function or class body.
pub fn docstring_from(suite: &[Stmt]) -> Option<&Expr> {
if let Some(stmt) = suite.first() {
if let StmtKind::Expr { value } = &stmt.node {
if matches!(
&value.node,
ExprKind::Constant {
value: Constant::Str(_),
..
}
) {
return Some(value);
}
}
}
None
}
/// Extract a `Definition` from the AST node defined by a `Stmt`.
pub fn extract<'a>(
scope: &VisibleScope,
stmt: &'a Stmt,
body: &'a [Stmt],
kind: &Documentable,
) -> Definition<'a> {
let expr = docstring_from(body);
match kind {
Documentable::Function => match scope {
VisibleScope {
modifier: Modifier::Module,
..
} => Definition {
kind: DefinitionKind::Function(stmt),
docstring: expr,
},
VisibleScope {
modifier: Modifier::Class,
..
} => Definition {
kind: DefinitionKind::Method(stmt),
docstring: expr,
},
VisibleScope {
modifier: Modifier::Function,
..
} => Definition {
kind: DefinitionKind::NestedFunction(stmt),
docstring: expr,
},
},
Documentable::Class => match scope {
VisibleScope {
modifier: Modifier::Module,
..
} => Definition {
kind: DefinitionKind::Class(stmt),
docstring: expr,
},
VisibleScope {
modifier: Modifier::Class,
..
} => Definition {
kind: DefinitionKind::NestedClass(stmt),
docstring: expr,
},
VisibleScope {
modifier: Modifier::Function,
..
} => Definition {
kind: DefinitionKind::NestedClass(stmt),
docstring: expr,
},
},
}
}
/// Extract the source code range for a docstring.
pub fn range_for(docstring: &Expr) -> Range {
// RustPython currently omits the first quotation mark in a string, so offset the location.
Range {
location: Location::new(docstring.location.row(), docstring.location.column() - 1),
end_location: docstring.end_location,
}
}
use crate::docstrings::google::check_google_section;
use crate::docstrings::helpers;
use crate::docstrings::numpy::check_numpy_section;
use crate::docstrings::sections::section_contexts;
use crate::docstrings::styles::SectionStyle;
use crate::docstrings::types::{Definition, DefinitionKind};
use crate::visibility::{is_init, is_magic, is_overload, Visibility};
/// D100, D101, D102, D103, D104, D105, D106, D107
pub fn not_missing(
@ -218,7 +135,10 @@ pub fn one_liner(checker: &mut Checker, definition: &Definition) {
}
if non_empty_line_count == 1 && line_count > 1 {
checker.add_check(Check::new(CheckKind::FitsOnOneLine, range_for(docstring)));
checker.add_check(Check::new(
CheckKind::FitsOnOneLine,
helpers::range_for(docstring),
));
}
}
}
@ -241,9 +161,10 @@ pub fn blank_before_after_function(checker: &mut Checker, definition: &Definitio
..
} = &docstring.node
{
let (before, _, after) = checker
.locator
.partition_source_code_at(&Range::from_located(parent), &range_for(docstring));
let (before, _, after) = checker.locator.partition_source_code_at(
&Range::from_located(parent),
&helpers::range_for(docstring),
);
if checker.settings.enabled.contains(&CheckCode::D201) {
let blank_lines_before = before
@ -255,7 +176,7 @@ pub fn blank_before_after_function(checker: &mut Checker, definition: &Definitio
if blank_lines_before != 0 {
checker.add_check(Check::new(
CheckKind::NoBlankLineBeforeFunction(blank_lines_before),
range_for(docstring),
helpers::range_for(docstring),
));
}
}
@ -280,7 +201,7 @@ pub fn blank_before_after_function(checker: &mut Checker, definition: &Definitio
{
checker.add_check(Check::new(
CheckKind::NoBlankLineAfterFunction(blank_lines_after),
range_for(docstring),
helpers::range_for(docstring),
));
}
}
@ -300,9 +221,10 @@ pub fn blank_before_after_class(checker: &mut Checker, definition: &Definition)
..
} = &docstring.node
{
let (before, _, after) = checker
.locator
.partition_source_code_at(&Range::from_located(parent), &range_for(docstring));
let (before, _, after) = checker.locator.partition_source_code_at(
&Range::from_located(parent),
&helpers::range_for(docstring),
);
if checker.settings.enabled.contains(&CheckCode::D203)
|| checker.settings.enabled.contains(&CheckCode::D211)
@ -318,7 +240,7 @@ pub fn blank_before_after_class(checker: &mut Checker, definition: &Definition)
{
checker.add_check(Check::new(
CheckKind::NoBlankLineBeforeClass(blank_lines_before),
range_for(docstring),
helpers::range_for(docstring),
));
}
if blank_lines_before != 1
@ -326,7 +248,7 @@ pub fn blank_before_after_class(checker: &mut Checker, definition: &Definition)
{
checker.add_check(Check::new(
CheckKind::OneBlankLineBeforeClass(blank_lines_before),
range_for(docstring),
helpers::range_for(docstring),
));
}
}
@ -344,7 +266,7 @@ pub fn blank_before_after_class(checker: &mut Checker, definition: &Definition)
if !all_blank_after && blank_lines_after != 1 {
checker.add_check(Check::new(
CheckKind::OneBlankLineAfterClass(blank_lines_after),
range_for(docstring),
helpers::range_for(docstring),
));
}
}
@ -374,7 +296,7 @@ pub fn blank_after_summary(checker: &mut Checker, definition: &Definition) {
if lines_count > 1 && blanks_count != 1 {
checker.add_check(Check::new(
CheckKind::NoBlankLineAfterSummary,
range_for(docstring),
helpers::range_for(docstring),
));
}
}
@ -397,13 +319,13 @@ pub fn newline_after_last_paragraph(checker: &mut Checker, definition: &Definiti
if line_count > 1 {
let content = checker
.locator
.slice_source_code_range(&range_for(docstring));
.slice_source_code_range(&helpers::range_for(docstring));
if let Some(line) = content.lines().last() {
let line = line.trim();
if line != "\"\"\"" && line != "'''" {
checker.add_check(Check::new(
CheckKind::NewLineAfterLastParagraph,
range_for(docstring),
helpers::range_for(docstring),
));
}
}
@ -430,7 +352,7 @@ pub fn no_surrounding_whitespace(checker: &mut Checker, definition: &Definition)
if line.starts_with(' ') || (matches!(lines.next(), None) && line.ends_with(' ')) {
checker.add_check(Check::new(
CheckKind::NoSurroundingWhitespace,
range_for(docstring),
helpers::range_for(docstring),
));
}
}
@ -449,20 +371,20 @@ pub fn multi_line_summary_start(checker: &mut Checker, definition: &Definition)
if string.lines().nth(1).is_some() {
let content = checker
.locator
.slice_source_code_range(&range_for(docstring));
.slice_source_code_range(&helpers::range_for(docstring));
if let Some(first_line) = content.lines().next() {
let first_line = first_line.trim();
if first_line == "\"\"\"" || first_line == "'''" {
if checker.settings.enabled.contains(&CheckCode::D212) {
checker.add_check(Check::new(
CheckKind::MultiLineSummaryFirstLine,
range_for(docstring),
helpers::range_for(docstring),
));
}
} else if checker.settings.enabled.contains(&CheckCode::D213) {
checker.add_check(Check::new(
CheckKind::MultiLineSummarySecondLine,
range_for(docstring),
helpers::range_for(docstring),
));
}
}
@ -481,18 +403,18 @@ pub fn triple_quotes(checker: &mut Checker, definition: &Definition) {
{
let content = checker
.locator
.slice_source_code_range(&range_for(docstring));
.slice_source_code_range(&helpers::range_for(docstring));
if string.contains("\"\"\"") {
if !content.starts_with("'''") {
checker.add_check(Check::new(
CheckKind::UsesTripleQuotes,
range_for(docstring),
helpers::range_for(docstring),
));
}
} else if !content.starts_with("\"\"\"") {
checker.add_check(Check::new(
CheckKind::UsesTripleQuotes,
range_for(docstring),
helpers::range_for(docstring),
));
}
}
@ -509,7 +431,10 @@ pub fn ends_with_period(checker: &mut Checker, definition: &Definition) {
{
if let Some(string) = string.lines().next() {
if !string.ends_with('.') {
checker.add_check(Check::new(CheckKind::EndsInPeriod, range_for(docstring)));
checker.add_check(Check::new(
CheckKind::EndsInPeriod,
helpers::range_for(docstring),
));
}
}
}
@ -533,7 +458,7 @@ pub fn no_signature(checker: &mut Checker, definition: &Definition) {
if first_line.contains(&format!("{name}(")) {
checker.add_check(Check::new(
CheckKind::NoSignature,
range_for(docstring),
helpers::range_for(docstring),
));
}
}
@ -568,7 +493,7 @@ pub fn capitalized(checker: &mut Checker, definition: &Definition) {
if !first_char.is_uppercase() {
checker.add_check(Check::new(
CheckKind::FirstLineCapitalized,
range_for(docstring),
helpers::range_for(docstring),
));
}
}
@ -596,7 +521,10 @@ pub fn starts_with_this(checker: &mut Checker, definition: &Definition) {
.to_lowercase()
== "this"
{
checker.add_check(Check::new(CheckKind::NoThisPrefix, range_for(docstring)));
checker.add_check(Check::new(
CheckKind::NoThisPrefix,
helpers::range_for(docstring),
));
}
}
}
@ -615,7 +543,7 @@ pub fn ends_with_punctuation(checker: &mut Checker, definition: &Definition) {
if !(string.ends_with('.') || string.ends_with('!') || string.ends_with('?')) {
checker.add_check(Check::new(
CheckKind::EndsInPunctuation,
range_for(docstring),
helpers::range_for(docstring),
));
}
}
@ -650,7 +578,10 @@ pub fn not_empty(checker: &mut Checker, definition: &Definition) -> bool {
{
if string.trim().is_empty() {
if checker.settings.enabled.contains(&CheckCode::D419) {
checker.add_check(Check::new(CheckKind::NonEmpty, range_for(docstring)));
checker.add_check(Check::new(
CheckKind::NonEmpty,
helpers::range_for(docstring),
));
}
return false;
}
@ -659,7 +590,8 @@ pub fn not_empty(checker: &mut Checker, definition: &Definition) -> bool {
true
}
pub fn check_sections(checker: &mut Checker, definition: &Definition) {
/// D212, D214, D215, D405, D406, D407, D408, D409, D410, D411, D412, D413, D414, D416, D417
pub fn sections(checker: &mut Checker, definition: &Definition) {
if let Some(docstring) = definition.docstring {
if let ExprKind::Constant {
value: Constant::Str(string),
@ -671,7 +603,7 @@ pub fn check_sections(checker: &mut Checker, definition: &Definition) {
return;
}
// First, try to interpret as NumPy-style sections.
// First, interpret as NumPy-style sections.
let mut found_numpy_section = false;
for context in &section_contexts(&lines, &SectionStyle::NumPy) {
found_numpy_section = true;

View file

@ -0,0 +1,82 @@
//! Extract docstrings from an AST.
use rustpython_ast::{Constant, Expr, ExprKind, Stmt, StmtKind};
use crate::docstrings::types::{Definition, DefinitionKind, Documentable};
use crate::visibility::{Modifier, VisibleScope};
/// Extract a docstring from a function or class body.
pub fn docstring_from(suite: &[Stmt]) -> Option<&Expr> {
if let Some(stmt) = suite.first() {
if let StmtKind::Expr { value } = &stmt.node {
if matches!(
&value.node,
ExprKind::Constant {
value: Constant::Str(_),
..
}
) {
return Some(value);
}
}
}
None
}
/// Extract a `Definition` from the AST node defined by a `Stmt`.
pub fn extract<'a>(
scope: &VisibleScope,
stmt: &'a Stmt,
body: &'a [Stmt],
kind: &Documentable,
) -> Definition<'a> {
let expr = docstring_from(body);
match kind {
Documentable::Function => match scope {
VisibleScope {
modifier: Modifier::Module,
..
} => Definition {
kind: DefinitionKind::Function(stmt),
docstring: expr,
},
VisibleScope {
modifier: Modifier::Class,
..
} => Definition {
kind: DefinitionKind::Method(stmt),
docstring: expr,
},
VisibleScope {
modifier: Modifier::Function,
..
} => Definition {
kind: DefinitionKind::NestedFunction(stmt),
docstring: expr,
},
},
Documentable::Class => match scope {
VisibleScope {
modifier: Modifier::Module,
..
} => Definition {
kind: DefinitionKind::Class(stmt),
docstring: expr,
},
VisibleScope {
modifier: Modifier::Class,
..
} => Definition {
kind: DefinitionKind::NestedClass(stmt),
docstring: expr,
},
VisibleScope {
modifier: Modifier::Function,
..
} => Definition {
kind: DefinitionKind::NestedClass(stmt),
docstring: expr,
},
},
}
}

150
src/docstrings/google.rs Normal file
View file

@ -0,0 +1,150 @@
//! Abstractions for Google-style docstrings.
use std::collections::BTreeSet;
use once_cell::sync::Lazy;
use regex::Regex;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckCode, CheckKind};
use crate::docstrings::helpers::range_for;
use crate::docstrings::sections;
use crate::docstrings::sections::SectionContext;
use crate::docstrings::styles::SectionStyle;
use crate::docstrings::types::Definition;
pub(crate) static GOOGLE_SECTION_NAMES: Lazy<BTreeSet<&'static str>> = Lazy::new(|| {
BTreeSet::from([
"Args",
"Arguments",
"Attention",
"Attributes",
"Caution",
"Danger",
"Error",
"Example",
"Examples",
"Hint",
"Important",
"Keyword Args",
"Keyword Arguments",
"Methods",
"Note",
"Notes",
"Return",
"Returns",
"Raises",
"References",
"See Also",
"Tip",
"Todo",
"Warning",
"Warnings",
"Warns",
"Yield",
"Yields",
])
});
pub(crate) static LOWERCASE_GOOGLE_SECTION_NAMES: Lazy<BTreeSet<&'static str>> = Lazy::new(|| {
BTreeSet::from([
"args",
"arguments",
"attention",
"attributes",
"caution",
"danger",
"error",
"example",
"examples",
"hint",
"important",
"keyword args",
"keyword arguments",
"methods",
"note",
"notes",
"return",
"returns",
"raises",
"references",
"see also",
"tip",
"todo",
"warning",
"warnings",
"warns",
"yield",
"yields",
])
});
// See: `GOOGLE_ARGS_REGEX` in `pydocstyle/checker.py`.
static GOOGLE_ARGS_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^\s*(\w+)\s*(\(.*?\))?\s*:\n?\s*.+").expect("Invalid regex"));
fn check_args_section(checker: &mut Checker, definition: &Definition, context: &SectionContext) {
let mut args_sections: Vec<String> = vec![];
for line in textwrap::dedent(&context.following_lines.join("\n")).lines() {
if line
.chars()
.next()
.map(|char| char.is_whitespace())
.unwrap_or(true)
{
// This is a continuation of documentation for the last
// parameter because it does start with whitespace.
if let Some(current) = args_sections.last_mut() {
current.push_str(line);
}
} else {
// This line is the start of documentation for the next
// parameter because it doesn't start with any whitespace.
args_sections.push(line.to_string());
}
}
sections::check_missing_args(
checker,
definition,
// Collect the list of arguments documented in the docstring.
&BTreeSet::from_iter(args_sections.iter().filter_map(|section| {
match GOOGLE_ARGS_REGEX.captures(section.as_str()) {
Some(caps) => caps.get(1).map(|arg_name| arg_name.as_str()),
None => None,
}
})),
)
}
pub(crate) fn check_google_section(
checker: &mut Checker,
definition: &Definition,
context: &SectionContext,
) {
sections::check_common_section(checker, definition, context, &SectionStyle::Google);
if checker.settings.enabled.contains(&CheckCode::D416) {
let suffix = context
.line
.trim()
.strip_prefix(&context.section_name)
.unwrap();
if suffix != ":" {
let docstring = definition
.docstring
.expect("Sections are only available for docstrings.");
checker.add_check(Check::new(
CheckKind::SectionNameEndsInColon(context.section_name.to_string()),
range_for(docstring),
))
}
}
if checker.settings.enabled.contains(&CheckCode::D417) {
let capitalized_section_name = titlecase::titlecase(&context.section_name);
if capitalized_section_name == "Args" || capitalized_section_name == "Arguments" {
check_args_section(checker, definition, context);
}
}
}

37
src/docstrings/helpers.rs Normal file
View file

@ -0,0 +1,37 @@
use rustpython_ast::{Expr, Location};
use crate::ast::types::Range;
use crate::check_ast::Checker;
/// Extract the leading words from a line of text.
pub fn leading_words(line: &str) -> String {
line.trim()
.chars()
.take_while(|char| char.is_alphanumeric() || char.is_whitespace())
.collect()
}
/// Extract the leading whitespace from a line of text.
pub fn leading_space(line: &str) -> String {
line.chars()
.take_while(|char| char.is_whitespace())
.collect()
}
/// Extract the leading indentation from a docstring.
pub fn indentation<'a>(checker: &'a mut Checker, docstring: &Expr) -> &'a str {
let range = range_for(docstring);
checker.locator.slice_source_code_range(&Range {
location: Location::new(range.location.row(), 1),
end_location: Location::new(range.location.row(), range.location.column()),
})
}
/// Extract the source code range for a docstring.
pub fn range_for(docstring: &Expr) -> Range {
// RustPython currently omits the first quotation mark in a string, so offset the location.
Range {
location: Location::new(docstring.location.row(), docstring.location.column() - 1),
end_location: docstring.end_location,
}
}

112
src/docstrings/numpy.rs Normal file
View file

@ -0,0 +1,112 @@
//! Abstractions for NumPy-style docstrings.
use crate::check_ast::Checker;
use crate::checks::{Check, CheckCode, CheckKind};
use crate::docstrings::helpers::range_for;
use crate::docstrings::sections::SectionContext;
use crate::docstrings::styles::SectionStyle;
use crate::docstrings::types::Definition;
use crate::docstrings::{helpers, sections};
use once_cell::sync::Lazy;
use std::collections::BTreeSet;
pub(crate) static LOWERCASE_NUMPY_SECTION_NAMES: Lazy<BTreeSet<&'static str>> = Lazy::new(|| {
BTreeSet::from([
"short summary",
"extended summary",
"parameters",
"returns",
"yields",
"other parameters",
"raises",
"see also",
"notes",
"references",
"examples",
"attributes",
"methods",
])
});
pub(crate) static NUMPY_SECTION_NAMES: Lazy<BTreeSet<&'static str>> = Lazy::new(|| {
BTreeSet::from([
"Short Summary",
"Extended Summary",
"Parameters",
"Returns",
"Yields",
"Other Parameters",
"Raises",
"See Also",
"Notes",
"References",
"Examples",
"Attributes",
"Methods",
])
});
fn check_parameters_section(
checker: &mut Checker,
definition: &Definition,
context: &SectionContext,
) {
// Collect the list of arguments documented in the docstring.
let mut docstring_args: BTreeSet<&str> = Default::default();
let section_level_indent = helpers::leading_space(context.line);
for i in 1..context.following_lines.len() {
let current_line = context.following_lines[i - 1];
let current_leading_space = helpers::leading_space(current_line);
let next_line = context.following_lines[i];
if current_leading_space == section_level_indent
&& (helpers::leading_space(next_line).len() > current_leading_space.len())
&& !next_line.trim().is_empty()
{
let parameters = if let Some(semi_index) = current_line.find(':') {
// If the parameter has a type annotation, exclude it.
&current_line[..semi_index]
} else {
// Otherwise, it's just a list of parameters on the current line.
current_line.trim()
};
// Notably, NumPy lets you put multiple parameters of the same type on the same line.
for parameter in parameters.split(',') {
docstring_args.insert(parameter.trim());
}
}
}
// Validate that all arguments were documented.
sections::check_missing_args(checker, definition, &docstring_args);
}
pub(crate) fn check_numpy_section(
checker: &mut Checker,
definition: &Definition,
context: &SectionContext,
) {
sections::check_common_section(checker, definition, context, &SectionStyle::NumPy);
if checker.settings.enabled.contains(&CheckCode::D406) {
let suffix = context
.line
.trim()
.strip_prefix(&context.section_name)
.unwrap();
if !suffix.is_empty() {
let docstring = definition
.docstring
.expect("Sections are only available for docstrings.");
checker.add_check(Check::new(
CheckKind::NewLineAfterSectionName(context.section_name.to_string()),
range_for(docstring),
))
}
}
if checker.settings.enabled.contains(&CheckCode::D417) {
let capitalized_section_name = titlecase::titlecase(&context.section_name);
if capitalized_section_name == "Parameters" {
check_parameters_section(checker, definition, context);
}
}
}

View file

@ -1,176 +1,32 @@
use itertools::Itertools;
use std::collections::BTreeSet;
use once_cell::sync::Lazy;
use regex::Regex;
use rustpython_ast::{Arg, Expr, Location, StmtKind};
use itertools::Itertools;
use rustpython_ast::{Arg, StmtKind};
use titlecase::titlecase;
use crate::ast::types::Range;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckCode, CheckKind};
use crate::docstrings::docstring_checks::range_for;
use crate::docstrings::helpers;
use crate::docstrings::helpers::range_for;
use crate::docstrings::styles::SectionStyle;
use crate::docstrings::types::{Definition, DefinitionKind};
use crate::visibility::is_static;
static NUMPY_SECTION_NAMES: Lazy<BTreeSet<&'static str>> = Lazy::new(|| {
BTreeSet::from([
"Short Summary",
"Extended Summary",
"Parameters",
"Returns",
"Yields",
"Other Parameters",
"Raises",
"See Also",
"Notes",
"References",
"Examples",
"Attributes",
"Methods",
])
});
static LOWERCASE_NUMPY_SECTION_NAMES: Lazy<BTreeSet<&'static str>> = Lazy::new(|| {
BTreeSet::from([
"short summary",
"extended summary",
"parameters",
"returns",
"yields",
"other parameters",
"raises",
"see also",
"notes",
"references",
"examples",
"attributes",
"methods",
])
});
static GOOGLE_SECTION_NAMES: Lazy<BTreeSet<&'static str>> = Lazy::new(|| {
BTreeSet::from([
"Args",
"Arguments",
"Attention",
"Attributes",
"Caution",
"Danger",
"Error",
"Example",
"Examples",
"Hint",
"Important",
"Keyword Args",
"Keyword Arguments",
"Methods",
"Note",
"Notes",
"Return",
"Returns",
"Raises",
"References",
"See Also",
"Tip",
"Todo",
"Warning",
"Warnings",
"Warns",
"Yield",
"Yields",
])
});
static LOWERCASE_GOOGLE_SECTION_NAMES: Lazy<BTreeSet<&'static str>> = Lazy::new(|| {
BTreeSet::from([
"args",
"arguments",
"attention",
"attributes",
"caution",
"danger",
"error",
"example",
"examples",
"hint",
"important",
"keyword args",
"keyword arguments",
"methods",
"note",
"notes",
"return",
"returns",
"raises",
"references",
"see also",
"tip",
"todo",
"warning",
"warnings",
"warns",
"yield",
"yields",
])
});
pub enum SectionStyle {
NumPy,
Google,
}
impl SectionStyle {
fn section_names(&self) -> &Lazy<BTreeSet<&'static str>> {
match self {
SectionStyle::NumPy => &NUMPY_SECTION_NAMES,
SectionStyle::Google => &GOOGLE_SECTION_NAMES,
}
}
fn lowercase_section_names(&self) -> &Lazy<BTreeSet<&'static str>> {
match self {
SectionStyle::NumPy => &LOWERCASE_NUMPY_SECTION_NAMES,
SectionStyle::Google => &LOWERCASE_GOOGLE_SECTION_NAMES,
}
}
}
fn indentation<'a>(checker: &'a mut Checker, docstring: &Expr) -> &'a str {
let range = range_for(docstring);
checker.locator.slice_source_code_range(&Range {
location: Location::new(range.location.row(), 1),
end_location: Location::new(range.location.row(), range.location.column()),
})
}
fn leading_space(line: &str) -> String {
line.chars()
.take_while(|char| char.is_whitespace())
.collect()
}
fn leading_words(line: &str) -> String {
line.trim()
.chars()
.take_while(|char| char.is_alphanumeric() || char.is_whitespace())
.collect()
#[derive(Debug)]
pub(crate) struct SectionContext<'a> {
pub(crate) section_name: String,
pub(crate) previous_line: &'a str,
pub(crate) line: &'a str,
pub(crate) following_lines: &'a [&'a str],
pub(crate) is_last_section: bool,
original_index: usize,
}
fn suspected_as_section(line: &str, style: &SectionStyle) -> bool {
style
.lowercase_section_names()
.contains(&leading_words(line).to_lowercase().as_str())
}
#[derive(Debug)]
pub struct SectionContext<'a> {
section_name: String,
previous_line: &'a str,
line: &'a str,
following_lines: &'a [&'a str],
original_index: usize,
is_last_section: bool,
.contains(&helpers::leading_words(line).to_lowercase().as_str())
}
/// Check if the suspected context is really a section header.
@ -201,7 +57,10 @@ fn is_docstring_section(context: &SectionContext) -> bool {
}
/// Extract all `SectionContext` values from a docstring.
pub fn section_contexts<'a>(lines: &'a [&'a str], style: &SectionStyle) -> Vec<SectionContext<'a>> {
pub(crate) fn section_contexts<'a>(
lines: &'a [&'a str],
style: &SectionStyle,
) -> Vec<SectionContext<'a>> {
let suspected_section_indices: Vec<usize> = lines
.iter()
.enumerate()
@ -217,7 +76,7 @@ pub fn section_contexts<'a>(lines: &'a [&'a str], style: &SectionStyle) -> Vec<S
let mut contexts = vec![];
for lineno in suspected_section_indices {
let context = SectionContext {
section_name: leading_words(lines[lineno]),
section_name: helpers::leading_words(lines[lineno]),
previous_line: lines[lineno - 1],
line: lines[lineno],
following_lines: &lines[lineno + 1..],
@ -335,7 +194,9 @@ fn check_blanks_and_section_underline(
}
if checker.settings.enabled.contains(&CheckCode::D215) {
if leading_space(non_empty_line).len() > indentation(checker, docstring).len() {
if helpers::leading_space(non_empty_line).len()
> helpers::indentation(checker, docstring).len()
{
checker.add_check(Check::new(
CheckKind::SectionUnderlineNotOverIndented(context.section_name.to_string()),
range_for(docstring),
@ -378,7 +239,7 @@ fn check_blanks_and_section_underline(
}
}
fn check_common_section(
pub(crate) fn check_common_section(
checker: &mut Checker,
definition: &Definition,
context: &SectionContext,
@ -404,7 +265,9 @@ fn check_common_section(
}
if checker.settings.enabled.contains(&CheckCode::D214) {
if leading_space(context.line).len() > indentation(checker, docstring).len() {
if helpers::leading_space(context.line).len()
> helpers::indentation(checker, docstring).len()
{
checker.add_check(Check::new(
CheckKind::SectionNotOverIndented(context.section_name.to_string()),
range_for(docstring),
@ -443,9 +306,11 @@ fn check_common_section(
))
}
}
check_blanks_and_section_underline(checker, definition, context);
}
fn check_missing_args(
pub(crate) fn check_missing_args(
checker: &mut Checker,
definition: &Definition,
docstrings_args: &BTreeSet<&str>,
@ -510,139 +375,3 @@ fn check_missing_args(
}
}
}
fn check_parameters_section(
checker: &mut Checker,
definition: &Definition,
context: &SectionContext,
) {
// Collect the list of arguments documented in the docstring.
let mut docstring_args: BTreeSet<&str> = Default::default();
let section_level_indent = leading_space(context.line);
for i in 1..context.following_lines.len() {
let current_line = context.following_lines[i - 1];
let current_leading_space = leading_space(current_line);
let next_line = context.following_lines[i];
if current_leading_space == section_level_indent
&& (leading_space(next_line).len() > current_leading_space.len())
&& !next_line.trim().is_empty()
{
let parameters = if let Some(semi_index) = current_line.find(':') {
// If the parameter has a type annotation, exclude it.
&current_line[..semi_index]
} else {
// Otherwise, it's just a list of parameters on the current line.
current_line.trim()
};
// Notably, NumPy lets you put multiple parameters of the same type on the same line.
for parameter in parameters.split(',') {
docstring_args.insert(parameter.trim());
}
}
}
// Validate that all arguments were documented.
check_missing_args(checker, definition, &docstring_args);
}
pub fn check_numpy_section(
checker: &mut Checker,
definition: &Definition,
context: &SectionContext,
) {
check_common_section(checker, definition, context, &SectionStyle::NumPy);
check_blanks_and_section_underline(checker, definition, context);
if checker.settings.enabled.contains(&CheckCode::D406) {
let suffix = context
.line
.trim()
.strip_prefix(&context.section_name)
.unwrap();
if !suffix.is_empty() {
let docstring = definition
.docstring
.expect("Sections are only available for docstrings.");
checker.add_check(Check::new(
CheckKind::NewLineAfterSectionName(context.section_name.to_string()),
range_for(docstring),
))
}
}
if checker.settings.enabled.contains(&CheckCode::D417) {
if titlecase(&context.section_name) == "Parameters" {
check_parameters_section(checker, definition, context);
}
}
}
// See: `GOOGLE_ARGS_REGEX` in `pydocstyle/checker.py`.
static GOOGLE_ARGS_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^\s*(\w+)\s*(\(.*?\))?\s*:\n?\s*.+").expect("Invalid regex"));
fn check_args_section(checker: &mut Checker, definition: &Definition, context: &SectionContext) {
let mut args_sections: Vec<String> = vec![];
for line in textwrap::dedent(&context.following_lines.join("\n")).lines() {
if line
.chars()
.next()
.map(|char| char.is_whitespace())
.unwrap_or(true)
{
// This is a continuation of documentation for the last
// parameter because it does start with whitespace.
if let Some(current) = args_sections.last_mut() {
current.push_str(line);
}
} else {
// This line is the start of documentation for the next
// parameter because it doesn't start with any whitespace.
args_sections.push(line.to_string());
}
}
check_missing_args(
checker,
definition,
// Collect the list of arguments documented in the docstring.
&BTreeSet::from_iter(args_sections.iter().filter_map(|section| {
match GOOGLE_ARGS_REGEX.captures(section.as_str()) {
Some(caps) => caps.get(1).map(|arg_name| arg_name.as_str()),
None => None,
}
})),
)
}
pub fn check_google_section(
checker: &mut Checker,
definition: &Definition,
context: &SectionContext,
) {
check_common_section(checker, definition, context, &SectionStyle::Google);
check_blanks_and_section_underline(checker, definition, context);
if checker.settings.enabled.contains(&CheckCode::D416) {
let suffix = context
.line
.trim()
.strip_prefix(&context.section_name)
.unwrap();
if suffix != ":" {
let docstring = definition
.docstring
.expect("Sections are only available for docstrings.");
checker.add_check(Check::new(
CheckKind::SectionNameEndsInColon(context.section_name.to_string()),
range_for(docstring),
))
}
}
if checker.settings.enabled.contains(&CheckCode::D417) {
let capitalized_section_name = titlecase(&context.section_name);
if capitalized_section_name == "Args" || capitalized_section_name == "Arguments" {
check_args_section(checker, definition, context);
}
}
}

27
src/docstrings/styles.rs Normal file
View file

@ -0,0 +1,27 @@
use std::collections::BTreeSet;
use once_cell::sync::Lazy;
use crate::docstrings::google::{GOOGLE_SECTION_NAMES, LOWERCASE_GOOGLE_SECTION_NAMES};
use crate::docstrings::numpy::{LOWERCASE_NUMPY_SECTION_NAMES, NUMPY_SECTION_NAMES};
pub(crate) enum SectionStyle {
NumPy,
Google,
}
impl SectionStyle {
pub(crate) fn section_names(&self) -> &Lazy<BTreeSet<&'static str>> {
match self {
SectionStyle::NumPy => &NUMPY_SECTION_NAMES,
SectionStyle::Google => &GOOGLE_SECTION_NAMES,
}
}
pub(crate) fn lowercase_section_names(&self) -> &Lazy<BTreeSet<&'static str>> {
match self {
SectionStyle::NumPy => &LOWERCASE_NUMPY_SECTION_NAMES,
SectionStyle::Google => &LOWERCASE_GOOGLE_SECTION_NAMES,
}
}
}