Compare commits

..

4 Commits

Author SHA1 Message Date
Charlie Marsh
6a8e31b2ff Bump version to 0.0.74 2022-10-14 12:36:44 -04:00
Charlie Marsh
6407fd5a33 Re-arrange some docstring modules (#428) 2022-10-14 12:34:35 -04:00
Harutaka Kawamura
b64040cbb2 Implement C417 (#426) 2022-10-14 12:34:00 -04:00
Charlie Marsh
952a0eb4e3 Implement checks for Google-style docstrings (#427) 2022-10-14 11:53:29 -04:00
22 changed files with 1050 additions and 334 deletions

39
Cargo.lock generated
View File

@@ -981,6 +981,9 @@ name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
dependencies = [
"ahash",
]
[[package]]
name = "heck"
@@ -1966,7 +1969,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.73"
version = "0.0.74"
dependencies = [
"anyhow",
"assert_cmd",
@@ -1999,6 +2002,7 @@ dependencies = [
"strum",
"strum_macros",
"test-case",
"textwrap",
"titlecase",
"toml",
"update-informer",
@@ -2261,6 +2265,12 @@ version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
[[package]]
name = "smawk"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043"
[[package]]
name = "socket2"
version = "0.4.7"
@@ -2435,6 +2445,17 @@ dependencies = [
"syn",
]
[[package]]
name = "textwrap"
version = "0.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "949517c0cf1bf4ee812e2e07e08ab448e3ae0d23472aee8a06c985f0c8815b16"
dependencies = [
"smawk",
"unicode-linebreak",
"unicode-width",
]
[[package]]
name = "thiserror"
version = "1.0.37"
@@ -2602,6 +2623,16 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd"
[[package]]
name = "unicode-linebreak"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5faade31a542b8b35855fff6e8def199853b2da8da256da52f52f1316ee3137"
dependencies = [
"hashbrown",
"regex",
]
[[package]]
name = "unicode-normalization"
version = "0.1.22"
@@ -2611,6 +2642,12 @@ dependencies = [
"tinyvec",
]
[[package]]
name = "unicode-width"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
[[package]]
name = "unicode-xid"
version = "0.2.4"

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.0.73"
version = "0.0.74"
edition = "2021"
[lib]
@@ -35,6 +35,7 @@ serde = { version = "1.0.143", features = ["derive"] }
serde_json = { version = "1.0.83" }
strum = { version = "0.24.1", features = ["strum_macros"] }
strum_macros = { version = "0.24.3" }
textwrap = { version = "0.15.1" }
titlecase = { version = "2.2.1" }
toml = { version = "0.5.9" }
update-informer = { version = "0.5.0", default_features = false, features = ["pypi"], optional = true }

View File

@@ -57,7 +57,7 @@ ruff also works with [pre-commit](https://pre-commit.com):
```yaml
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.73
rev: v0.0.74
hooks:
- id: lint
```
@@ -219,9 +219,9 @@ ruff also implements some of the most popular Flake8 plugins natively, including
- [`flake8-builtins`](https://pypi.org/project/flake8-builtins/)
- [`flake8-super`](https://pypi.org/project/flake8-super/)
- [`flake8-print`](https://pypi.org/project/flake8-print/)
- [`flake8-comprehensions`](https://pypi.org/project/flake8-comprehensions/) (15/16)
- [`flake8-comprehensions`](https://pypi.org/project/flake8-comprehensions/)
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (3/32)
- [`flake8-docstrings`](https://pypi.org/project/flake8-docstrings/) (37/48)
- [`flake8-docstrings`](https://pypi.org/project/flake8-docstrings/) (41/48)
- [`pyupgrade`](https://pypi.org/project/pyupgrade/) (8/34)
Beyond rule-set parity, ruff suffers from the following limitations vis-à-vis Flake8:
@@ -302,6 +302,7 @@ The 🛠 emoji indicates that a rule is automatically fixable by the `--fix` com
| C414 | UnnecessaryDoubleCastOrProcess | Unnecessary <list/reversed/set/sorted/tuple> call within <list/set/sorted/tuple>(). | | |
| C415 | UnnecessarySubscriptReversal | Unnecessary subscript reversal of iterable within <reversed/set/sorted>() | | |
| C416 | UnnecessaryComprehension | Unnecessary <list/set> comprehension - rewrite using <list/set>() | | |
| C417 | UnnecessaryMap | Unnecessary map usage - rewrite using a <list/set/dict> comprehension | | |
| T201 | PrintFound | `print` found | | 🛠 |
| T203 | PPrintFound | `pprint` found | | 🛠 |
| U001 | UselessMetaclassType | `__metaclass__ = type` is implied | | 🛠 |
@@ -349,6 +350,7 @@ The 🛠 emoji indicates that a rule is automatically fixable by the `--fix` com
| D413 | BlankLineAfterLastSection | Missing blank line after last section ("Returns") | | |
| D414 | NonEmptySection | Section has no content ("Returns") | | |
| D415 | EndsInPunctuation | First line should end with a period, question mark, or exclamation point | | |
| D416 | SectionNameEndsInColon | Section name should end with a colon ("Returns") | | |
| D417 | DocumentAllArguments | Missing argument descriptions in the docstring: `x`, `y` | | |
| D418 | SkipDocstring | Function decorated with @overload shouldn't contain a docstring | | |
| D419 | NonEmpty | Docstring is empty | | |

6
resources/test/fixtures/C417.py vendored Normal file
View File

@@ -0,0 +1,6 @@
nums = [1, 2, 3]
map(lambda x: x + 1, nums)
map(lambda x: str(x), nums)
list(map(lambda x: x * 2, nums))
set(map(lambda x: x % 2 == 0, nums))
dict(map(lambda v: (v, v**2), nums))

View File

@@ -0,0 +1,108 @@
"""A one line summary of the module or program, terminated by a period.
Leave one blank line. The rest of this docstring should contain an
overall description of the module or program. Optionally, it may also
contain a brief description of exported classes and functions and/or usage
examples.
Typical usage example:
foo = ClassFoo()
bar = foo.FunctionBar()
"""
# above: "2.8.2 Modules" section example
# https://google.github.io/styleguide/pyguide.html#382-modules
# Examples from the official "Google Python Style Guide" documentation:
# * As HTML: https://google.github.io/styleguide/pyguide.html
# * Source Markdown:
# https://github.com/google/styleguide/blob/gh-pages/pyguide.md
import os
from .expected import Expectation
expectation = Expectation()
expect = expectation.expect
# module docstring expected violations:
expectation.expected.add((
os.path.normcase(__file__),
"D213: Multi-line docstring summary should start at the second line"))
# "3.8.3 Functions and Methods" section example
# https://google.github.io/styleguide/pyguide.html#383-functions-and-methods
@expect("D213: Multi-line docstring summary should start at the second line",
arg_count=3)
@expect("D401: First line should be in imperative mood "
"(perhaps 'Fetch', not 'Fetches')", arg_count=3)
@expect("D406: Section name should end with a newline "
"('Raises', not 'Raises:')", arg_count=3)
@expect("D406: Section name should end with a newline "
"('Returns', not 'Returns:')", arg_count=3)
@expect("D407: Missing dashed underline after section ('Raises')", arg_count=3)
@expect("D407: Missing dashed underline after section ('Returns')",
arg_count=3)
@expect("D413: Missing blank line after last section ('Raises')", arg_count=3)
def fetch_bigtable_rows(big_table, keys, other_silly_variable=None):
"""Fetches rows from a Bigtable.
Retrieves rows pertaining to the given keys from the Table instance
represented by big_table. Silly things may happen if
other_silly_variable is not None.
Args:
big_table: An open Bigtable Table instance.
keys: A sequence of strings representing the key of each table row
to fetch.
other_silly_variable: Another optional variable, that has a much
longer name than the other args, and which does nothing.
Returns:
A dict mapping keys to the corresponding table row data
fetched. Each row is represented as a tuple of strings. For
example:
{'Serak': ('Rigel VII', 'Preparer'),
'Zim': ('Irk', 'Invader'),
'Lrrr': ('Omicron Persei 8', 'Emperor')}
If a key from the keys argument is missing from the dictionary,
then that row was not found in the table.
Raises:
IOError: An error occurred accessing the bigtable.Table object.
"""
# "3.8.4 Classes" section example
# https://google.github.io/styleguide/pyguide.html#384-classes
@expect("D203: 1 blank line required before class docstring (found 0)")
@expect("D213: Multi-line docstring summary should start at the second line")
@expect("D406: Section name should end with a newline "
"('Attributes', not 'Attributes:')")
@expect("D407: Missing dashed underline after section ('Attributes')")
@expect("D413: Missing blank line after last section ('Attributes')")
class SampleClass:
"""Summary of class here.
Longer class information....
Longer class information....
Attributes:
likes_spam: A boolean indicating if we like SPAM or not.
eggs: An integer count of the eggs we have laid.
"""
@expect("D401: First line should be in imperative mood "
"(perhaps 'Init', not 'Inits')", arg_count=2)
def __init__(self, likes_spam=False):
"""Inits SampleClass with blah."""
if self: # added to avoid NameError when run via @expect decorator
self.likes_spam = likes_spam
self.eggs = 0
@expect("D401: First line should be in imperative mood "
"(perhaps 'Perform', not 'Performs')", arg_count=1)
def public_method(self):
"""Performs operation blah."""

View File

@@ -1175,6 +1175,64 @@ pub fn unnecessary_comprehension(
None
}
pub fn unnecessary_map(expr: &Expr, func: &Expr, args: &[Expr]) -> Option<Check> {
if let ExprKind::Name { id, .. } = &func.node {
if id == "map" {
if args.len() == 2 {
if let ExprKind::Lambda { .. } = &args[0].node {
return Some(Check::new(
CheckKind::UnnecessaryMap("generator".to_string()),
Range::from_located(expr),
));
}
}
} else if id == "list" || id == "set" {
if let Some(arg) = args.first() {
if let ExprKind::Call { func, args, .. } = &arg.node {
if let ExprKind::Name { id: f, .. } = &func.node {
if f == "map" {
if let Some(arg) = args.first() {
if let ExprKind::Lambda { .. } = &arg.node {
return Some(Check::new(
CheckKind::UnnecessaryMap(id.to_string()),
Range::from_located(expr),
));
}
}
}
}
}
}
} else if id == "dict" {
if args.len() == 1 {
if let ExprKind::Call { func, args, .. } = &args[0].node {
if let ExprKind::Name { id: f, .. } = &func.node {
if f == "map" {
if let Some(arg) = args.first() {
if let ExprKind::Lambda { body, .. } = &arg.node {
match &body.node {
ExprKind::Tuple { elts, .. }
| ExprKind::List { elts, .. }
if elts.len() == 2 =>
{
return Some(Check::new(
CheckKind::UnnecessaryMap(id.to_string()),
Range::from_located(expr),
))
}
_ => {}
}
}
}
}
}
}
}
}
}
None
}
// flake8-super
/// Check that `super()` has no args
pub fn super_args(

View File

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

View File

@@ -142,6 +142,7 @@ pub enum CheckCode {
C414,
C415,
C416,
C417,
// flake8-print
T201,
T203,
@@ -192,6 +193,7 @@ pub enum CheckCode {
D413,
D414,
D415,
D416,
D417,
D418,
D419,
@@ -283,6 +285,7 @@ pub enum CheckKind {
UnnecessaryDoubleCastOrProcess(String, String),
UnnecessarySubscriptReversal(String),
UnnecessaryComprehension(String),
UnnecessaryMap(String),
// flake8-print
PrintFound,
PPrintFound,
@@ -330,6 +333,7 @@ pub enum CheckKind {
PublicModule,
PublicNestedClass,
PublicPackage,
SectionNameEndsInColon(String),
SectionNotOverIndented(String),
SectionUnderlineAfterName(String),
SectionUnderlineMatchesSectionLength(String),
@@ -438,6 +442,7 @@ impl CheckCode {
CheckKind::UnnecessarySubscriptReversal("<reversed/set/sorted>".to_string())
}
CheckCode::C416 => CheckKind::UnnecessaryComprehension("<list/set>".to_string()),
CheckCode::C417 => CheckKind::UnnecessaryMap("<list/set/dict>".to_string()),
// flake8-print
CheckCode::T201 => CheckKind::PrintFound,
CheckCode::T203 => CheckKind::PPrintFound,
@@ -473,6 +478,8 @@ impl CheckCode {
CheckCode::D211 => CheckKind::NoBlankLineBeforeClass(1),
CheckCode::D212 => CheckKind::MultiLineSummaryFirstLine,
CheckCode::D213 => CheckKind::MultiLineSummarySecondLine,
CheckCode::D214 => CheckKind::SectionNotOverIndented("Returns".to_string()),
CheckCode::D215 => CheckKind::SectionUnderlineNotOverIndented("Returns".to_string()),
CheckCode::D300 => CheckKind::UsesTripleQuotes,
CheckCode::D400 => CheckKind::EndsInPeriod,
CheckCode::D402 => CheckKind::NoSignature,
@@ -493,13 +500,12 @@ impl CheckCode {
CheckCode::D413 => CheckKind::BlankLineAfterLastSection("Returns".to_string()),
CheckCode::D414 => CheckKind::NonEmptySection("Returns".to_string()),
CheckCode::D415 => CheckKind::EndsInPunctuation,
CheckCode::D418 => CheckKind::SkipDocstring,
CheckCode::D419 => CheckKind::NonEmpty,
CheckCode::D214 => CheckKind::SectionNotOverIndented("Returns".to_string()),
CheckCode::D215 => CheckKind::SectionUnderlineNotOverIndented("Returns".to_string()),
CheckCode::D416 => CheckKind::SectionNameEndsInColon("Returns".to_string()),
CheckCode::D417 => {
CheckKind::DocumentAllArguments(vec!["x".to_string(), "y".to_string()])
}
CheckCode::D418 => CheckKind::SkipDocstring,
CheckCode::D419 => CheckKind::NonEmpty,
// Meta
CheckCode::M001 => CheckKind::UnusedNOQA(None),
}
@@ -579,6 +585,7 @@ impl CheckKind {
CheckKind::UnnecessaryDoubleCastOrProcess(..) => &CheckCode::C414,
CheckKind::UnnecessarySubscriptReversal(_) => &CheckCode::C415,
CheckKind::UnnecessaryComprehension(..) => &CheckCode::C416,
CheckKind::UnnecessaryMap(_) => &CheckCode::C417,
// flake8-print
CheckKind::PrintFound => &CheckCode::T201,
CheckKind::PPrintFound => &CheckCode::T203,
@@ -626,6 +633,7 @@ impl CheckKind {
CheckKind::PublicModule => &CheckCode::D100,
CheckKind::PublicNestedClass => &CheckCode::D106,
CheckKind::PublicPackage => &CheckCode::D104,
CheckKind::SectionNameEndsInColon(_) => &CheckCode::D416,
CheckKind::SectionNotOverIndented(_) => &CheckCode::D214,
CheckKind::SectionUnderlineAfterName(_) => &CheckCode::D408,
CheckKind::SectionUnderlineMatchesSectionLength(_) => &CheckCode::D409,
@@ -859,6 +867,13 @@ impl CheckKind {
CheckKind::UnnecessaryComprehension(obj_type) => {
format!(" Unnecessary {obj_type} comprehension - rewrite using {obj_type}()")
}
CheckKind::UnnecessaryMap(obj_type) => {
if obj_type == "generator" {
"Unnecessary map usage - rewrite using a generator expression".to_string()
} else {
format!("Unnecessary map usage - rewrite using a {obj_type} comprehension")
}
}
// flake8-print
CheckKind::PrintFound => "`print` found".to_string(),
CheckKind::PPrintFound => "`pprint` found".to_string(),
@@ -982,6 +997,9 @@ impl CheckKind {
CheckKind::SectionUnderlineNotOverIndented(name) => {
format!("Section underline is over-indented (\"{name}\")")
}
CheckKind::SectionNameEndsInColon(name) => {
format!("Section name should end with a colon (\"{name}\")")
}
CheckKind::DocumentAllArguments(names) => {
if names.len() == 1 {
let name = &names[0];

View File

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

View File

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

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

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

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

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

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

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

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

View File

@@ -1,120 +1,32 @@
use itertools::Itertools;
use std::collections::BTreeSet;
use once_cell::sync::Lazy;
use rustpython_ast::{Arg, Expr, Location, StmtKind};
use itertools::Itertools;
use rustpython_ast::{Arg, StmtKind};
use titlecase::titlecase;
use crate::ast::types::Range;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckCode, CheckKind};
use crate::docstrings::docstring_checks::range_for;
use crate::docstrings::helpers;
use crate::docstrings::helpers::range_for;
use crate::docstrings::styles::SectionStyle;
use crate::docstrings::types::{Definition, DefinitionKind};
use crate::visibility::is_static;
static NUMPY_SECTION_NAMES: Lazy<BTreeSet<&'static str>> = Lazy::new(|| {
BTreeSet::from([
"Short Summary",
"Extended Summary",
"Parameters",
"Returns",
"Yields",
"Other Parameters",
"Raises",
"See Also",
"Notes",
"References",
"Examples",
"Attributes",
"Methods",
])
});
static NUMPY_SECTION_NAMES_LOWERCASE: 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",
])
});
// TODO(charlie): Include Google section names.
// 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",
// ])
// });
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) -> bool {
NUMPY_SECTION_NAMES_LOWERCASE.contains(&leading_words(line).to_lowercase().as_str())
}
#[derive(Debug)]
pub struct SectionContext<'a> {
section_name: String,
previous_line: &'a str,
line: &'a str,
following_lines: &'a [&'a str],
pub(crate) struct SectionContext<'a> {
pub(crate) section_name: String,
pub(crate) previous_line: &'a str,
pub(crate) line: &'a str,
pub(crate) following_lines: &'a [&'a str],
pub(crate) is_last_section: bool,
original_index: usize,
is_last_section: bool,
}
fn suspected_as_section(line: &str, style: &SectionStyle) -> bool {
style
.lowercase_section_names()
.contains(&helpers::leading_words(line).to_lowercase().as_str())
}
/// Check if the suspected context is really a section header.
@@ -145,12 +57,15 @@ fn is_docstring_section(context: &SectionContext) -> bool {
}
/// Extract all `SectionContext` values from a docstring.
pub fn section_contexts<'a>(lines: &'a [&'a str]) -> Vec<SectionContext<'a>> {
pub(crate) fn section_contexts<'a>(
lines: &'a [&'a str],
style: &SectionStyle,
) -> Vec<SectionContext<'a>> {
let suspected_section_indices: Vec<usize> = lines
.iter()
.enumerate()
.filter_map(|(lineno, line)| {
if lineno > 0 && suspected_as_section(line) {
if lineno > 0 && suspected_as_section(line, style) {
Some(lineno)
} else {
None
@@ -161,7 +76,7 @@ pub fn section_contexts<'a>(lines: &'a [&'a str]) -> Vec<SectionContext<'a>> {
let mut contexts = vec![];
for lineno in suspected_section_indices {
let context = SectionContext {
section_name: leading_words(lines[lineno]),
section_name: helpers::leading_words(lines[lineno]),
previous_line: lines[lineno - 1],
line: lines[lineno],
following_lines: &lines[lineno + 1..],
@@ -279,7 +194,9 @@ fn check_blanks_and_section_underline(
}
if checker.settings.enabled.contains(&CheckCode::D215) {
if leading_space(non_empty_line).len() > indentation(checker, docstring).len() {
if helpers::leading_space(non_empty_line).len()
> helpers::indentation(checker, docstring).len()
{
checker.add_check(Check::new(
CheckKind::SectionUnderlineNotOverIndented(context.section_name.to_string()),
range_for(docstring),
@@ -322,14 +239,23 @@ fn check_blanks_and_section_underline(
}
}
fn check_common_section(checker: &mut Checker, definition: &Definition, context: &SectionContext) {
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 !NUMPY_SECTION_NAMES.contains(&context.section_name.as_str())
&& NUMPY_SECTION_NAMES.contains(titlecase(&context.section_name).as_str())
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()),
@@ -339,7 +265,9 @@ fn check_common_section(checker: &mut Checker, definition: &Definition, context:
}
if checker.settings.enabled.contains(&CheckCode::D214) {
if leading_space(context.line).len() > indentation(checker, docstring).len() {
if helpers::leading_space(context.line).len()
> helpers::indentation(checker, docstring).len()
{
checker.add_check(Check::new(
CheckKind::SectionNotOverIndented(context.section_name.to_string()),
range_for(docstring),
@@ -378,12 +306,14 @@ fn check_common_section(checker: &mut Checker, definition: &Definition, context:
))
}
}
check_blanks_and_section_underline(checker, definition, context);
}
fn check_missing_args(
pub(crate) fn check_missing_args(
checker: &mut Checker,
definition: &Definition,
docstrings_args: BTreeSet<&str>,
docstrings_args: &BTreeSet<&str>,
) {
if let DefinitionKind::Function(parent)
| DefinitionKind::NestedFunction(parent)
@@ -445,68 +375,3 @@ fn check_missing_args(
}
}
}
fn check_parameters_section(
checker: &mut Checker,
definition: &Definition,
context: &SectionContext,
) {
// Collect the list of arguments documented in the docstring.
let mut docstring_args: BTreeSet<&str> = Default::default();
let section_level_indent = leading_space(context.line);
for i in 1..context.following_lines.len() {
let current_line = context.following_lines[i - 1];
let current_leading_space = leading_space(current_line);
let next_line = context.following_lines[i];
if current_leading_space == section_level_indent
&& (leading_space(next_line).len() > current_leading_space.len())
&& !next_line.trim().is_empty()
{
let parameters = if let Some(semi_index) = current_line.find(':') {
// If the parameter has a type annotation, exclude it.
&current_line[..semi_index]
} else {
// Otherwise, it's just a list of parameters on the current line.
current_line.trim()
};
// Notably, NumPy lets you put multiple parameters of the same type on the same line.
for parameter in parameters.split(',') {
docstring_args.insert(parameter.trim());
}
}
}
// Validate that all arguments were documented.
check_missing_args(checker, definition, docstring_args);
}
pub fn check_numpy_section(
checker: &mut Checker,
definition: &Definition,
context: &SectionContext,
) {
check_common_section(checker, definition, context);
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);
}
}
}

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

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

View File

@@ -259,6 +259,7 @@ mod tests {
#[test_case(CheckCode::C414, Path::new("C414.py"); "C414")]
#[test_case(CheckCode::C415, Path::new("C415.py"); "C415")]
#[test_case(CheckCode::C416, Path::new("C416.py"); "C416")]
#[test_case(CheckCode::C417, Path::new("C417.py"); "C417")]
#[test_case(CheckCode::D100, Path::new("D.py"); "D100")]
#[test_case(CheckCode::D101, Path::new("D.py"); "D101")]
#[test_case(CheckCode::D102, Path::new("D.py"); "D102")]
@@ -295,8 +296,10 @@ mod tests {
#[test_case(CheckCode::D413, Path::new("sections.py"); "D413")]
#[test_case(CheckCode::D414, Path::new("sections.py"); "D414")]
#[test_case(CheckCode::D415, Path::new("D.py"); "D415")]
#[test_case(CheckCode::D416, Path::new("D.py"); "D416")]
#[test_case(CheckCode::D417, Path::new("sections.py"); "D417_0")]
#[test_case(CheckCode::D417, Path::new("canonical_numpy_examples.py"); "D417_1")]
#[test_case(CheckCode::D417, Path::new("canonical_google_examples.py"); "D417_2")]
#[test_case(CheckCode::D418, Path::new("D.py"); "D418")]
#[test_case(CheckCode::D419, Path::new("D.py"); "D419")]
#[test_case(CheckCode::E402, Path::new("E402.py"); "E402")]

View File

@@ -0,0 +1,77 @@
---
source: src/linter.rs
expression: checks
---
- kind:
UnnecessaryMap: generator
location:
row: 2
column: 1
end_location:
row: 2
column: 27
fix: ~
- kind:
UnnecessaryMap: generator
location:
row: 3
column: 1
end_location:
row: 3
column: 28
fix: ~
- kind:
UnnecessaryMap: list
location:
row: 4
column: 1
end_location:
row: 4
column: 33
fix: ~
- kind:
UnnecessaryMap: generator
location:
row: 4
column: 6
end_location:
row: 4
column: 32
fix: ~
- kind:
UnnecessaryMap: set
location:
row: 5
column: 1
end_location:
row: 5
column: 37
fix: ~
- kind:
UnnecessaryMap: generator
location:
row: 5
column: 5
end_location:
row: 5
column: 36
fix: ~
- kind:
UnnecessaryMap: dict
location:
row: 6
column: 1
end_location:
row: 6
column: 37
fix: ~
- kind:
UnnecessaryMap: generator
location:
row: 6
column: 6
end_location:
row: 6
column: 36
fix: ~

View File

@@ -47,4 +47,94 @@ expression: checks
row: 262
column: 8
fix: ~
- kind:
DashedUnderlineAfterSection: Args
location:
row: 269
column: 5
end_location:
row: 274
column: 8
fix: ~
- kind:
DashedUnderlineAfterSection: Args
location:
row: 284
column: 9
end_location:
row: 292
column: 12
fix: ~
- kind:
DashedUnderlineAfterSection: Args
location:
row: 301
column: 5
end_location:
row: 306
column: 8
fix: ~
- kind:
DashedUnderlineAfterSection: Args
location:
row: 313
column: 9
end_location:
row: 319
column: 12
fix: ~
- kind:
DashedUnderlineAfterSection: Args
location:
row: 325
column: 9
end_location:
row: 330
column: 12
fix: ~
- kind:
DashedUnderlineAfterSection: Args
location:
row: 337
column: 9
end_location:
row: 343
column: 12
fix: ~
- kind:
DashedUnderlineAfterSection: Args
location:
row: 350
column: 9
end_location:
row: 355
column: 12
fix: ~
- kind:
DashedUnderlineAfterSection: Args
location:
row: 362
column: 9
end_location:
row: 367
column: 12
fix: ~
- kind:
DashedUnderlineAfterSection: Args
location:
row: 371
column: 9
end_location:
row: 382
column: 12
fix: ~
- kind:
DashedUnderlineAfterSection: Args
location:
row: 490
column: 9
end_location:
row: 497
column: 12
fix: ~

View File

@@ -0,0 +1,6 @@
---
source: src/linter.rs
expression: checks
---
[]

View File

@@ -0,0 +1,6 @@
---
source: src/linter.rs
expression: checks
---
[]

View File

@@ -2,6 +2,73 @@
source: src/linter.rs
expression: checks
---
- kind:
DocumentAllArguments:
- y
location:
row: 283
column: 5
end_location:
row: 296
column: 1
fix: ~
- kind:
DocumentAllArguments:
- y
location:
row: 300
column: 1
end_location:
row: 309
column: 1
fix: ~
- kind:
DocumentAllArguments:
- test
- y
- z
location:
row: 324
column: 5
end_location:
row: 332
column: 5
fix: ~
- kind:
DocumentAllArguments:
- test
- y
- z
location:
row: 336
column: 5
end_location:
row: 345
column: 5
fix: ~
- kind:
DocumentAllArguments:
- a
- y
- z
location:
row: 349
column: 5
end_location:
row: 357
column: 5
fix: ~
- kind:
DocumentAllArguments:
- a
- b
location:
row: 361
column: 5
end_location:
row: 369
column: 5
fix: ~
- kind:
DocumentAllArguments:
- y
@@ -47,4 +114,14 @@ expression: checks
row: 471
column: 5
fix: ~
- kind:
DocumentAllArguments:
- y
location:
row: 489
column: 5
end_location:
row: 498
column: 1
fix: ~