mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-27 12:29:48 +00:00
Re-arrange some docstring modules (#428)
This commit is contained in:
parent
b64040cbb2
commit
6407fd5a33
9 changed files with 519 additions and 447 deletions
|
@ -21,13 +21,13 @@ use crate::ast::visitor::{walk_excepthandler, Visitor};
|
||||||
use crate::ast::{checkers, helpers, operations, visitor};
|
use crate::ast::{checkers, helpers, operations, visitor};
|
||||||
use crate::autofix::{fixer, fixes};
|
use crate::autofix::{fixer, fixes};
|
||||||
use crate::checks::{Check, CheckCode, CheckKind};
|
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::docstrings::types::{Definition, DefinitionKind, Documentable};
|
||||||
use crate::plugins;
|
|
||||||
use crate::python::builtins::{BUILTINS, MAGIC_GLOBALS};
|
use crate::python::builtins::{BUILTINS, MAGIC_GLOBALS};
|
||||||
use crate::python::future::ALL_FEATURE_NAMES;
|
use crate::python::future::ALL_FEATURE_NAMES;
|
||||||
use crate::settings::{PythonVersion, Settings};
|
use crate::settings::{PythonVersion, Settings};
|
||||||
use crate::visibility::{module_visibility, transition_scope, Modifier, Visibility, VisibleScope};
|
use crate::visibility::{module_visibility, transition_scope, Modifier, Visibility, VisibleScope};
|
||||||
|
use crate::{docstrings, plugins};
|
||||||
|
|
||||||
pub const GLOBAL_SCOPE_INDEX: usize = 0;
|
pub const GLOBAL_SCOPE_INDEX: usize = 0;
|
||||||
|
|
||||||
|
@ -572,7 +572,7 @@ where
|
||||||
let prev_visibile_scope = self.visible_scope.clone();
|
let prev_visibile_scope = self.visible_scope.clone();
|
||||||
match &stmt.node {
|
match &stmt.node {
|
||||||
StmtKind::FunctionDef { body, .. } | StmtKind::AsyncFunctionDef { body, .. } => {
|
StmtKind::FunctionDef { body, .. } | StmtKind::AsyncFunctionDef { body, .. } => {
|
||||||
let definition = docstring_checks::extract(
|
let definition = docstrings::extraction::extract(
|
||||||
&self.visible_scope,
|
&self.visible_scope,
|
||||||
stmt,
|
stmt,
|
||||||
body,
|
body,
|
||||||
|
@ -590,7 +590,7 @@ where
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
StmtKind::ClassDef { body, .. } => {
|
StmtKind::ClassDef { body, .. } => {
|
||||||
let definition = docstring_checks::extract(
|
let definition = docstrings::extraction::extract(
|
||||||
&self.visible_scope,
|
&self.visible_scope,
|
||||||
stmt,
|
stmt,
|
||||||
body,
|
body,
|
||||||
|
@ -1683,7 +1683,7 @@ impl<'a> Checker<'a> {
|
||||||
where
|
where
|
||||||
'b: 'a,
|
'b: 'a,
|
||||||
{
|
{
|
||||||
let docstring = docstring_checks::docstring_from(python_ast);
|
let docstring = docstrings::extraction::docstring_from(python_ast);
|
||||||
self.docstrings.push((
|
self.docstrings.push((
|
||||||
Definition {
|
Definition {
|
||||||
kind: if self.path.ends_with("__init__.py") {
|
kind: if self.path.ends_with("__init__.py") {
|
||||||
|
@ -1946,60 +1946,60 @@ impl<'a> Checker<'a> {
|
||||||
|
|
||||||
fn check_docstrings(&mut self) {
|
fn check_docstrings(&mut self) {
|
||||||
while let Some((docstring, visibility)) = self.docstrings.pop() {
|
while let Some((docstring, visibility)) = self.docstrings.pop() {
|
||||||
if !docstring_checks::not_empty(self, &docstring) {
|
if !docstring_plugins::not_empty(self, &docstring) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if !docstring_checks::not_missing(self, &docstring, &visibility) {
|
if !docstring_plugins::not_missing(self, &docstring, &visibility) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if self.settings.enabled.contains(&CheckCode::D200) {
|
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)
|
if self.settings.enabled.contains(&CheckCode::D201)
|
||||||
|| self.settings.enabled.contains(&CheckCode::D202)
|
|| 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)
|
if self.settings.enabled.contains(&CheckCode::D203)
|
||||||
|| self.settings.enabled.contains(&CheckCode::D204)
|
|| self.settings.enabled.contains(&CheckCode::D204)
|
||||||
|| self.settings.enabled.contains(&CheckCode::D211)
|
|| 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) {
|
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) {
|
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) {
|
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)
|
if self.settings.enabled.contains(&CheckCode::D212)
|
||||||
|| self.settings.enabled.contains(&CheckCode::D213)
|
|| 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) {
|
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) {
|
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) {
|
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) {
|
if self.settings.enabled.contains(&CheckCode::D403) {
|
||||||
docstring_checks::capitalized(self, &docstring);
|
docstring_plugins::capitalized(self, &docstring);
|
||||||
}
|
}
|
||||||
if self.settings.enabled.contains(&CheckCode::D404) {
|
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) {
|
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) {
|
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)
|
if self.settings.enabled.contains(&CheckCode::D212)
|
||||||
|| self.settings.enabled.contains(&CheckCode::D214)
|
|| 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::D405)
|
||||||
|| self.settings.enabled.contains(&CheckCode::D406)
|
|| self.settings.enabled.contains(&CheckCode::D406)
|
||||||
|| self.settings.enabled.contains(&CheckCode::D407)
|
|| self.settings.enabled.contains(&CheckCode::D407)
|
||||||
|| self.settings.enabled.contains(&CheckCode::D407)
|
|
||||||
|| self.settings.enabled.contains(&CheckCode::D408)
|
|| self.settings.enabled.contains(&CheckCode::D408)
|
||||||
|| self.settings.enabled.contains(&CheckCode::D409)
|
|| self.settings.enabled.contains(&CheckCode::D409)
|
||||||
|| self.settings.enabled.contains(&CheckCode::D410)
|
|| 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::D412)
|
||||||
|| self.settings.enabled.contains(&CheckCode::D413)
|
|| 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::D414)
|
|
||||||
|| self.settings.enabled.contains(&CheckCode::D417)
|
|| self.settings.enabled.contains(&CheckCode::D417)
|
||||||
{
|
{
|
||||||
docstring_checks::check_sections(self, &docstring);
|
docstring_plugins::sections(self, &docstring);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
pub mod docstring_checks;
|
pub mod docstring_plugins;
|
||||||
|
pub mod extraction;
|
||||||
|
mod google;
|
||||||
|
mod helpers;
|
||||||
|
mod numpy;
|
||||||
pub mod sections;
|
pub mod sections;
|
||||||
|
mod styles;
|
||||||
pub mod types;
|
pub mod types;
|
||||||
|
|
|
@ -2,101 +2,18 @@
|
||||||
|
|
||||||
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, ExprKind, Location, StmtKind};
|
||||||
|
|
||||||
use crate::ast::types::Range;
|
use crate::ast::types::Range;
|
||||||
use crate::check_ast::Checker;
|
use crate::check_ast::Checker;
|
||||||
use crate::checks::{Check, CheckCode, CheckKind};
|
use crate::checks::{Check, CheckCode, CheckKind};
|
||||||
use crate::docstrings::sections::{
|
use crate::docstrings::google::check_google_section;
|
||||||
check_google_section, check_numpy_section, section_contexts, SectionStyle,
|
use crate::docstrings::helpers;
|
||||||
};
|
use crate::docstrings::numpy::check_numpy_section;
|
||||||
use crate::docstrings::types::{Definition, DefinitionKind, Documentable};
|
use crate::docstrings::sections::section_contexts;
|
||||||
use crate::visibility::{is_init, is_magic, is_overload, Modifier, Visibility, VisibleScope};
|
use crate::docstrings::styles::SectionStyle;
|
||||||
|
use crate::docstrings::types::{Definition, DefinitionKind};
|
||||||
/// Extract a docstring from a function or class body.
|
use crate::visibility::{is_init, is_magic, is_overload, Visibility};
|
||||||
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// D100, D101, D102, D103, D104, D105, D106, D107
|
/// D100, D101, D102, D103, D104, D105, D106, D107
|
||||||
pub fn not_missing(
|
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 {
|
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
|
} = &docstring.node
|
||||||
{
|
{
|
||||||
let (before, _, after) = checker
|
let (before, _, after) = checker.locator.partition_source_code_at(
|
||||||
.locator
|
&Range::from_located(parent),
|
||||||
.partition_source_code_at(&Range::from_located(parent), &range_for(docstring));
|
&helpers::range_for(docstring),
|
||||||
|
);
|
||||||
|
|
||||||
if checker.settings.enabled.contains(&CheckCode::D201) {
|
if checker.settings.enabled.contains(&CheckCode::D201) {
|
||||||
let blank_lines_before = before
|
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 {
|
if blank_lines_before != 0 {
|
||||||
checker.add_check(Check::new(
|
checker.add_check(Check::new(
|
||||||
CheckKind::NoBlankLineBeforeFunction(blank_lines_before),
|
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(
|
checker.add_check(Check::new(
|
||||||
CheckKind::NoBlankLineAfterFunction(blank_lines_after),
|
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
|
} = &docstring.node
|
||||||
{
|
{
|
||||||
let (before, _, after) = checker
|
let (before, _, after) = checker.locator.partition_source_code_at(
|
||||||
.locator
|
&Range::from_located(parent),
|
||||||
.partition_source_code_at(&Range::from_located(parent), &range_for(docstring));
|
&helpers::range_for(docstring),
|
||||||
|
);
|
||||||
|
|
||||||
if checker.settings.enabled.contains(&CheckCode::D203)
|
if checker.settings.enabled.contains(&CheckCode::D203)
|
||||||
|| checker.settings.enabled.contains(&CheckCode::D211)
|
|| 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(
|
checker.add_check(Check::new(
|
||||||
CheckKind::NoBlankLineBeforeClass(blank_lines_before),
|
CheckKind::NoBlankLineBeforeClass(blank_lines_before),
|
||||||
range_for(docstring),
|
helpers::range_for(docstring),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if blank_lines_before != 1
|
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(
|
checker.add_check(Check::new(
|
||||||
CheckKind::OneBlankLineBeforeClass(blank_lines_before),
|
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 {
|
if !all_blank_after && blank_lines_after != 1 {
|
||||||
checker.add_check(Check::new(
|
checker.add_check(Check::new(
|
||||||
CheckKind::OneBlankLineAfterClass(blank_lines_after),
|
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 {
|
if lines_count > 1 && blanks_count != 1 {
|
||||||
checker.add_check(Check::new(
|
checker.add_check(Check::new(
|
||||||
CheckKind::NoBlankLineAfterSummary,
|
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 {
|
if line_count > 1 {
|
||||||
let content = checker
|
let content = checker
|
||||||
.locator
|
.locator
|
||||||
.slice_source_code_range(&range_for(docstring));
|
.slice_source_code_range(&helpers::range_for(docstring));
|
||||||
if let Some(line) = content.lines().last() {
|
if let Some(line) = content.lines().last() {
|
||||||
let line = line.trim();
|
let line = line.trim();
|
||||||
if line != "\"\"\"" && line != "'''" {
|
if line != "\"\"\"" && line != "'''" {
|
||||||
checker.add_check(Check::new(
|
checker.add_check(Check::new(
|
||||||
CheckKind::NewLineAfterLastParagraph,
|
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(' ')) {
|
if line.starts_with(' ') || (matches!(lines.next(), None) && line.ends_with(' ')) {
|
||||||
checker.add_check(Check::new(
|
checker.add_check(Check::new(
|
||||||
CheckKind::NoSurroundingWhitespace,
|
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() {
|
if string.lines().nth(1).is_some() {
|
||||||
let content = checker
|
let content = checker
|
||||||
.locator
|
.locator
|
||||||
.slice_source_code_range(&range_for(docstring));
|
.slice_source_code_range(&helpers::range_for(docstring));
|
||||||
if let Some(first_line) = content.lines().next() {
|
if let Some(first_line) = content.lines().next() {
|
||||||
let first_line = first_line.trim();
|
let first_line = first_line.trim();
|
||||||
if first_line == "\"\"\"" || first_line == "'''" {
|
if first_line == "\"\"\"" || first_line == "'''" {
|
||||||
if checker.settings.enabled.contains(&CheckCode::D212) {
|
if checker.settings.enabled.contains(&CheckCode::D212) {
|
||||||
checker.add_check(Check::new(
|
checker.add_check(Check::new(
|
||||||
CheckKind::MultiLineSummaryFirstLine,
|
CheckKind::MultiLineSummaryFirstLine,
|
||||||
range_for(docstring),
|
helpers::range_for(docstring),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
} else if checker.settings.enabled.contains(&CheckCode::D213) {
|
} else if checker.settings.enabled.contains(&CheckCode::D213) {
|
||||||
checker.add_check(Check::new(
|
checker.add_check(Check::new(
|
||||||
CheckKind::MultiLineSummarySecondLine,
|
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
|
let content = checker
|
||||||
.locator
|
.locator
|
||||||
.slice_source_code_range(&range_for(docstring));
|
.slice_source_code_range(&helpers::range_for(docstring));
|
||||||
if string.contains("\"\"\"") {
|
if string.contains("\"\"\"") {
|
||||||
if !content.starts_with("'''") {
|
if !content.starts_with("'''") {
|
||||||
checker.add_check(Check::new(
|
checker.add_check(Check::new(
|
||||||
CheckKind::UsesTripleQuotes,
|
CheckKind::UsesTripleQuotes,
|
||||||
range_for(docstring),
|
helpers::range_for(docstring),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
} else if !content.starts_with("\"\"\"") {
|
} else if !content.starts_with("\"\"\"") {
|
||||||
checker.add_check(Check::new(
|
checker.add_check(Check::new(
|
||||||
CheckKind::UsesTripleQuotes,
|
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 let Some(string) = string.lines().next() {
|
||||||
if !string.ends_with('.') {
|
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}(")) {
|
if first_line.contains(&format!("{name}(")) {
|
||||||
checker.add_check(Check::new(
|
checker.add_check(Check::new(
|
||||||
CheckKind::NoSignature,
|
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() {
|
if !first_char.is_uppercase() {
|
||||||
checker.add_check(Check::new(
|
checker.add_check(Check::new(
|
||||||
CheckKind::FirstLineCapitalized,
|
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()
|
.to_lowercase()
|
||||||
== "this"
|
== "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('?')) {
|
if !(string.ends_with('.') || string.ends_with('!') || string.ends_with('?')) {
|
||||||
checker.add_check(Check::new(
|
checker.add_check(Check::new(
|
||||||
CheckKind::EndsInPunctuation,
|
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 string.trim().is_empty() {
|
||||||
if checker.settings.enabled.contains(&CheckCode::D419) {
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -659,7 +590,8 @@ pub fn not_empty(checker: &mut Checker, definition: &Definition) -> bool {
|
||||||
true
|
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 Some(docstring) = definition.docstring {
|
||||||
if let ExprKind::Constant {
|
if let ExprKind::Constant {
|
||||||
value: Constant::Str(string),
|
value: Constant::Str(string),
|
||||||
|
@ -671,7 +603,7 @@ pub fn check_sections(checker: &mut Checker, definition: &Definition) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// First, try to interpret as NumPy-style sections.
|
// First, interpret as NumPy-style sections.
|
||||||
let mut found_numpy_section = false;
|
let mut found_numpy_section = false;
|
||||||
for context in §ion_contexts(&lines, &SectionStyle::NumPy) {
|
for context in §ion_contexts(&lines, &SectionStyle::NumPy) {
|
||||||
found_numpy_section = true;
|
found_numpy_section = true;
|
82
src/docstrings/extraction.rs
Normal file
82
src/docstrings/extraction.rs
Normal 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
150
src/docstrings/google.rs
Normal 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
37
src/docstrings/helpers.rs
Normal 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
112
src/docstrings/numpy.rs
Normal 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.
|
||||||
|
¤t_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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,176 +1,32 @@
|
||||||
use itertools::Itertools;
|
|
||||||
use std::collections::BTreeSet;
|
use std::collections::BTreeSet;
|
||||||
|
|
||||||
use once_cell::sync::Lazy;
|
use itertools::Itertools;
|
||||||
use regex::Regex;
|
use rustpython_ast::{Arg, StmtKind};
|
||||||
use rustpython_ast::{Arg, Expr, Location, StmtKind};
|
|
||||||
use titlecase::titlecase;
|
use titlecase::titlecase;
|
||||||
|
|
||||||
use crate::ast::types::Range;
|
use crate::ast::types::Range;
|
||||||
use crate::check_ast::Checker;
|
use crate::check_ast::Checker;
|
||||||
use crate::checks::{Check, CheckCode, CheckKind};
|
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::docstrings::types::{Definition, DefinitionKind};
|
||||||
use crate::visibility::is_static;
|
use crate::visibility::is_static;
|
||||||
|
|
||||||
static NUMPY_SECTION_NAMES: Lazy<BTreeSet<&'static str>> = Lazy::new(|| {
|
#[derive(Debug)]
|
||||||
BTreeSet::from([
|
pub(crate) struct SectionContext<'a> {
|
||||||
"Short Summary",
|
pub(crate) section_name: String,
|
||||||
"Extended Summary",
|
pub(crate) previous_line: &'a str,
|
||||||
"Parameters",
|
pub(crate) line: &'a str,
|
||||||
"Returns",
|
pub(crate) following_lines: &'a [&'a str],
|
||||||
"Yields",
|
pub(crate) is_last_section: bool,
|
||||||
"Other Parameters",
|
original_index: usize,
|
||||||
"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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn suspected_as_section(line: &str, style: &SectionStyle) -> bool {
|
fn suspected_as_section(line: &str, style: &SectionStyle) -> bool {
|
||||||
style
|
style
|
||||||
.lowercase_section_names()
|
.lowercase_section_names()
|
||||||
.contains(&leading_words(line).to_lowercase().as_str())
|
.contains(&helpers::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,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if the suspected context is really a section header.
|
/// 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.
|
/// 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
|
let suspected_section_indices: Vec<usize> = lines
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
|
@ -217,7 +76,7 @@ pub fn section_contexts<'a>(lines: &'a [&'a str], style: &SectionStyle) -> Vec<S
|
||||||
let mut contexts = vec![];
|
let mut contexts = vec![];
|
||||||
for lineno in suspected_section_indices {
|
for lineno in suspected_section_indices {
|
||||||
let context = SectionContext {
|
let context = SectionContext {
|
||||||
section_name: leading_words(lines[lineno]),
|
section_name: helpers::leading_words(lines[lineno]),
|
||||||
previous_line: lines[lineno - 1],
|
previous_line: lines[lineno - 1],
|
||||||
line: lines[lineno],
|
line: lines[lineno],
|
||||||
following_lines: &lines[lineno + 1..],
|
following_lines: &lines[lineno + 1..],
|
||||||
|
@ -335,7 +194,9 @@ fn check_blanks_and_section_underline(
|
||||||
}
|
}
|
||||||
|
|
||||||
if checker.settings.enabled.contains(&CheckCode::D215) {
|
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(
|
checker.add_check(Check::new(
|
||||||
CheckKind::SectionUnderlineNotOverIndented(context.section_name.to_string()),
|
CheckKind::SectionUnderlineNotOverIndented(context.section_name.to_string()),
|
||||||
range_for(docstring),
|
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,
|
checker: &mut Checker,
|
||||||
definition: &Definition,
|
definition: &Definition,
|
||||||
context: &SectionContext,
|
context: &SectionContext,
|
||||||
|
@ -404,7 +265,9 @@ fn check_common_section(
|
||||||
}
|
}
|
||||||
|
|
||||||
if checker.settings.enabled.contains(&CheckCode::D214) {
|
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(
|
checker.add_check(Check::new(
|
||||||
CheckKind::SectionNotOverIndented(context.section_name.to_string()),
|
CheckKind::SectionNotOverIndented(context.section_name.to_string()),
|
||||||
range_for(docstring),
|
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,
|
checker: &mut Checker,
|
||||||
definition: &Definition,
|
definition: &Definition,
|
||||||
docstrings_args: &BTreeSet<&str>,
|
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.
|
|
||||||
¤t_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
27
src/docstrings/styles.rs
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue