Split checks and plugins into source-related modules (#447)

This commit is contained in:
Charlie Marsh
2022-10-17 15:38:49 -04:00
committed by GitHub
parent 1f2ccb059a
commit 118a9feec8
49 changed files with 2083 additions and 2048 deletions

View File

@@ -2,16 +2,7 @@
use std::collections::BTreeSet;
use crate::ast::types::Range;
use once_cell::sync::Lazy;
use regex::Regex;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckCode, CheckKind};
use crate::docstrings::definition::Definition;
use crate::docstrings::sections;
use crate::docstrings::sections::SectionContext;
use crate::docstrings::styles::SectionStyle;
pub(crate) static GOOGLE_SECTION_NAMES: Lazy<BTreeSet<&'static str>> = Lazy::new(|| {
BTreeSet::from([
@@ -78,73 +69,3 @@ pub(crate) static LOWERCASE_GOOGLE_SECTION_NAMES: Lazy<BTreeSet<&'static str>> =
"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::from_located(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);
}
}
}

7
src/docstrings/mod.rs Normal file
View File

@@ -0,0 +1,7 @@
pub mod definition;
pub mod extraction;
pub mod google;
pub mod helpers;
pub mod numpy;
pub mod sections;
pub mod styles;

View File

@@ -2,16 +2,8 @@
use std::collections::BTreeSet;
use crate::ast::types::Range;
use once_cell::sync::Lazy;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckCode, CheckKind};
use crate::docstrings::definition::Definition;
use crate::docstrings::sections::SectionContext;
use crate::docstrings::styles::SectionStyle;
use crate::docstrings::{helpers, sections};
pub(crate) static LOWERCASE_NUMPY_SECTION_NAMES: Lazy<BTreeSet<&'static str>> = Lazy::new(|| {
BTreeSet::from([
"short summary",
@@ -47,68 +39,3 @@ pub(crate) static NUMPY_SECTION_NAMES: Lazy<BTreeSet<&'static str>> = Lazy::new(
"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::from_located(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,826 +0,0 @@
//! Abstractions for tracking and validating docstrings in Python code.
use once_cell::sync::Lazy;
use regex::Regex;
use rustpython_ast::{Constant, ExprKind, Location, StmtKind};
use crate::ast::types::Range;
use crate::autofix::fixer;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckCode, CheckKind, Fix};
use crate::docstrings::definition::{Definition, DefinitionKind};
use crate::docstrings::google::check_google_section;
use crate::docstrings::helpers::{
indentation, leading_space, SINGLE_QUOTE_PREFIXES, TRIPLE_QUOTE_PREFIXES,
};
use crate::docstrings::numpy::check_numpy_section;
use crate::docstrings::sections::section_contexts;
use crate::docstrings::styles::SectionStyle;
use crate::visibility::{is_init, is_magic, is_overload, Visibility};
/// D100, D101, D102, D103, D104, D105, D106, D107
pub fn not_missing(
checker: &mut Checker,
definition: &Definition,
visibility: &Visibility,
) -> bool {
if matches!(visibility, Visibility::Private) {
return true;
}
if definition.docstring.is_some() {
return true;
}
match definition.kind {
DefinitionKind::Module => {
if checker.settings.enabled.contains(&CheckCode::D100) {
checker.add_check(Check::new(
CheckKind::PublicModule,
Range {
location: Location::new(1, 1),
end_location: Location::new(1, 1),
},
));
}
false
}
DefinitionKind::Package => {
if checker.settings.enabled.contains(&CheckCode::D104) {
checker.add_check(Check::new(
CheckKind::PublicPackage,
Range {
location: Location::new(1, 1),
end_location: Location::new(1, 1),
},
));
}
false
}
DefinitionKind::Class(stmt) => {
if checker.settings.enabled.contains(&CheckCode::D101) {
checker.add_check(Check::new(
CheckKind::PublicClass,
Range::from_located(stmt),
));
}
false
}
DefinitionKind::NestedClass(stmt) => {
if checker.settings.enabled.contains(&CheckCode::D106) {
checker.add_check(Check::new(
CheckKind::PublicNestedClass,
Range::from_located(stmt),
));
}
false
}
DefinitionKind::Function(stmt) | DefinitionKind::NestedFunction(stmt) => {
if is_overload(stmt) {
true
} else {
if checker.settings.enabled.contains(&CheckCode::D103) {
checker.add_check(Check::new(
CheckKind::PublicFunction,
Range::from_located(stmt),
));
}
false
}
}
DefinitionKind::Method(stmt) => {
if is_overload(stmt) {
true
} else if is_magic(stmt) {
if checker.settings.enabled.contains(&CheckCode::D105) {
checker.add_check(Check::new(
CheckKind::MagicMethod,
Range::from_located(stmt),
));
}
true
} else if is_init(stmt) {
if checker.settings.enabled.contains(&CheckCode::D107) {
checker.add_check(Check::new(CheckKind::PublicInit, Range::from_located(stmt)));
}
true
} else {
if checker.settings.enabled.contains(&CheckCode::D102) {
checker.add_check(Check::new(
CheckKind::PublicMethod,
Range::from_located(stmt),
));
}
true
}
}
}
}
/// D200
pub fn one_liner(checker: &mut Checker, definition: &Definition) {
if let Some(docstring) = &definition.docstring {
if let ExprKind::Constant {
value: Constant::Str(string),
..
} = &docstring.node
{
let mut line_count = 0;
let mut non_empty_line_count = 0;
for line in string.lines() {
line_count += 1;
if !line.trim().is_empty() {
non_empty_line_count += 1;
}
if non_empty_line_count > 1 {
break;
}
}
if non_empty_line_count == 1 && line_count > 1 {
checker.add_check(Check::new(
CheckKind::FitsOnOneLine,
Range::from_located(docstring),
));
}
}
}
}
static COMMENT_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^\s*#").unwrap());
static INNER_FUNCTION_OR_CLASS_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^\s+(?:(?:class|def|async def)\s|@)").unwrap());
/// D201, D202
pub fn blank_before_after_function(checker: &mut Checker, definition: &Definition) {
if let Some(docstring) = definition.docstring {
if let DefinitionKind::Function(parent)
| DefinitionKind::NestedFunction(parent)
| DefinitionKind::Method(parent) = &definition.kind
{
if let ExprKind::Constant {
value: Constant::Str(_),
..
} = &docstring.node
{
let (before, _, after) = checker.locator.partition_source_code_at(
&Range::from_located(parent),
&Range::from_located(docstring),
);
if checker.settings.enabled.contains(&CheckCode::D201) {
let blank_lines_before = before
.lines()
.rev()
.skip(1)
.take_while(|line| line.trim().is_empty())
.count();
if blank_lines_before != 0 {
let mut check = Check::new(
CheckKind::NoBlankLineBeforeFunction(blank_lines_before),
Range::from_located(docstring),
);
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
check.amend(Fix::deletion(
Location::new(docstring.location.row() - blank_lines_before, 1),
Location::new(docstring.location.row(), 1),
));
}
checker.add_check(check);
}
}
if checker.settings.enabled.contains(&CheckCode::D202) {
let all_blank_after = after
.lines()
.skip(1)
.all(|line| line.trim().is_empty() || COMMENT_REGEX.is_match(line));
if all_blank_after {
return;
}
let blank_lines_after = after
.lines()
.skip(1)
.take_while(|line| line.trim().is_empty())
.count();
// Report a D202 violation if the docstring is followed by a blank line and the
// blank line is not itself followed by an inner function or class.
let expected_blank_lines_after =
if INNER_FUNCTION_OR_CLASS_REGEX.is_match(after) {
1
} else {
0
};
if blank_lines_after != expected_blank_lines_after {
let mut check = Check::new(
CheckKind::NoBlankLineAfterFunction(blank_lines_after),
Range::from_located(docstring),
);
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
check.amend(Fix::deletion(
Location::new(
docstring.location.row() + 1 + expected_blank_lines_after,
1,
),
Location::new(docstring.location.row() + 1 + blank_lines_after, 1),
));
}
checker.add_check(check);
}
}
}
}
}
}
/// D203, D204, D211
pub fn blank_before_after_class(checker: &mut Checker, definition: &Definition) {
if let Some(docstring) = &definition.docstring {
if let DefinitionKind::Class(parent) | DefinitionKind::NestedClass(parent) =
&definition.kind
{
if let ExprKind::Constant {
value: Constant::Str(_),
..
} = &docstring.node
{
let (before, _, after) = checker.locator.partition_source_code_at(
&Range::from_located(parent),
&Range::from_located(docstring),
);
if checker.settings.enabled.contains(&CheckCode::D203)
|| checker.settings.enabled.contains(&CheckCode::D211)
{
let blank_lines_before = before
.lines()
.rev()
.skip(1)
.take_while(|line| line.trim().is_empty())
.count();
if checker.settings.enabled.contains(&CheckCode::D211) {
if blank_lines_before != 0 {
let mut check = Check::new(
CheckKind::NoBlankLineBeforeClass(blank_lines_before),
Range::from_located(docstring),
);
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply)
{
check.amend(Fix::deletion(
Location::new(docstring.location.row() - blank_lines_before, 1),
Location::new(docstring.location.row(), 1),
));
}
checker.add_check(check);
}
}
if checker.settings.enabled.contains(&CheckCode::D203) {
if blank_lines_before != 1 {
let mut check = Check::new(
CheckKind::OneBlankLineBeforeClass(blank_lines_before),
Range::from_located(docstring),
);
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply)
{
check.amend(Fix::replacement(
"\n".to_string(),
Location::new(docstring.location.row() - blank_lines_before, 1),
Location::new(docstring.location.row(), 1),
));
}
checker.add_check(check);
}
}
}
if checker.settings.enabled.contains(&CheckCode::D204) {
let all_blank_after = after
.lines()
.skip(1)
.all(|line| line.trim().is_empty() || COMMENT_REGEX.is_match(line));
if all_blank_after {
return;
}
let blank_lines_after = after
.lines()
.skip(1)
.take_while(|line| line.trim().is_empty())
.count();
if blank_lines_after != 1 {
let mut check = Check::new(
CheckKind::OneBlankLineAfterClass(blank_lines_after),
Range::from_located(docstring),
);
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
check.amend(Fix::replacement(
"\n".to_string(),
Location::new(docstring.end_location.unwrap().row() + 1, 1),
Location::new(
docstring.end_location.unwrap().row() + 1 + blank_lines_after,
1,
),
));
}
checker.add_check(check);
}
}
}
}
}
}
/// D205
pub fn blank_after_summary(checker: &mut Checker, definition: &Definition) {
if let Some(docstring) = definition.docstring {
if let ExprKind::Constant {
value: Constant::Str(string),
..
} = &docstring.node
{
let mut lines_count = 1;
let mut blanks_count = 0;
for line in string.trim().lines().skip(1) {
lines_count += 1;
if line.trim().is_empty() {
blanks_count += 1;
} else {
break;
}
}
if lines_count > 1 && blanks_count != 1 {
let mut check = Check::new(
CheckKind::NoBlankLineAfterSummary,
Range::from_located(docstring),
);
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
check.amend(Fix::replacement(
"\n".to_string(),
Location::new(docstring.location.row() + 1, 1),
Location::new(docstring.location.row() + 1 + blanks_count, 1),
));
}
checker.add_check(check);
}
}
}
}
/// D206, D207, D208
pub fn indent(checker: &mut Checker, definition: &Definition) {
if let Some(docstring) = definition.docstring {
if let ExprKind::Constant {
value: Constant::Str(string),
..
} = &docstring.node
{
let lines: Vec<&str> = string.lines().collect();
if lines.len() <= 1 {
return;
}
let mut has_seen_tab = false;
let mut has_seen_over_indent = false;
let mut has_seen_under_indent = false;
let docstring_indent = indentation(checker, docstring).to_string();
if !has_seen_tab {
if docstring_indent.contains('\t') {
if checker.settings.enabled.contains(&CheckCode::D206) {
checker.add_check(Check::new(
CheckKind::IndentWithSpaces,
Range::from_located(docstring),
));
}
has_seen_tab = true;
}
}
for i in 0..lines.len() {
// First lines and continuations doesn't need any indentation.
if i == 0 || lines[i - 1].ends_with('\\') {
continue;
}
// Omit empty lines, except for the last line, which is non-empty by way of
// containing the closing quotation marks.
if i < lines.len() - 1 && lines[i].trim().is_empty() {
continue;
}
let line_indent = leading_space(lines[i]);
if !has_seen_tab {
if line_indent.contains('\t') {
if checker.settings.enabled.contains(&CheckCode::D206) {
checker.add_check(Check::new(
CheckKind::IndentWithSpaces,
Range::from_located(docstring),
));
}
has_seen_tab = true;
}
}
if !has_seen_over_indent {
if line_indent.len() > docstring_indent.len() {
if checker.settings.enabled.contains(&CheckCode::D208) {
checker.add_check(Check::new(
CheckKind::NoOverIndentation,
Range::from_located(docstring),
));
}
has_seen_over_indent = true;
}
}
if !has_seen_under_indent {
if line_indent.len() < docstring_indent.len() {
if checker.settings.enabled.contains(&CheckCode::D207) {
checker.add_check(Check::new(
CheckKind::NoUnderIndentation,
Range::from_located(docstring),
));
}
has_seen_under_indent = true;
}
}
}
}
}
}
/// D209
pub fn newline_after_last_paragraph(checker: &mut Checker, definition: &Definition) {
if let Some(docstring) = definition.docstring {
if let ExprKind::Constant {
value: Constant::Str(string),
..
} = &docstring.node
{
let mut line_count = 0;
for line in string.lines() {
if !line.trim().is_empty() {
line_count += 1;
}
if line_count > 1 {
let content = checker
.locator
.slice_source_code_range(&Range::from_located(docstring));
if let Some(last_line) = content.lines().last().map(|line| line.trim()) {
if last_line != "\"\"\"" && last_line != "'''" {
let mut check = Check::new(
CheckKind::NewLineAfterLastParagraph,
Range::from_located(docstring),
);
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply)
{
// Insert a newline just before the end-quote(s).
let mut content = "\n".to_string();
content.push_str(indentation(checker, docstring));
check.amend(Fix::insertion(
content,
Location::new(
docstring.end_location.unwrap().row(),
docstring.end_location.unwrap().column() - "\"\"\"".len(),
),
));
}
checker.add_check(check);
}
}
return;
}
}
}
}
}
/// D210
pub fn no_surrounding_whitespace(checker: &mut Checker, definition: &Definition) {
if let Some(docstring) = definition.docstring {
if let ExprKind::Constant {
value: Constant::Str(string),
..
} = &docstring.node
{
let mut lines = string.lines();
if let Some(line) = lines.next() {
let trimmed = line.trim();
if trimmed.is_empty() {
return;
}
if line != trimmed {
let mut check = Check::new(
CheckKind::NoSurroundingWhitespace,
Range::from_located(docstring),
);
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
if let Some(first_line) = checker
.locator
.slice_source_code_range(&Range::from_located(docstring))
.lines()
.next()
.map(|line| line.to_lowercase())
{
for pattern in TRIPLE_QUOTE_PREFIXES.iter().chain(SINGLE_QUOTE_PREFIXES)
{
if first_line.starts_with(pattern) {
check.amend(Fix::replacement(
trimmed.to_string(),
Location::new(
docstring.location.row(),
docstring.location.column() + pattern.len(),
),
Location::new(
docstring.location.row(),
docstring.location.column()
+ pattern.len()
+ line.chars().count(),
),
));
break;
}
}
}
}
checker.add_check(check);
}
}
}
}
}
/// D212, D213
pub fn multi_line_summary_start(checker: &mut Checker, definition: &Definition) {
if let Some(docstring) = definition.docstring {
if let ExprKind::Constant {
value: Constant::Str(string),
..
} = &docstring.node
{
if string.lines().nth(1).is_some() {
if let Some(first_line) = checker
.locator
.slice_source_code_range(&Range::from_located(docstring))
.lines()
.next()
.map(|line| line.to_lowercase())
{
if TRIPLE_QUOTE_PREFIXES.contains(&first_line.as_str()) {
if checker.settings.enabled.contains(&CheckCode::D212) {
checker.add_check(Check::new(
CheckKind::MultiLineSummaryFirstLine,
Range::from_located(docstring),
));
}
} else {
if checker.settings.enabled.contains(&CheckCode::D213) {
checker.add_check(Check::new(
CheckKind::MultiLineSummarySecondLine,
Range::from_located(docstring),
));
}
}
}
}
}
}
}
/// D300
pub fn triple_quotes(checker: &mut Checker, definition: &Definition) {
if let Some(docstring) = definition.docstring {
if let ExprKind::Constant {
value: Constant::Str(string),
..
} = &docstring.node
{
if let Some(first_line) = checker
.locator
.slice_source_code_range(&Range::from_located(docstring))
.lines()
.next()
.map(|line| line.to_lowercase())
{
let starts_with_triple = if string.contains("\"\"\"") {
first_line.starts_with("'''")
|| first_line.starts_with("u'''")
|| first_line.starts_with("r'''")
|| first_line.starts_with("ur'''")
} else {
first_line.starts_with("\"\"\"")
|| first_line.starts_with("u\"\"\"")
|| first_line.starts_with("r\"\"\"")
|| first_line.starts_with("ur\"\"\"")
};
if !starts_with_triple {
checker.add_check(Check::new(
CheckKind::UsesTripleQuotes,
Range::from_located(docstring),
));
}
}
}
}
}
/// D400
pub fn ends_with_period(checker: &mut Checker, definition: &Definition) {
if let Some(docstring) = definition.docstring {
if let ExprKind::Constant {
value: Constant::Str(string),
..
} = &docstring.node
{
if let Some(string) = string.lines().next() {
if !string.ends_with('.') {
checker.add_check(Check::new(
CheckKind::EndsInPeriod,
Range::from_located(docstring),
));
}
}
}
}
}
/// D402
pub fn no_signature(checker: &mut Checker, definition: &Definition) {
if let Some(docstring) = definition.docstring {
if let DefinitionKind::Function(parent)
| DefinitionKind::NestedFunction(parent)
| DefinitionKind::Method(parent) = definition.kind
{
if let StmtKind::FunctionDef { name, .. } = &parent.node {
if let ExprKind::Constant {
value: Constant::Str(string),
..
} = &docstring.node
{
if let Some(first_line) = string.lines().next() {
if first_line.contains(&format!("{name}(")) {
checker.add_check(Check::new(
CheckKind::NoSignature,
Range::from_located(docstring),
));
}
}
}
}
}
}
}
/// D403
pub fn capitalized(checker: &mut Checker, definition: &Definition) {
if !matches!(definition.kind, DefinitionKind::Function(_)) {
return;
}
if let Some(docstring) = definition.docstring {
if let ExprKind::Constant {
value: Constant::Str(string),
..
} = &docstring.node
{
if let Some(first_word) = string.split(' ').next() {
if first_word == first_word.to_uppercase() {
return;
}
for char in first_word.chars() {
if !char.is_ascii_alphabetic() && char != '\'' {
return;
}
}
if let Some(first_char) = first_word.chars().next() {
if !first_char.is_uppercase() {
checker.add_check(Check::new(
CheckKind::FirstLineCapitalized,
Range::from_located(docstring),
));
}
}
}
}
}
}
/// D404
pub fn starts_with_this(checker: &mut Checker, definition: &Definition) {
if let Some(docstring) = definition.docstring {
if let ExprKind::Constant {
value: Constant::Str(string),
..
} = &docstring.node
{
let trimmed = string.trim();
if trimmed.is_empty() {
return;
}
if let Some(first_word) = string.split(' ').next() {
if first_word
.replace(|c: char| !c.is_alphanumeric(), "")
.to_lowercase()
== "this"
{
checker.add_check(Check::new(
CheckKind::NoThisPrefix,
Range::from_located(docstring),
));
}
}
}
}
}
/// D415
pub fn ends_with_punctuation(checker: &mut Checker, definition: &Definition) {
if let Some(docstring) = definition.docstring {
if let ExprKind::Constant {
value: Constant::Str(string),
..
} = &docstring.node
{
if let Some(string) = string.lines().next() {
if !(string.ends_with('.') || string.ends_with('!') || string.ends_with('?')) {
checker.add_check(Check::new(
CheckKind::EndsInPunctuation,
Range::from_located(docstring),
));
}
}
}
}
}
/// D418
pub fn if_needed(checker: &mut Checker, definition: &Definition) {
if definition.docstring.is_some() {
if let DefinitionKind::Function(stmt)
| DefinitionKind::NestedFunction(stmt)
| DefinitionKind::Method(stmt) = definition.kind
{
if is_overload(stmt) {
checker.add_check(Check::new(
CheckKind::SkipDocstring,
Range::from_located(stmt),
));
}
}
}
}
/// D419
pub fn not_empty(checker: &mut Checker, definition: &Definition) -> bool {
if let Some(docstring) = definition.docstring {
if let ExprKind::Constant {
value: Constant::Str(string),
..
} = &docstring.node
{
if string.trim().is_empty() {
if checker.settings.enabled.contains(&CheckCode::D419) {
checker.add_check(Check::new(
CheckKind::NonEmpty,
Range::from_located(docstring),
));
}
return false;
}
}
}
true
}
/// 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),
..
} = &docstring.node
{
let lines: Vec<&str> = string.lines().collect();
if lines.len() < 2 {
return;
}
// First, interpret as NumPy-style sections.
let mut found_numpy_section = false;
for context in &section_contexts(&lines, &SectionStyle::NumPy) {
found_numpy_section = true;
check_numpy_section(checker, definition, context);
}
// If no such sections were identified, interpret as Google-style sections.
if !found_numpy_section {
for context in &section_contexts(&lines, &SectionStyle::Google) {
check_google_section(checker, definition, context);
}
}
}
}
}

View File

@@ -1,17 +1,5 @@
use std::collections::BTreeSet;
use itertools::Itertools;
use rustpython_ast::{Arg, Location, StmtKind};
use titlecase::titlecase;
use crate::ast::types::Range;
use crate::autofix::fixer;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckCode, CheckKind, Fix};
use crate::docstrings::definition::{Definition, DefinitionKind};
use crate::docstrings::helpers;
use crate::docstrings::styles::SectionStyle;
use crate::visibility::is_static;
#[derive(Debug)]
pub(crate) struct SectionContext<'a> {
@@ -20,7 +8,7 @@ pub(crate) struct SectionContext<'a> {
pub(crate) line: &'a str,
pub(crate) following_lines: &'a [&'a str],
pub(crate) is_last_section: bool,
original_index: usize,
pub(crate) original_index: usize,
}
fn suspected_as_section(line: &str, style: &SectionStyle) -> bool {
@@ -109,295 +97,3 @@ pub(crate) fn section_contexts<'a>(
truncated_contexts.reverse();
truncated_contexts
}
fn check_blanks_and_section_underline(
checker: &mut Checker,
definition: &Definition,
context: &SectionContext,
) {
let docstring = definition
.docstring
.expect("Sections are only available for docstrings.");
let mut blank_lines_after_header = 0;
for line in context.following_lines {
if !line.trim().is_empty() {
break;
}
blank_lines_after_header += 1;
}
// Nothing but blank lines after the section header.
if blank_lines_after_header == context.following_lines.len() {
if checker.settings.enabled.contains(&CheckCode::D407) {
checker.add_check(Check::new(
CheckKind::DashedUnderlineAfterSection(context.section_name.to_string()),
Range::from_located(docstring),
));
}
if checker.settings.enabled.contains(&CheckCode::D414) {
checker.add_check(Check::new(
CheckKind::NonEmptySection(context.section_name.to_string()),
Range::from_located(docstring),
));
}
return;
}
let non_empty_line = context.following_lines[blank_lines_after_header];
let dash_line_found = non_empty_line
.chars()
.all(|char| char.is_whitespace() || char == '-');
if !dash_line_found {
if checker.settings.enabled.contains(&CheckCode::D407) {
checker.add_check(Check::new(
CheckKind::DashedUnderlineAfterSection(context.section_name.to_string()),
Range::from_located(docstring),
));
}
if blank_lines_after_header > 0 {
if checker.settings.enabled.contains(&CheckCode::D212) {
checker.add_check(Check::new(
CheckKind::NoBlankLinesBetweenHeaderAndContent(
context.section_name.to_string(),
),
Range::from_located(docstring),
));
}
}
} else {
if blank_lines_after_header > 0 {
if checker.settings.enabled.contains(&CheckCode::D408) {
checker.add_check(Check::new(
CheckKind::SectionUnderlineAfterName(context.section_name.to_string()),
Range::from_located(docstring),
));
}
}
if non_empty_line
.trim()
.chars()
.filter(|char| *char == '-')
.count()
!= context.section_name.len()
{
if checker.settings.enabled.contains(&CheckCode::D409) {
checker.add_check(Check::new(
CheckKind::SectionUnderlineMatchesSectionLength(
context.section_name.to_string(),
),
Range::from_located(docstring),
));
}
}
if checker.settings.enabled.contains(&CheckCode::D215) {
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::from_located(docstring),
));
}
}
let line_after_dashes_index = blank_lines_after_header + 1;
if line_after_dashes_index < context.following_lines.len() {
let line_after_dashes = context.following_lines[line_after_dashes_index];
if line_after_dashes.trim().is_empty() {
let rest_of_lines = &context.following_lines[line_after_dashes_index..];
if rest_of_lines.iter().all(|line| line.trim().is_empty()) {
if checker.settings.enabled.contains(&CheckCode::D414) {
checker.add_check(Check::new(
CheckKind::NonEmptySection(context.section_name.to_string()),
Range::from_located(docstring),
));
}
} else {
if checker.settings.enabled.contains(&CheckCode::D412) {
checker.add_check(Check::new(
CheckKind::NoBlankLinesBetweenHeaderAndContent(
context.section_name.to_string(),
),
Range::from_located(docstring),
));
}
}
}
} else {
if checker.settings.enabled.contains(&CheckCode::D414) {
checker.add_check(Check::new(
CheckKind::NonEmptySection(context.section_name.to_string()),
Range::from_located(docstring),
));
}
}
}
}
pub(crate) fn check_common_section(
checker: &mut Checker,
definition: &Definition,
context: &SectionContext,
style: &SectionStyle,
) {
let docstring = definition
.docstring
.expect("Sections are only available for docstrings.");
if checker.settings.enabled.contains(&CheckCode::D405) {
if !style
.section_names()
.contains(&context.section_name.as_str())
&& style
.section_names()
.contains(titlecase(&context.section_name).as_str())
{
checker.add_check(Check::new(
CheckKind::CapitalizeSectionName(context.section_name.to_string()),
Range::from_located(docstring),
))
}
}
if checker.settings.enabled.contains(&CheckCode::D214) {
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::from_located(docstring),
))
}
}
if context
.following_lines
.last()
.map(|line| !line.trim().is_empty())
.unwrap_or(true)
{
if context.is_last_section {
if checker.settings.enabled.contains(&CheckCode::D413) {
let mut check = Check::new(
CheckKind::BlankLineAfterLastSection(context.section_name.to_string()),
Range::from_located(docstring),
);
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
check.amend(Fix::insertion(
"\n".to_string(),
Location::new(
docstring.location.row()
+ context.original_index
+ 1
+ context.following_lines.len(),
1,
),
))
}
checker.add_check(check);
}
} else {
if checker.settings.enabled.contains(&CheckCode::D410) {
let mut check = Check::new(
CheckKind::BlankLineAfterSection(context.section_name.to_string()),
Range::from_located(docstring),
);
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
check.amend(Fix::insertion(
"\n".to_string(),
Location::new(
docstring.location.row()
+ context.original_index
+ 1
+ context.following_lines.len(),
1,
),
))
}
checker.add_check(check);
}
}
}
if checker.settings.enabled.contains(&CheckCode::D411) {
if !context.previous_line.is_empty() {
checker.add_check(Check::new(
CheckKind::BlankLineBeforeSection(context.section_name.to_string()),
Range::from_located(docstring),
))
}
}
check_blanks_and_section_underline(checker, definition, context);
}
pub(crate) fn check_missing_args(
checker: &mut Checker,
definition: &Definition,
docstrings_args: &BTreeSet<&str>,
) {
if let DefinitionKind::Function(parent)
| DefinitionKind::NestedFunction(parent)
| DefinitionKind::Method(parent) = definition.kind
{
if let StmtKind::FunctionDef {
args: arguments, ..
}
| StmtKind::AsyncFunctionDef {
args: arguments, ..
} = &parent.node
{
// Collect all the arguments into a single vector.
let mut all_arguments: Vec<&Arg> = arguments
.args
.iter()
.chain(arguments.posonlyargs.iter())
.chain(arguments.kwonlyargs.iter())
.skip(
// If this is a non-static method, skip `cls` or `self`.
if matches!(definition.kind, DefinitionKind::Method(_)) && !is_static(parent) {
1
} else {
0
},
)
.collect();
if let Some(arg) = &arguments.vararg {
all_arguments.push(arg);
}
if let Some(arg) = &arguments.kwarg {
all_arguments.push(arg);
}
// Look for arguments that weren't included in the docstring.
let mut missing_args: BTreeSet<&str> = Default::default();
for arg in all_arguments {
let arg_name = arg.node.arg.as_str();
if arg_name.starts_with('_') {
continue;
}
if docstrings_args.contains(&arg_name) {
continue;
}
missing_args.insert(arg_name);
}
if !missing_args.is_empty() {
let names = missing_args
.into_iter()
.map(String::from)
.sorted()
.collect();
checker.add_check(Check::new(
CheckKind::DocumentAllArguments(names),
Range::from_located(parent),
));
}
}
}
}