mirror of https://github.com/astral-sh/ruff
Enable definition tracking for docstrings (#407)
This commit is contained in:
parent
590aa92ead
commit
1a68a38306
|
|
@ -21,7 +21,7 @@ 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, DocstringKind};
|
use crate::docstrings::{Definition, DefinitionKind, Documentable};
|
||||||
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};
|
||||||
|
|
@ -31,7 +31,7 @@ pub const GLOBAL_SCOPE_INDEX: usize = 0;
|
||||||
|
|
||||||
pub struct Checker<'a> {
|
pub struct Checker<'a> {
|
||||||
// Input data.
|
// Input data.
|
||||||
path: &'a Path,
|
pub(crate) path: &'a Path,
|
||||||
// TODO(charlie): Separate immutable from mutable state. (None of these should ever change.)
|
// TODO(charlie): Separate immutable from mutable state. (None of these should ever change.)
|
||||||
pub(crate) locator: SourceCodeLocator<'a>,
|
pub(crate) locator: SourceCodeLocator<'a>,
|
||||||
pub(crate) settings: &'a Settings,
|
pub(crate) settings: &'a Settings,
|
||||||
|
|
@ -39,7 +39,7 @@ pub struct Checker<'a> {
|
||||||
// Computed checks.
|
// Computed checks.
|
||||||
checks: Vec<Check>,
|
checks: Vec<Check>,
|
||||||
// Docstring tracking.
|
// Docstring tracking.
|
||||||
docstrings: Vec<Docstring<'a>>,
|
docstrings: Vec<Definition<'a>>,
|
||||||
// Edit tracking.
|
// Edit tracking.
|
||||||
// TODO(charlie): Instead of exposing deletions, wrap in a public API.
|
// TODO(charlie): Instead of exposing deletions, wrap in a public API.
|
||||||
pub(crate) deletions: BTreeSet<usize>,
|
pub(crate) deletions: BTreeSet<usize>,
|
||||||
|
|
@ -125,36 +125,8 @@ where
|
||||||
StmtKind::Import { .. } => {
|
StmtKind::Import { .. } => {
|
||||||
self.futures_allowed = false;
|
self.futures_allowed = false;
|
||||||
}
|
}
|
||||||
StmtKind::Expr { value } => {
|
|
||||||
// Track all docstrings: module-, class-, and function-level.
|
|
||||||
let mut is_module_docstring = false;
|
|
||||||
if matches!(
|
|
||||||
&value.node,
|
|
||||||
ExprKind::Constant {
|
|
||||||
value: Constant::Str(_),
|
|
||||||
..
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
if let Some(docstring) = docstrings::extract(self, stmt, value) {
|
|
||||||
if matches!(&docstring.kind, DocstringKind::Module) {
|
|
||||||
is_module_docstring = true;
|
|
||||||
}
|
|
||||||
self.docstrings.push(docstring);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !is_module_docstring {
|
|
||||||
if !self.seen_import_boundary
|
|
||||||
&& !operations::in_nested_block(&self.parent_stack, &self.parents)
|
|
||||||
{
|
|
||||||
self.seen_import_boundary = true;
|
|
||||||
}
|
|
||||||
self.futures_allowed = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
node => {
|
node => {
|
||||||
self.futures_allowed = false;
|
self.futures_allowed = false;
|
||||||
|
|
||||||
if !self.seen_import_boundary
|
if !self.seen_import_boundary
|
||||||
&& !helpers::is_assignment_to_a_dunder(node)
|
&& !helpers::is_assignment_to_a_dunder(node)
|
||||||
&& !operations::in_nested_block(&self.parent_stack, &self.parents)
|
&& !operations::in_nested_block(&self.parent_stack, &self.parents)
|
||||||
|
|
@ -594,7 +566,23 @@ where
|
||||||
|
|
||||||
// Recurse.
|
// Recurse.
|
||||||
match &stmt.node {
|
match &stmt.node {
|
||||||
StmtKind::FunctionDef { .. } | StmtKind::AsyncFunctionDef { .. } => {
|
StmtKind::FunctionDef { body, .. } | StmtKind::AsyncFunctionDef { body, .. } => {
|
||||||
|
// TODO(charlie): Track public / private.
|
||||||
|
// Grab all parents, to enable nested definition tracking. (Ignore the most recent
|
||||||
|
// parent which, confusingly, is `stmt`.)
|
||||||
|
let parents = self
|
||||||
|
.parent_stack
|
||||||
|
.iter()
|
||||||
|
.take(self.parents.len() - 1)
|
||||||
|
.map(|index| self.parents[*index])
|
||||||
|
.collect();
|
||||||
|
self.docstrings.push(docstrings::extract(
|
||||||
|
parents,
|
||||||
|
stmt,
|
||||||
|
body,
|
||||||
|
Documentable::Function,
|
||||||
|
));
|
||||||
|
|
||||||
self.deferred_functions.push((
|
self.deferred_functions.push((
|
||||||
stmt,
|
stmt,
|
||||||
self.scope_stack.clone(),
|
self.scope_stack.clone(),
|
||||||
|
|
@ -602,6 +590,22 @@ where
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
StmtKind::ClassDef { body, .. } => {
|
StmtKind::ClassDef { body, .. } => {
|
||||||
|
// TODO(charlie): Track public / priva
|
||||||
|
// Grab all parents, to enable nested definition tracking. (Ignore the most recent
|
||||||
|
// parent which, confusingly, is `stmt`.)
|
||||||
|
let parents = self
|
||||||
|
.parent_stack
|
||||||
|
.iter()
|
||||||
|
.take(self.parents.len() - 1)
|
||||||
|
.map(|index| self.parents[*index])
|
||||||
|
.collect();
|
||||||
|
self.docstrings.push(docstrings::extract(
|
||||||
|
parents,
|
||||||
|
stmt,
|
||||||
|
body,
|
||||||
|
Documentable::Class,
|
||||||
|
));
|
||||||
|
|
||||||
for stmt in body {
|
for stmt in body {
|
||||||
self.visit_stmt(stmt);
|
self.visit_stmt(stmt);
|
||||||
}
|
}
|
||||||
|
|
@ -1643,6 +1647,22 @@ impl<'a> Checker<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn visit_docstring<'b>(&mut self, python_ast: &'b Suite) -> bool
|
||||||
|
where
|
||||||
|
'b: 'a,
|
||||||
|
{
|
||||||
|
let docstring = docstrings::docstring_from(python_ast);
|
||||||
|
self.docstrings.push(Definition {
|
||||||
|
kind: if self.path.ends_with("__init__.py") {
|
||||||
|
DefinitionKind::Package
|
||||||
|
} else {
|
||||||
|
DefinitionKind::Module
|
||||||
|
},
|
||||||
|
docstring,
|
||||||
|
});
|
||||||
|
docstring.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
fn check_deferred_annotations(&mut self) {
|
fn check_deferred_annotations(&mut self) {
|
||||||
while let Some((expr, scopes, parents)) = self.deferred_annotations.pop() {
|
while let Some((expr, scopes, parents)) = self.deferred_annotations.pop() {
|
||||||
self.parent_stack = parents;
|
self.parent_stack = parents;
|
||||||
|
|
@ -1988,6 +2008,13 @@ pub fn check_ast(
|
||||||
checker.push_scope(Scope::new(ScopeKind::Module));
|
checker.push_scope(Scope::new(ScopeKind::Module));
|
||||||
checker.bind_builtins();
|
checker.bind_builtins();
|
||||||
|
|
||||||
|
// Check for module docstring.
|
||||||
|
let python_ast = if checker.visit_docstring(python_ast) {
|
||||||
|
&python_ast[1..]
|
||||||
|
} else {
|
||||||
|
python_ast
|
||||||
|
};
|
||||||
|
|
||||||
// Iterate over the AST.
|
// Iterate over the AST.
|
||||||
for stmt in python_ast {
|
for stmt in python_ast {
|
||||||
checker.visit_stmt(stmt);
|
checker.visit_stmt(stmt);
|
||||||
|
|
|
||||||
|
|
@ -7,92 +7,117 @@ use crate::check_ast::Checker;
|
||||||
use crate::checks::{Check, CheckCode, CheckKind};
|
use crate::checks::{Check, CheckCode, CheckKind};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum DocstringKind<'a> {
|
pub enum DefinitionKind<'a> {
|
||||||
Module,
|
Module,
|
||||||
Function(&'a Stmt),
|
Package,
|
||||||
Class(&'a Stmt),
|
Class(&'a Stmt),
|
||||||
|
NestedClass(&'a Stmt),
|
||||||
|
Function(&'a Stmt),
|
||||||
|
NestedFunction(&'a Stmt),
|
||||||
|
Method(&'a Stmt),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Docstring<'a> {
|
pub struct Definition<'a> {
|
||||||
pub kind: DocstringKind<'a>,
|
pub kind: DefinitionKind<'a>,
|
||||||
pub expr: &'a Expr,
|
pub docstring: Option<&'a Expr>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extract a `Docstring` from an `Expr`.
|
pub enum Documentable {
|
||||||
pub fn extract<'a, 'b>(
|
Class,
|
||||||
checker: &'a Checker<'b>,
|
Function,
|
||||||
stmt: &'b Stmt,
|
}
|
||||||
expr: &'b Expr,
|
|
||||||
) -> Option<Docstring<'b>> {
|
fn nest(parents: &[&Stmt]) -> Option<Documentable> {
|
||||||
let defined_in = checker
|
for parent in parents.iter().rev() {
|
||||||
.binding_context()
|
match &parent.node {
|
||||||
.defined_in
|
StmtKind::FunctionDef { .. } | StmtKind::AsyncFunctionDef { .. } => {
|
||||||
.map(|index| checker.parents[index]);
|
return Some(Documentable::Function)
|
||||||
match defined_in {
|
|
||||||
None => {
|
|
||||||
if checker.initial {
|
|
||||||
return Some(Docstring {
|
|
||||||
kind: DocstringKind::Module,
|
|
||||||
expr,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(parent) => {
|
|
||||||
if let StmtKind::FunctionDef { body, .. }
|
|
||||||
| StmtKind::AsyncFunctionDef { body, .. }
|
|
||||||
| StmtKind::ClassDef { body, .. } = &parent.node
|
|
||||||
{
|
|
||||||
if body.first().map(|node| node == stmt).unwrap_or_default() {
|
|
||||||
return Some(Docstring {
|
|
||||||
kind: if matches!(&parent.node, StmtKind::ClassDef { .. }) {
|
|
||||||
DocstringKind::Class(parent)
|
|
||||||
} else {
|
|
||||||
DocstringKind::Function(parent)
|
|
||||||
},
|
|
||||||
expr,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
StmtKind::ClassDef { .. } => return Some(Documentable::Class),
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extract the source code range for a `Docstring`.
|
/// Extract a docstring from a function or class body.
|
||||||
fn range_for(docstring: &Docstring) -> Range {
|
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>(
|
||||||
|
parents: Vec<&'a Stmt>,
|
||||||
|
stmt: &'a Stmt,
|
||||||
|
body: &'a [Stmt],
|
||||||
|
kind: Documentable,
|
||||||
|
) -> Definition<'a> {
|
||||||
|
let expr = docstring_from(body);
|
||||||
|
match kind {
|
||||||
|
Documentable::Function => Definition {
|
||||||
|
kind: match nest(&parents) {
|
||||||
|
None => DefinitionKind::Function(stmt),
|
||||||
|
Some(Documentable::Function) => DefinitionKind::NestedFunction(stmt),
|
||||||
|
Some(Documentable::Class) => DefinitionKind::Method(stmt),
|
||||||
|
},
|
||||||
|
docstring: expr,
|
||||||
|
},
|
||||||
|
Documentable::Class => Definition {
|
||||||
|
kind: match nest(&parents) {
|
||||||
|
None => DefinitionKind::Class(stmt),
|
||||||
|
Some(_) => DefinitionKind::NestedClass(stmt),
|
||||||
|
},
|
||||||
|
docstring: expr,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract the source code range for a docstring.
|
||||||
|
fn range_for(docstring: &Expr) -> Range {
|
||||||
// RustPython currently omits the first quotation mark in a string, so offset the location.
|
// RustPython currently omits the first quotation mark in a string, so offset the location.
|
||||||
Range {
|
Range {
|
||||||
location: Location::new(
|
location: Location::new(docstring.location.row(), docstring.location.column() - 1),
|
||||||
docstring.expr.location.row(),
|
end_location: docstring.end_location,
|
||||||
docstring.expr.location.column() - 1,
|
|
||||||
),
|
|
||||||
end_location: docstring.expr.end_location,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// D200
|
/// D200
|
||||||
pub fn one_liner(checker: &mut Checker, docstring: &Docstring) {
|
pub fn one_liner(checker: &mut Checker, definition: &Definition) {
|
||||||
if let ExprKind::Constant {
|
if let Some(docstring) = &definition.docstring {
|
||||||
value: Constant::Str(string),
|
if let ExprKind::Constant {
|
||||||
..
|
value: Constant::Str(string),
|
||||||
} = &docstring.expr.node
|
..
|
||||||
{
|
} = &docstring.node
|
||||||
let mut line_count = 0;
|
{
|
||||||
let mut non_empty_line_count = 0;
|
let mut line_count = 0;
|
||||||
for line in string.lines() {
|
let mut non_empty_line_count = 0;
|
||||||
line_count += 1;
|
for line in string.lines() {
|
||||||
if !line.trim().is_empty() {
|
line_count += 1;
|
||||||
non_empty_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 {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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, range_for(docstring)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -103,53 +128,59 @@ static INNER_FUNCTION_OR_CLASS_REGEX: Lazy<Regex> =
|
||||||
Lazy::new(|| Regex::new(r"^\s+(?:(?:class|def|async def)\s|@)").unwrap());
|
Lazy::new(|| Regex::new(r"^\s+(?:(?:class|def|async def)\s|@)").unwrap());
|
||||||
|
|
||||||
/// D201, D202
|
/// D201, D202
|
||||||
pub fn blank_before_after_function(checker: &mut Checker, docstring: &Docstring) {
|
pub fn blank_before_after_function(checker: &mut Checker, definition: &Definition) {
|
||||||
if let DocstringKind::Function(parent) = &docstring.kind {
|
if let Some(docstring) = definition.docstring {
|
||||||
if let ExprKind::Constant {
|
if let DefinitionKind::Function(parent)
|
||||||
value: Constant::Str(_),
|
| DefinitionKind::NestedFunction(parent)
|
||||||
..
|
| DefinitionKind::Method(parent) = &definition.kind
|
||||||
} = &docstring.expr.node
|
|
||||||
{
|
{
|
||||||
let (before, _, after) = checker
|
if let ExprKind::Constant {
|
||||||
.locator
|
value: Constant::Str(_),
|
||||||
.partition_source_code_at(&Range::from_located(parent), &range_for(docstring));
|
..
|
||||||
|
} = &docstring.node
|
||||||
|
{
|
||||||
|
let (before, _, after) = checker
|
||||||
|
.locator
|
||||||
|
.partition_source_code_at(&Range::from_located(parent), &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
|
||||||
.lines()
|
.lines()
|
||||||
.rev()
|
.rev()
|
||||||
.skip(1)
|
.skip(1)
|
||||||
.take_while(|line| line.trim().is_empty())
|
.take_while(|line| line.trim().is_empty())
|
||||||
.count();
|
.count();
|
||||||
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),
|
range_for(docstring),
|
||||||
));
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if checker.settings.enabled.contains(&CheckCode::D202) {
|
if checker.settings.enabled.contains(&CheckCode::D202) {
|
||||||
let blank_lines_after = after
|
let blank_lines_after = after
|
||||||
.lines()
|
.lines()
|
||||||
.skip(1)
|
.skip(1)
|
||||||
.take_while(|line| line.trim().is_empty())
|
.take_while(|line| line.trim().is_empty())
|
||||||
.count();
|
.count();
|
||||||
let all_blank_after = after
|
let all_blank_after = after
|
||||||
.lines()
|
.lines()
|
||||||
.skip(1)
|
.skip(1)
|
||||||
.all(|line| line.trim().is_empty() || COMMENT_REGEX.is_match(line));
|
.all(|line| line.trim().is_empty() || COMMENT_REGEX.is_match(line));
|
||||||
// Report a D202 violation if the docstring is followed by a blank line
|
// 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
|
// and the blank line is not itself followed by an inner function or
|
||||||
// class.
|
// class.
|
||||||
if !all_blank_after
|
if !all_blank_after
|
||||||
&& blank_lines_after != 0
|
&& blank_lines_after != 0
|
||||||
&& !(blank_lines_after == 1 && INNER_FUNCTION_OR_CLASS_REGEX.is_match(after))
|
&& !(blank_lines_after == 1
|
||||||
{
|
&& INNER_FUNCTION_OR_CLASS_REGEX.is_match(after))
|
||||||
checker.add_check(Check::new(
|
{
|
||||||
CheckKind::NoBlankLineAfterFunction(blank_lines_after),
|
checker.add_check(Check::new(
|
||||||
range_for(docstring),
|
CheckKind::NoBlankLineAfterFunction(blank_lines_after),
|
||||||
));
|
range_for(docstring),
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -157,55 +188,63 @@ pub fn blank_before_after_function(checker: &mut Checker, docstring: &Docstring)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// D203, D204, D211
|
/// D203, D204, D211
|
||||||
pub fn blank_before_after_class(checker: &mut Checker, docstring: &Docstring) {
|
pub fn blank_before_after_class(checker: &mut Checker, definition: &Definition) {
|
||||||
if let DocstringKind::Class(parent) = &docstring.kind {
|
if let Some(docstring) = &definition.docstring {
|
||||||
if let ExprKind::Constant {
|
if let DefinitionKind::Class(parent) | DefinitionKind::NestedClass(parent) =
|
||||||
value: Constant::Str(_),
|
&definition.kind
|
||||||
..
|
|
||||||
} = &docstring.expr.node
|
|
||||||
{
|
{
|
||||||
let (before, _, after) = checker
|
if let ExprKind::Constant {
|
||||||
.locator
|
value: Constant::Str(_),
|
||||||
.partition_source_code_at(&Range::from_located(parent), &range_for(docstring));
|
..
|
||||||
|
} = &docstring.node
|
||||||
if checker.settings.enabled.contains(&CheckCode::D203)
|
|
||||||
|| checker.settings.enabled.contains(&CheckCode::D211)
|
|
||||||
{
|
{
|
||||||
let blank_lines_before = before
|
let (before, _, after) = checker
|
||||||
.lines()
|
.locator
|
||||||
.rev()
|
.partition_source_code_at(&Range::from_located(parent), &range_for(docstring));
|
||||||
.skip(1)
|
|
||||||
.take_while(|line| line.trim().is_empty())
|
|
||||||
.count();
|
|
||||||
if blank_lines_before != 0 && checker.settings.enabled.contains(&CheckCode::D211) {
|
|
||||||
checker.add_check(Check::new(
|
|
||||||
CheckKind::NoBlankLineBeforeClass(blank_lines_before),
|
|
||||||
range_for(docstring),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if blank_lines_before != 1 && checker.settings.enabled.contains(&CheckCode::D203) {
|
|
||||||
checker.add_check(Check::new(
|
|
||||||
CheckKind::OneBlankLineBeforeClass(blank_lines_before),
|
|
||||||
range_for(docstring),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if checker.settings.enabled.contains(&CheckCode::D204) {
|
if checker.settings.enabled.contains(&CheckCode::D203)
|
||||||
let blank_lines_after = after
|
|| checker.settings.enabled.contains(&CheckCode::D211)
|
||||||
.lines()
|
{
|
||||||
.skip(1)
|
let blank_lines_before = before
|
||||||
.take_while(|line| line.trim().is_empty())
|
.lines()
|
||||||
.count();
|
.rev()
|
||||||
let all_blank_after = after
|
.skip(1)
|
||||||
.lines()
|
.take_while(|line| line.trim().is_empty())
|
||||||
.skip(1)
|
.count();
|
||||||
.all(|line| line.trim().is_empty() || COMMENT_REGEX.is_match(line));
|
if blank_lines_before != 0
|
||||||
if !all_blank_after && blank_lines_after != 1 {
|
&& checker.settings.enabled.contains(&CheckCode::D211)
|
||||||
checker.add_check(Check::new(
|
{
|
||||||
CheckKind::OneBlankLineAfterClass(blank_lines_after),
|
checker.add_check(Check::new(
|
||||||
range_for(docstring),
|
CheckKind::NoBlankLineBeforeClass(blank_lines_before),
|
||||||
));
|
range_for(docstring),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if blank_lines_before != 1
|
||||||
|
&& checker.settings.enabled.contains(&CheckCode::D203)
|
||||||
|
{
|
||||||
|
checker.add_check(Check::new(
|
||||||
|
CheckKind::OneBlankLineBeforeClass(blank_lines_before),
|
||||||
|
range_for(docstring),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if checker.settings.enabled.contains(&CheckCode::D204) {
|
||||||
|
let blank_lines_after = after
|
||||||
|
.lines()
|
||||||
|
.skip(1)
|
||||||
|
.take_while(|line| line.trim().is_empty())
|
||||||
|
.count();
|
||||||
|
let all_blank_after = after
|
||||||
|
.lines()
|
||||||
|
.skip(1)
|
||||||
|
.all(|line| line.trim().is_empty() || COMMENT_REGEX.is_match(line));
|
||||||
|
if !all_blank_after && blank_lines_after != 1 {
|
||||||
|
checker.add_check(Check::new(
|
||||||
|
CheckKind::OneBlankLineAfterClass(blank_lines_after),
|
||||||
|
range_for(docstring),
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -213,77 +252,26 @@ pub fn blank_before_after_class(checker: &mut Checker, docstring: &Docstring) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// D205
|
/// D205
|
||||||
pub fn blank_after_summary(checker: &mut Checker, docstring: &Docstring) {
|
pub fn blank_after_summary(checker: &mut Checker, definition: &Definition) {
|
||||||
if let ExprKind::Constant {
|
if let Some(docstring) = definition.docstring {
|
||||||
value: Constant::Str(string),
|
if let ExprKind::Constant {
|
||||||
..
|
value: Constant::Str(string),
|
||||||
} = &docstring.expr.node
|
..
|
||||||
{
|
} = &docstring.node
|
||||||
let mut lines_count = 1;
|
{
|
||||||
let mut blanks_count = 0;
|
let mut lines_count = 1;
|
||||||
for line in string.trim().lines().skip(1) {
|
let mut blanks_count = 0;
|
||||||
lines_count += 1;
|
for line in string.trim().lines().skip(1) {
|
||||||
if line.trim().is_empty() {
|
lines_count += 1;
|
||||||
blanks_count += 1;
|
if line.trim().is_empty() {
|
||||||
} else {
|
blanks_count += 1;
|
||||||
break;
|
} else {
|
||||||
}
|
break;
|
||||||
}
|
|
||||||
if lines_count > 1 && blanks_count != 1 {
|
|
||||||
checker.add_check(Check::new(
|
|
||||||
CheckKind::NoBlankLineAfterSummary,
|
|
||||||
range_for(docstring),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// D209
|
|
||||||
pub fn newline_after_last_paragraph(checker: &mut Checker, docstring: &Docstring) {
|
|
||||||
if let ExprKind::Constant {
|
|
||||||
value: Constant::Str(string),
|
|
||||||
..
|
|
||||||
} = &docstring.expr.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_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),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
if lines_count > 1 && blanks_count != 1 {
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// D210
|
|
||||||
pub fn no_surrounding_whitespace(checker: &mut Checker, docstring: &Docstring) {
|
|
||||||
if let ExprKind::Constant {
|
|
||||||
value: Constant::Str(string),
|
|
||||||
..
|
|
||||||
} = &docstring.expr.node
|
|
||||||
{
|
|
||||||
let mut lines = string.lines();
|
|
||||||
if let Some(line) = lines.next() {
|
|
||||||
if line.trim().is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if line.starts_with(' ') || (matches!(lines.next(), None) && line.ends_with(' ')) {
|
|
||||||
checker.add_check(Check::new(
|
checker.add_check(Check::new(
|
||||||
CheckKind::NoSurroundingWhitespace,
|
CheckKind::NoBlankLineAfterSummary,
|
||||||
range_for(docstring),
|
range_for(docstring),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
@ -291,29 +279,55 @@ pub fn no_surrounding_whitespace(checker: &mut Checker, docstring: &Docstring) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// D212, D213
|
/// D209
|
||||||
pub fn multi_line_summary_start(checker: &mut Checker, docstring: &Docstring) {
|
pub fn newline_after_last_paragraph(checker: &mut Checker, definition: &Definition) {
|
||||||
if let ExprKind::Constant {
|
if let Some(docstring) = definition.docstring {
|
||||||
value: Constant::Str(string),
|
if let ExprKind::Constant {
|
||||||
..
|
value: Constant::Str(string),
|
||||||
} = &docstring.expr.node
|
..
|
||||||
{
|
} = &docstring.node
|
||||||
if string.lines().nth(1).is_some() {
|
{
|
||||||
let content = checker
|
let mut line_count = 0;
|
||||||
.locator
|
for line in string.lines() {
|
||||||
.slice_source_code_range(&range_for(docstring));
|
if !line.trim().is_empty() {
|
||||||
if let Some(first_line) = content.lines().next() {
|
line_count += 1;
|
||||||
let first_line = first_line.trim();
|
}
|
||||||
if first_line == "\"\"\"" || first_line == "'''" {
|
if line_count > 1 {
|
||||||
if checker.settings.enabled.contains(&CheckCode::D212) {
|
let content = checker
|
||||||
checker.add_check(Check::new(
|
.locator
|
||||||
CheckKind::MultiLineSummaryFirstLine,
|
.slice_source_code_range(&range_for(docstring));
|
||||||
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),
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if checker.settings.enabled.contains(&CheckCode::D213) {
|
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() {
|
||||||
|
if line.trim().is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if line.starts_with(' ') || (matches!(lines.next(), None) && line.ends_with(' ')) {
|
||||||
checker.add_check(Check::new(
|
checker.add_check(Check::new(
|
||||||
CheckKind::MultiLineSummarySecondLine,
|
CheckKind::NoSurroundingWhitespace,
|
||||||
range_for(docstring),
|
range_for(docstring),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
@ -322,59 +336,104 @@ pub fn multi_line_summary_start(checker: &mut Checker, docstring: &Docstring) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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() {
|
||||||
|
let content = checker
|
||||||
|
.locator
|
||||||
|
.slice_source_code_range(&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),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else if checker.settings.enabled.contains(&CheckCode::D213) {
|
||||||
|
checker.add_check(Check::new(
|
||||||
|
CheckKind::MultiLineSummarySecondLine,
|
||||||
|
range_for(docstring),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// D300
|
/// D300
|
||||||
pub fn triple_quotes(checker: &mut Checker, docstring: &Docstring) {
|
pub fn triple_quotes(checker: &mut Checker, definition: &Definition) {
|
||||||
if let ExprKind::Constant {
|
if let Some(docstring) = definition.docstring {
|
||||||
value: Constant::Str(string),
|
if let ExprKind::Constant {
|
||||||
..
|
value: Constant::Str(string),
|
||||||
} = &docstring.expr.node
|
..
|
||||||
{
|
} = &docstring.node
|
||||||
let content = checker
|
{
|
||||||
.locator
|
let content = checker
|
||||||
.slice_source_code_range(&range_for(docstring));
|
.locator
|
||||||
if string.contains("\"\"\"") {
|
.slice_source_code_range(&range_for(docstring));
|
||||||
if !content.starts_with("'''") {
|
if string.contains("\"\"\"") {
|
||||||
|
if !content.starts_with("'''") {
|
||||||
|
checker.add_check(Check::new(
|
||||||
|
CheckKind::UsesTripleQuotes,
|
||||||
|
range_for(docstring),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else if !content.starts_with("\"\"\"") {
|
||||||
checker.add_check(Check::new(
|
checker.add_check(Check::new(
|
||||||
CheckKind::UsesTripleQuotes,
|
CheckKind::UsesTripleQuotes,
|
||||||
range_for(docstring),
|
range_for(docstring),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
} else if !content.starts_with("\"\"\"") {
|
|
||||||
checker.add_check(Check::new(
|
|
||||||
CheckKind::UsesTripleQuotes,
|
|
||||||
range_for(docstring),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// D400
|
/// D400
|
||||||
pub fn ends_with_period(checker: &mut Checker, docstring: &Docstring) {
|
pub fn ends_with_period(checker: &mut Checker, definition: &Definition) {
|
||||||
if let ExprKind::Constant {
|
if let Some(docstring) = definition.docstring {
|
||||||
value: Constant::Str(string),
|
if let ExprKind::Constant {
|
||||||
..
|
value: Constant::Str(string),
|
||||||
} = &docstring.expr.node
|
..
|
||||||
{
|
} = &docstring.node
|
||||||
if let Some(string) = string.lines().next() {
|
{
|
||||||
if !string.ends_with('.') {
|
if let Some(string) = string.lines().next() {
|
||||||
checker.add_check(Check::new(CheckKind::EndsInPeriod, range_for(docstring)));
|
if !string.ends_with('.') {
|
||||||
|
checker.add_check(Check::new(CheckKind::EndsInPeriod, range_for(docstring)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// D402
|
/// D402
|
||||||
pub fn no_signature(checker: &mut Checker, docstring: &Docstring) {
|
pub fn no_signature(checker: &mut Checker, definition: &Definition) {
|
||||||
if let DocstringKind::Function(parent) = docstring.kind {
|
if let Some(docstring) = definition.docstring {
|
||||||
if let StmtKind::FunctionDef { name, .. } = &parent.node {
|
if let DefinitionKind::Function(parent)
|
||||||
if let ExprKind::Constant {
|
| DefinitionKind::NestedFunction(parent)
|
||||||
value: Constant::Str(string),
|
| DefinitionKind::Method(parent) = definition.kind
|
||||||
..
|
{
|
||||||
} = &docstring.expr.node
|
if let StmtKind::FunctionDef { name, .. } = &parent.node {
|
||||||
{
|
if let ExprKind::Constant {
|
||||||
if let Some(first_line) = string.lines().next() {
|
value: Constant::Str(string),
|
||||||
if first_line.contains(&format!("{name}(")) {
|
..
|
||||||
checker.add_check(Check::new(CheckKind::NoSignature, range_for(docstring)));
|
} = &docstring.node
|
||||||
|
{
|
||||||
|
if let Some(first_line) = string.lines().next() {
|
||||||
|
if first_line.contains(&format!("{name}(")) {
|
||||||
|
checker.add_check(Check::new(
|
||||||
|
CheckKind::NoSignature,
|
||||||
|
range_for(docstring),
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -383,29 +442,51 @@ pub fn no_signature(checker: &mut Checker, docstring: &Docstring) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// D403
|
/// D403
|
||||||
pub fn capitalized(checker: &mut Checker, docstring: &Docstring) {
|
pub fn capitalized(checker: &mut Checker, definition: &Definition) {
|
||||||
if !matches!(docstring.kind, DocstringKind::Function(_)) {
|
if !matches!(definition.kind, DefinitionKind::Function(_)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let ExprKind::Constant {
|
if let Some(docstring) = definition.docstring {
|
||||||
value: Constant::Str(string),
|
if let ExprKind::Constant {
|
||||||
..
|
value: Constant::Str(string),
|
||||||
} = &docstring.expr.node
|
..
|
||||||
{
|
} = &docstring.node
|
||||||
if let Some(first_word) = string.split(' ').next() {
|
{
|
||||||
if first_word == first_word.to_uppercase() {
|
if let Some(first_word) = string.split(' ').next() {
|
||||||
return;
|
if first_word == first_word.to_uppercase() {
|
||||||
}
|
|
||||||
for char in first_word.chars() {
|
|
||||||
if !char.is_ascii_alphabetic() && char != '\'' {
|
|
||||||
return;
|
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_for(docstring),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if let Some(first_char) = first_word.chars().next() {
|
}
|
||||||
if !first_char.is_uppercase() {
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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(
|
checker.add_check(Check::new(
|
||||||
CheckKind::FirstLineCapitalized,
|
CheckKind::EndsInPunctuation,
|
||||||
range_for(docstring),
|
range_for(docstring),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
@ -414,36 +495,20 @@ pub fn capitalized(checker: &mut Checker, docstring: &Docstring) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// D415
|
|
||||||
pub fn ends_with_punctuation(checker: &mut Checker, docstring: &Docstring) {
|
|
||||||
if let ExprKind::Constant {
|
|
||||||
value: Constant::Str(string),
|
|
||||||
..
|
|
||||||
} = &docstring.expr.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_for(docstring),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// D419
|
/// D419
|
||||||
pub fn not_empty(checker: &mut Checker, docstring: &Docstring) -> bool {
|
pub fn not_empty(checker: &mut Checker, definition: &Definition) -> bool {
|
||||||
if let ExprKind::Constant {
|
if let Some(docstring) = definition.docstring {
|
||||||
value: Constant::Str(string),
|
if let ExprKind::Constant {
|
||||||
..
|
value: Constant::Str(string),
|
||||||
} = &docstring.expr.node
|
..
|
||||||
{
|
} = &docstring.node
|
||||||
if string.trim().is_empty() {
|
{
|
||||||
if checker.settings.enabled.contains(&CheckCode::D419) {
|
if string.trim().is_empty() {
|
||||||
checker.add_check(Check::new(CheckKind::NonEmpty, range_for(docstring)));
|
if checker.settings.enabled.contains(&CheckCode::D419) {
|
||||||
|
checker.add_check(Check::new(CheckKind::NonEmpty, range_for(docstring)));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue