Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
46e1b16472 | ||
|
|
720bfe0161 | ||
|
|
2f69be0d41 | ||
|
|
54cb2eb15b | ||
|
|
167992ad48 | ||
|
|
f0dab24079 | ||
|
|
77055faab6 |
20
Cargo.lock
generated
20
Cargo.lock
generated
@@ -1115,6 +1115,12 @@ version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754"
|
||||
|
||||
[[package]]
|
||||
name = "joinery"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72167d68f5fce3b8655487b8038691a3c9984ee769590f93f2a631f4ad64e4f5"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.60"
|
||||
@@ -1960,7 +1966,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.0.70"
|
||||
version = "0.0.72"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assert_cmd",
|
||||
@@ -1992,6 +1998,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"strum",
|
||||
"strum_macros",
|
||||
"titlecase",
|
||||
"toml",
|
||||
"update-informer",
|
||||
"walkdir",
|
||||
@@ -2460,6 +2467,17 @@ version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
|
||||
|
||||
[[package]]
|
||||
name = "titlecase"
|
||||
version = "2.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38397a8cdb017cfeb48bf6c154d6de975ac69ffeed35980fde199d2ee0842042"
|
||||
dependencies = [
|
||||
"joinery",
|
||||
"lazy_static",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.5.9"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.0.70"
|
||||
version = "0.0.72"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
@@ -38,6 +38,7 @@ walkdir = { version = "2.3.2" }
|
||||
strum = { version = "0.24.1", features = ["strum_macros"] }
|
||||
strum_macros = "0.24.3"
|
||||
num-bigint = "0.4.3"
|
||||
titlecase = "2.2.1"
|
||||
|
||||
[dev-dependencies]
|
||||
assert_cmd = "2.0.4"
|
||||
|
||||
16
README.md
16
README.md
@@ -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.70
|
||||
rev: v0.0.72
|
||||
hooks:
|
||||
- id: lint
|
||||
```
|
||||
@@ -217,7 +217,7 @@ ruff also implements some of the most popular Flake8 plugins natively, including
|
||||
- [`flake8-print`](https://pypi.org/project/flake8-print/)
|
||||
- [`flake8-comprehensions`](https://pypi.org/project/flake8-comprehensions/) (12/16)
|
||||
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (3/32)
|
||||
- [`flake8-docstrings`](https://pypi.org/project/flake8-docstrings/) (25/48)
|
||||
- [`flake8-docstrings`](https://pypi.org/project/flake8-docstrings/) (37/48)
|
||||
- [`pyupgrade`](https://pypi.org/project/pyupgrade/) (8/34)
|
||||
|
||||
Beyond rule-set parity, ruff suffers from the following limitations vis-à-vis Flake8:
|
||||
@@ -323,13 +323,25 @@ The 🛠 emoji indicates that a rule is automatically fixable by the `--fix` com
|
||||
| D400 | EndsInPeriod | First line should end with a period | | |
|
||||
| D402 | NoSignature | First line should not be the function's 'signature' | | |
|
||||
| D403 | FirstLineCapitalized | First word of the first line should be properly capitalized | | |
|
||||
| D404 | NoThisPrefix | First word of the docstring should not be `This` | | |
|
||||
| D415 | EndsInPunctuation | First line should end with a period, question mark, or exclamation point | | |
|
||||
| D418 | SkipDocstring | Function decorated with @overload shouldn't contain a docstring | | |
|
||||
| D419 | NonEmpty | Docstring is empty | | |
|
||||
| D201 | NoBlankLineBeforeFunction | No blank lines allowed before function docstring (found 1) | | |
|
||||
| D202 | NoBlankLineAfterFunction | No blank lines allowed after function docstring (found 1) | | |
|
||||
| D211 | NoBlankLineBeforeClass | No blank lines allowed before class docstring | | |
|
||||
| D203 | OneBlankLineBeforeClass | 1 blank line required before class docstring | | |
|
||||
| D204 | OneBlankLineAfterClass | 1 blank line required after class docstring | | |
|
||||
| D405 | CapitalizeSectionName | Section name should be properly capitalized ("returns") | | |
|
||||
| D413 | BlankLineAfterLastSection | Missing blank line after last section ("Returns") | | |
|
||||
| D410 | BlankLineAfterSection | Missing blank line after section ("Returns") | | |
|
||||
| D411 | BlankLineBeforeSection | Missing blank line before section ("Returns") | | |
|
||||
| D406 | NewLineAfterSectionName | Section name should end with a newline ("Returns") | | |
|
||||
| D407 | DashedUnderlineAfterSection | Missing dashed underline after section ("Returns") | | |
|
||||
| D408 | SectionUnderlineAfterName | Section underline should be in the line following the section's name ("Returns") | | |
|
||||
| D409 | SectionUnderlineMatchesSectionLength | Section underline should match the length of its name ("Returns") | | |
|
||||
| D412 | NoBlankLinesBetweenHeaderAndContent | No blank lines allowed between a section header and its content ("Returns") | | |
|
||||
| D414 | NonEmptySection | Section has no content ("Returns") | | |
|
||||
| M001 | UnusedNOQA | Unused `noqa` directive | | 🛠 |
|
||||
|
||||
## Integrations
|
||||
|
||||
497
resources/test/fixtures/sections.py
vendored
Normal file
497
resources/test/fixtures/sections.py
vendored
Normal file
@@ -0,0 +1,497 @@
|
||||
"""A valid module docstring."""
|
||||
|
||||
from .expected import Expectation
|
||||
|
||||
expectation = Expectation()
|
||||
expect = expectation.expect
|
||||
|
||||
|
||||
_D213 = 'D213: Multi-line docstring summary should start at the second line'
|
||||
_D400 = "D400: First line should end with a period (not '!')"
|
||||
|
||||
|
||||
@expect(_D213)
|
||||
@expect("D405: Section name should be properly capitalized "
|
||||
"('Returns', not 'returns')")
|
||||
def not_capitalized(): # noqa: D416
|
||||
"""Toggle the gizmo.
|
||||
|
||||
returns
|
||||
-------
|
||||
A value of some sort.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
@expect(_D213)
|
||||
@expect("D406: Section name should end with a newline "
|
||||
"('Returns', not 'Returns:')")
|
||||
def superfluous_suffix(): # noqa: D416
|
||||
"""Toggle the gizmo.
|
||||
|
||||
Returns:
|
||||
-------
|
||||
A value of some sort.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
@expect(_D213)
|
||||
@expect("D407: Missing dashed underline after section ('Returns')")
|
||||
def no_underline(): # noqa: D416
|
||||
"""Toggle the gizmo.
|
||||
|
||||
Returns
|
||||
A value of some sort.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
@expect(_D213)
|
||||
@expect("D407: Missing dashed underline after section ('Returns')")
|
||||
@expect("D414: Section has no content ('Returns')")
|
||||
def no_underline_and_no_description(): # noqa: D416
|
||||
"""Toggle the gizmo.
|
||||
|
||||
Returns
|
||||
|
||||
"""
|
||||
|
||||
|
||||
@expect(_D213)
|
||||
@expect("D410: Missing blank line after section ('Returns')")
|
||||
@expect("D414: Section has no content ('Returns')")
|
||||
@expect("D411: Missing blank line before section ('Yields')")
|
||||
@expect("D414: Section has no content ('Yields')")
|
||||
def consecutive_sections(): # noqa: D416
|
||||
"""Toggle the gizmo.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Yields
|
||||
------
|
||||
|
||||
Raises
|
||||
------
|
||||
Questions.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
@expect(_D213)
|
||||
@expect("D408: Section underline should be in the line following the "
|
||||
"section's name ('Returns')")
|
||||
def blank_line_before_underline(): # noqa: D416
|
||||
"""Toggle the gizmo.
|
||||
|
||||
Returns
|
||||
|
||||
-------
|
||||
A value of some sort.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
@expect(_D213)
|
||||
@expect("D409: Section underline should match the length of its name "
|
||||
"(Expected 7 dashes in section 'Returns', got 2)")
|
||||
def bad_underline_length(): # noqa: D416
|
||||
"""Toggle the gizmo.
|
||||
|
||||
Returns
|
||||
--
|
||||
A value of some sort.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
@expect(_D213)
|
||||
@expect("D413: Missing blank line after last section ('Returns')")
|
||||
def no_blank_line_after_last_section(): # noqa: D416
|
||||
"""Toggle the gizmo.
|
||||
|
||||
Returns
|
||||
-------
|
||||
A value of some sort.
|
||||
"""
|
||||
|
||||
|
||||
@expect(_D213)
|
||||
@expect("D411: Missing blank line before section ('Returns')")
|
||||
def no_blank_line_before_section(): # noqa: D416
|
||||
"""Toggle the gizmo.
|
||||
|
||||
The function's description.
|
||||
Returns
|
||||
-------
|
||||
A value of some sort.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
@expect(_D213)
|
||||
@expect("D214: Section is over-indented ('Returns')")
|
||||
def section_overindented(): # noqa: D416
|
||||
"""Toggle the gizmo.
|
||||
|
||||
Returns
|
||||
-------
|
||||
A value of some sort.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
@expect(_D213)
|
||||
@expect("D215: Section underline is over-indented (in section 'Returns')")
|
||||
def section_underline_overindented(): # noqa: D416
|
||||
"""Toggle the gizmo.
|
||||
|
||||
Returns
|
||||
-------
|
||||
A value of some sort.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
@expect(_D213)
|
||||
@expect("D215: Section underline is over-indented (in section 'Returns')")
|
||||
@expect("D413: Missing blank line after last section ('Returns')")
|
||||
@expect("D414: Section has no content ('Returns')")
|
||||
def section_underline_overindented_and_contentless(): # noqa: D416
|
||||
"""Toggle the gizmo.
|
||||
|
||||
Returns
|
||||
-------
|
||||
"""
|
||||
|
||||
|
||||
@expect(_D213)
|
||||
def ignore_non_actual_section(): # noqa: D416
|
||||
"""Toggle the gizmo.
|
||||
|
||||
This is the function's description, which will also specify what it
|
||||
returns
|
||||
|
||||
"""
|
||||
|
||||
|
||||
@expect(_D213)
|
||||
@expect("D401: First line should be in imperative mood "
|
||||
"(perhaps 'Return', not 'Returns')")
|
||||
@expect("D400: First line should end with a period (not 's')")
|
||||
@expect("D415: First line should end with a period, question "
|
||||
"mark, or exclamation point (not 's')")
|
||||
@expect("D205: 1 blank line required between summary line and description "
|
||||
"(found 0)")
|
||||
def section_name_in_first_line(): # noqa: D416
|
||||
"""Returns
|
||||
-------
|
||||
A value of some sort.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
@expect(_D213)
|
||||
@expect("D405: Section name should be properly capitalized "
|
||||
"('Short Summary', not 'Short summary')")
|
||||
@expect("D412: No blank lines allowed between a section header and its "
|
||||
"content ('Short Summary')")
|
||||
@expect("D409: Section underline should match the length of its name "
|
||||
"(Expected 7 dashes in section 'Returns', got 6)")
|
||||
@expect("D410: Missing blank line after section ('Returns')")
|
||||
@expect("D411: Missing blank line before section ('Raises')")
|
||||
@expect("D406: Section name should end with a newline "
|
||||
"('Raises', not 'Raises:')")
|
||||
@expect("D407: Missing dashed underline after section ('Raises')")
|
||||
def multiple_sections(): # noqa: D416
|
||||
"""Toggle the gizmo.
|
||||
|
||||
Short summary
|
||||
-------------
|
||||
|
||||
This is the function's description, which will also specify what it
|
||||
returns.
|
||||
|
||||
Returns
|
||||
------
|
||||
Many many wonderful things.
|
||||
Raises:
|
||||
My attention.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
@expect(_D213)
|
||||
def false_positive_section_prefix(): # noqa: D416
|
||||
"""Toggle the gizmo.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
attributes_are_fun: attributes for the function.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
@expect(_D213)
|
||||
def section_names_as_parameter_names(): # noqa: D416
|
||||
"""Toggle the gizmo.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
notes : list
|
||||
A list of wonderful notes.
|
||||
examples: list
|
||||
A list of horrible examples.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
@expect(_D213)
|
||||
@expect("D414: Section has no content ('Returns')")
|
||||
def valid_google_style_section(): # noqa: D406, D407
|
||||
"""Toggle the gizmo.
|
||||
|
||||
Args:
|
||||
note: A random string.
|
||||
|
||||
Returns:
|
||||
|
||||
Raises:
|
||||
RandomError: A random error that occurs randomly.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
@expect(_D213)
|
||||
@expect("D416: Section name should end with a colon "
|
||||
"('Args:', not 'Args')")
|
||||
def missing_colon_google_style_section(): # noqa: D406, D407
|
||||
"""Toggle the gizmo.
|
||||
|
||||
Args
|
||||
note: A random string.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
@expect("D417: Missing argument descriptions in the docstring "
|
||||
"(argument(s) y are missing descriptions in "
|
||||
"'bar' docstring)", func_name="bar")
|
||||
def _test_nested_functions():
|
||||
x = 1
|
||||
|
||||
def bar(y=2): # noqa: D207, D213, D406, D407
|
||||
"""Nested function test for docstrings.
|
||||
|
||||
Will this work when referencing x?
|
||||
|
||||
Args:
|
||||
x: Test something
|
||||
that is broken.
|
||||
|
||||
"""
|
||||
print(x)
|
||||
|
||||
|
||||
@expect(_D213)
|
||||
@expect("D417: Missing argument descriptions in the docstring "
|
||||
"(argument(s) y are missing descriptions in "
|
||||
"'test_missing_google_args' docstring)")
|
||||
def test_missing_google_args(x=1, y=2, _private=3): # noqa: D406, D407
|
||||
"""Toggle the gizmo.
|
||||
|
||||
Args:
|
||||
x (int): The greatest integer.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class TestGoogle: # noqa: D203
|
||||
"""Test class."""
|
||||
|
||||
def test_method(self, test, another_test, _): # noqa: D213, D407
|
||||
"""Test a valid args section.
|
||||
|
||||
Args:
|
||||
test: A parameter.
|
||||
another_test: Another parameter.
|
||||
|
||||
"""
|
||||
|
||||
@expect("D417: Missing argument descriptions in the docstring "
|
||||
"(argument(s) test, y, z are missing descriptions in "
|
||||
"'test_missing_args' docstring)", arg_count=5)
|
||||
def test_missing_args(self, test, x, y, z=3, _private_arg=3): # noqa: D213, D407
|
||||
"""Test a valid args section.
|
||||
|
||||
Args:
|
||||
x: Another parameter.
|
||||
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
@expect("D417: Missing argument descriptions in the docstring "
|
||||
"(argument(s) test, y, z are missing descriptions in "
|
||||
"'test_missing_args_class_method' docstring)", arg_count=5)
|
||||
def test_missing_args_class_method(cls, test, x, y, _, z=3): # noqa: D213, D407
|
||||
"""Test a valid args section.
|
||||
|
||||
Args:
|
||||
x: Another parameter. The parameter below is missing description.
|
||||
y:
|
||||
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
@expect("D417: Missing argument descriptions in the docstring "
|
||||
"(argument(s) a, y, z are missing descriptions in "
|
||||
"'test_missing_args_static_method' docstring)", arg_count=4)
|
||||
def test_missing_args_static_method(a, x, y, _test, z=3): # noqa: D213, D407
|
||||
"""Test a valid args section.
|
||||
|
||||
Args:
|
||||
x: Another parameter.
|
||||
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
@expect("D417: Missing argument descriptions in the docstring "
|
||||
"(argument(s) a, b are missing descriptions in "
|
||||
"'test_missing_docstring' docstring)", arg_count=2)
|
||||
def test_missing_docstring(a, b): # noqa: D213, D407
|
||||
"""Test a valid args section.
|
||||
|
||||
Args:
|
||||
a:
|
||||
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def test_hanging_indent(skip, verbose): # noqa: D213, D407
|
||||
"""Do stuff.
|
||||
|
||||
Args:
|
||||
skip (:attr:`.Skip`):
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
Etiam at tellus a tellus faucibus maximus. Curabitur tellus
|
||||
mauris, semper id vehicula ac, feugiat ut tortor.
|
||||
verbose (bool):
|
||||
If True, print out as much infromation as possible.
|
||||
If False, print out concise "one-liner" information.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
@expect(_D213)
|
||||
@expect("D417: Missing argument descriptions in the docstring "
|
||||
"(argument(s) y are missing descriptions in "
|
||||
"'test_missing_numpy_args' docstring)")
|
||||
def test_missing_numpy_args(_private_arg=0, x=1, y=2): # noqa: D406, D407
|
||||
"""Toggle the gizmo.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
x : int
|
||||
The greatest integer in the history \
|
||||
of the entire world.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class TestNumpy: # noqa: D203
|
||||
"""Test class."""
|
||||
|
||||
def test_method(self, test, another_test, z, _, x=1, y=2, _private_arg=1): # noqa: D213, D407
|
||||
"""Test a valid args section.
|
||||
|
||||
Some long string with a \
|
||||
line continuation.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
test, another_test
|
||||
Some parameters without type.
|
||||
z : some parameter with a very long type description that requires a \
|
||||
line continuation.
|
||||
But no further description.
|
||||
x, y : int
|
||||
Some integer parameters.
|
||||
|
||||
"""
|
||||
|
||||
@expect("D417: Missing argument descriptions in the docstring "
|
||||
"(argument(s) test, y, z are missing descriptions in "
|
||||
"'test_missing_args' docstring)", arg_count=5)
|
||||
def test_missing_args(self, test, x, y, z=3, t=1, _private=0): # noqa: D213, D407
|
||||
"""Test a valid args section.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
x, t : int
|
||||
Some parameters.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
@expect("D417: Missing argument descriptions in the docstring "
|
||||
"(argument(s) test, y, z are missing descriptions in "
|
||||
"'test_missing_args_class_method' docstring)", arg_count=4)
|
||||
def test_missing_args_class_method(cls, test, x, y, z=3): # noqa: D213, D407
|
||||
"""Test a valid args section.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
z
|
||||
x
|
||||
Another parameter. The parameters y, test below are
|
||||
missing descriptions. The parameter z above is also missing
|
||||
a description.
|
||||
y
|
||||
test
|
||||
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
@expect("D417: Missing argument descriptions in the docstring "
|
||||
"(argument(s) a, z are missing descriptions in "
|
||||
"'test_missing_args_static_method' docstring)", arg_count=3)
|
||||
def test_missing_args_static_method(a, x, y, z=3, t=1): # noqa: D213, D407
|
||||
"""Test a valid args section.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
x, y
|
||||
Another parameter.
|
||||
t : int
|
||||
Yet another parameter.
|
||||
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def test_mixing_numpy_and_google(danger): # noqa: D213
|
||||
"""Repro for #388.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
danger
|
||||
Zoneeeeee!
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class TestIncorrectIndent: # noqa: D203
|
||||
"""Test class."""
|
||||
|
||||
@expect("D417: Missing argument descriptions in the docstring "
|
||||
"(argument(s) y are missing descriptions in "
|
||||
"'test_incorrect_indent' docstring)", arg_count=3)
|
||||
def test_incorrect_indent(self, x=1, y=2): # noqa: D207, D213, D407
|
||||
"""Reproducing issue #437.
|
||||
|
||||
Testing this incorrectly indented docstring.
|
||||
|
||||
Args:
|
||||
x: Test argument.
|
||||
|
||||
"""
|
||||
@@ -1,7 +1,3 @@
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
use itertools::Itertools;
|
||||
use rustpython_parser::ast::Location;
|
||||
|
||||
@@ -24,17 +20,15 @@ impl From<bool> for Mode {
|
||||
}
|
||||
|
||||
/// Auto-fix errors in a file, and write the fixed source code to disk.
|
||||
pub fn fix_file(checks: &mut [Check], contents: &str, path: &Path) -> Result<()> {
|
||||
pub fn fix_file(checks: &mut [Check], contents: &str) -> Option<String> {
|
||||
if checks.iter().all(|check| check.fix.is_none()) {
|
||||
return Ok(());
|
||||
return None;
|
||||
}
|
||||
|
||||
let output = apply_fixes(
|
||||
Some(apply_fixes(
|
||||
checks.iter_mut().filter_map(|check| check.fix.as_mut()),
|
||||
contents,
|
||||
);
|
||||
|
||||
fs::write(path, output).map_err(|e| e.into())
|
||||
))
|
||||
}
|
||||
|
||||
/// Apply a series of fixes.
|
||||
|
||||
@@ -21,12 +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::{Definition, DefinitionKind, Documentable};
|
||||
use crate::docstrings::docstring_checks;
|
||||
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;
|
||||
|
||||
@@ -571,8 +572,12 @@ where
|
||||
let prev_visibile_scope = self.visible_scope.clone();
|
||||
match &stmt.node {
|
||||
StmtKind::FunctionDef { body, .. } | StmtKind::AsyncFunctionDef { body, .. } => {
|
||||
let definition =
|
||||
docstrings::extract(&self.visible_scope, stmt, body, &Documentable::Function);
|
||||
let definition = docstring_checks::extract(
|
||||
&self.visible_scope,
|
||||
stmt,
|
||||
body,
|
||||
&Documentable::Function,
|
||||
);
|
||||
let scope = transition_scope(&self.visible_scope, stmt, &Documentable::Function);
|
||||
self.docstrings.push((definition, scope.visibility.clone()));
|
||||
self.visible_scope = scope;
|
||||
@@ -585,8 +590,12 @@ where
|
||||
));
|
||||
}
|
||||
StmtKind::ClassDef { body, .. } => {
|
||||
let definition =
|
||||
docstrings::extract(&self.visible_scope, stmt, body, &Documentable::Class);
|
||||
let definition = docstring_checks::extract(
|
||||
&self.visible_scope,
|
||||
stmt,
|
||||
body,
|
||||
&Documentable::Class,
|
||||
);
|
||||
let scope = transition_scope(&self.visible_scope, stmt, &Documentable::Class);
|
||||
self.docstrings.push((definition, scope.visibility.clone()));
|
||||
self.visible_scope = scope;
|
||||
@@ -1646,7 +1655,7 @@ impl<'a> Checker<'a> {
|
||||
where
|
||||
'b: 'a,
|
||||
{
|
||||
let docstring = docstrings::docstring_from(python_ast);
|
||||
let docstring = docstring_checks::docstring_from(python_ast);
|
||||
self.docstrings.push((
|
||||
Definition {
|
||||
kind: if self.path.ends_with("__init__.py") {
|
||||
@@ -1909,54 +1918,77 @@ impl<'a> Checker<'a> {
|
||||
|
||||
fn check_docstrings(&mut self) {
|
||||
while let Some((docstring, visibility)) = self.docstrings.pop() {
|
||||
if !docstrings::not_empty(self, &docstring) {
|
||||
if !docstring_checks::not_empty(self, &docstring) {
|
||||
continue;
|
||||
}
|
||||
if !docstrings::not_missing(self, &docstring, &visibility) {
|
||||
if !docstring_checks::not_missing(self, &docstring, &visibility) {
|
||||
continue;
|
||||
}
|
||||
if self.settings.enabled.contains(&CheckCode::D200) {
|
||||
docstrings::one_liner(self, &docstring);
|
||||
docstring_checks::one_liner(self, &docstring);
|
||||
}
|
||||
if self.settings.enabled.contains(&CheckCode::D201)
|
||||
|| self.settings.enabled.contains(&CheckCode::D202)
|
||||
{
|
||||
docstrings::blank_before_after_function(self, &docstring);
|
||||
docstring_checks::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)
|
||||
{
|
||||
docstrings::blank_before_after_class(self, &docstring);
|
||||
docstring_checks::blank_before_after_class(self, &docstring);
|
||||
}
|
||||
if self.settings.enabled.contains(&CheckCode::D205) {
|
||||
docstrings::blank_after_summary(self, &docstring);
|
||||
docstring_checks::blank_after_summary(self, &docstring);
|
||||
}
|
||||
if self.settings.enabled.contains(&CheckCode::D209) {
|
||||
docstrings::newline_after_last_paragraph(self, &docstring);
|
||||
docstring_checks::newline_after_last_paragraph(self, &docstring);
|
||||
}
|
||||
if self.settings.enabled.contains(&CheckCode::D210) {
|
||||
docstrings::no_surrounding_whitespace(self, &docstring);
|
||||
docstring_checks::no_surrounding_whitespace(self, &docstring);
|
||||
}
|
||||
if self.settings.enabled.contains(&CheckCode::D212)
|
||||
|| self.settings.enabled.contains(&CheckCode::D213)
|
||||
{
|
||||
docstrings::multi_line_summary_start(self, &docstring);
|
||||
docstring_checks::multi_line_summary_start(self, &docstring);
|
||||
}
|
||||
if self.settings.enabled.contains(&CheckCode::D300) {
|
||||
docstrings::triple_quotes(self, &docstring);
|
||||
docstring_checks::triple_quotes(self, &docstring);
|
||||
}
|
||||
if self.settings.enabled.contains(&CheckCode::D400) {
|
||||
docstrings::ends_with_period(self, &docstring);
|
||||
docstring_checks::ends_with_period(self, &docstring);
|
||||
}
|
||||
if self.settings.enabled.contains(&CheckCode::D402) {
|
||||
docstrings::no_signature(self, &docstring);
|
||||
docstring_checks::no_signature(self, &docstring);
|
||||
}
|
||||
if self.settings.enabled.contains(&CheckCode::D403) {
|
||||
docstrings::capitalized(self, &docstring);
|
||||
docstring_checks::capitalized(self, &docstring);
|
||||
}
|
||||
if self.settings.enabled.contains(&CheckCode::D404) {
|
||||
docstring_checks::starts_with_this(self, &docstring);
|
||||
}
|
||||
if self.settings.enabled.contains(&CheckCode::D415) {
|
||||
docstrings::ends_with_punctuation(self, &docstring);
|
||||
docstring_checks::ends_with_punctuation(self, &docstring);
|
||||
}
|
||||
if self.settings.enabled.contains(&CheckCode::D418) {
|
||||
docstring_checks::if_needed(self, &docstring);
|
||||
}
|
||||
if self.settings.enabled.contains(&CheckCode::D407)
|
||||
|| self.settings.enabled.contains(&CheckCode::D414)
|
||||
|| self.settings.enabled.contains(&CheckCode::D407)
|
||||
|| self.settings.enabled.contains(&CheckCode::D212)
|
||||
|| self.settings.enabled.contains(&CheckCode::D408)
|
||||
|| self.settings.enabled.contains(&CheckCode::D409)
|
||||
|| self.settings.enabled.contains(&CheckCode::D414)
|
||||
|| self.settings.enabled.contains(&CheckCode::D412)
|
||||
|| self.settings.enabled.contains(&CheckCode::D414)
|
||||
|| self.settings.enabled.contains(&CheckCode::D405)
|
||||
|| self.settings.enabled.contains(&CheckCode::D413)
|
||||
|| self.settings.enabled.contains(&CheckCode::D410)
|
||||
|| self.settings.enabled.contains(&CheckCode::D411)
|
||||
|| self.settings.enabled.contains(&CheckCode::D406)
|
||||
{
|
||||
docstring_checks::check_sections(self, &docstring);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
174
src/checks.rs
174
src/checks.rs
@@ -161,22 +161,34 @@ pub enum CheckCode {
|
||||
D106,
|
||||
D107,
|
||||
D200,
|
||||
D201,
|
||||
D202,
|
||||
D203,
|
||||
D204,
|
||||
D205,
|
||||
D209,
|
||||
D210,
|
||||
D211,
|
||||
D212,
|
||||
D213,
|
||||
D300,
|
||||
D400,
|
||||
D402,
|
||||
D403,
|
||||
D404,
|
||||
D405,
|
||||
D406,
|
||||
D407,
|
||||
D408,
|
||||
D409,
|
||||
D410,
|
||||
D411,
|
||||
D412,
|
||||
D413,
|
||||
D414,
|
||||
D415,
|
||||
D418,
|
||||
D419,
|
||||
D201,
|
||||
D202,
|
||||
D211,
|
||||
D203,
|
||||
D204,
|
||||
// Meta
|
||||
M001,
|
||||
}
|
||||
@@ -275,31 +287,43 @@ pub enum CheckKind {
|
||||
UsePEP604Annotation,
|
||||
SuperCallWithParameters,
|
||||
// pydocstyle
|
||||
BlankLineAfterLastSection(String),
|
||||
BlankLineAfterSection(String),
|
||||
BlankLineBeforeSection(String),
|
||||
CapitalizeSectionName(String),
|
||||
DashedUnderlineAfterSection(String),
|
||||
EndsInPeriod,
|
||||
EndsInPunctuation,
|
||||
FirstLineCapitalized,
|
||||
FitsOnOneLine,
|
||||
MagicMethod,
|
||||
MultiLineSummaryFirstLine,
|
||||
MultiLineSummarySecondLine,
|
||||
NewLineAfterLastParagraph,
|
||||
NoBlankLineAfterSummary,
|
||||
NoSurroundingWhitespace,
|
||||
NonEmpty,
|
||||
UsesTripleQuotes,
|
||||
NoSignature,
|
||||
NoBlankLineBeforeFunction(usize),
|
||||
NewLineAfterSectionName(String),
|
||||
NoBlankLineAfterFunction(usize),
|
||||
NoBlankLineAfterSummary,
|
||||
NoBlankLineBeforeClass(usize),
|
||||
OneBlankLineBeforeClass(usize),
|
||||
NoBlankLineBeforeFunction(usize),
|
||||
NoBlankLinesBetweenHeaderAndContent(String),
|
||||
NoSignature,
|
||||
NoSurroundingWhitespace,
|
||||
NoThisPrefix,
|
||||
NonEmpty,
|
||||
NonEmptySection(String),
|
||||
OneBlankLineAfterClass(usize),
|
||||
PublicModule,
|
||||
OneBlankLineBeforeClass(usize),
|
||||
PublicClass,
|
||||
PublicMethod,
|
||||
PublicFunction,
|
||||
PublicPackage,
|
||||
MagicMethod,
|
||||
PublicNestedClass,
|
||||
PublicInit,
|
||||
PublicMethod,
|
||||
PublicModule,
|
||||
PublicNestedClass,
|
||||
PublicPackage,
|
||||
SectionUnderlineAfterName(String),
|
||||
SectionUnderlineMatchesSectionLength(String),
|
||||
SkipDocstring,
|
||||
UsesTripleQuotes,
|
||||
// Meta
|
||||
UnusedNOQA(Option<Vec<String>>),
|
||||
}
|
||||
@@ -422,22 +446,38 @@ impl CheckCode {
|
||||
CheckCode::D106 => CheckKind::PublicNestedClass,
|
||||
CheckCode::D107 => CheckKind::PublicInit,
|
||||
CheckCode::D200 => CheckKind::FitsOnOneLine,
|
||||
CheckCode::D201 => CheckKind::NoBlankLineBeforeFunction(1),
|
||||
CheckCode::D202 => CheckKind::NoBlankLineAfterFunction(1),
|
||||
CheckCode::D203 => CheckKind::OneBlankLineBeforeClass(0),
|
||||
CheckCode::D204 => CheckKind::OneBlankLineAfterClass(0),
|
||||
CheckCode::D205 => CheckKind::NoBlankLineAfterSummary,
|
||||
CheckCode::D209 => CheckKind::NewLineAfterLastParagraph,
|
||||
CheckCode::D210 => CheckKind::NoSurroundingWhitespace,
|
||||
CheckCode::D400 => CheckKind::EndsInPeriod,
|
||||
CheckCode::D419 => CheckKind::NonEmpty,
|
||||
CheckCode::D211 => CheckKind::NoBlankLineBeforeClass(1),
|
||||
CheckCode::D212 => CheckKind::MultiLineSummaryFirstLine,
|
||||
CheckCode::D213 => CheckKind::MultiLineSummarySecondLine,
|
||||
CheckCode::D300 => CheckKind::UsesTripleQuotes,
|
||||
CheckCode::D400 => CheckKind::EndsInPeriod,
|
||||
CheckCode::D402 => CheckKind::NoSignature,
|
||||
CheckCode::D403 => CheckKind::FirstLineCapitalized,
|
||||
CheckCode::D404 => CheckKind::NoThisPrefix,
|
||||
CheckCode::D405 => CheckKind::CapitalizeSectionName("returns".to_string()),
|
||||
CheckCode::D406 => CheckKind::NewLineAfterSectionName("Returns".to_string()),
|
||||
CheckCode::D407 => CheckKind::DashedUnderlineAfterSection("Returns".to_string()),
|
||||
CheckCode::D408 => CheckKind::SectionUnderlineAfterName("Returns".to_string()),
|
||||
CheckCode::D409 => {
|
||||
CheckKind::SectionUnderlineMatchesSectionLength("Returns".to_string())
|
||||
}
|
||||
CheckCode::D410 => CheckKind::BlankLineAfterSection("Returns".to_string()),
|
||||
CheckCode::D411 => CheckKind::BlankLineBeforeSection("Returns".to_string()),
|
||||
CheckCode::D412 => {
|
||||
CheckKind::NoBlankLinesBetweenHeaderAndContent("Returns".to_string())
|
||||
}
|
||||
CheckCode::D413 => CheckKind::BlankLineAfterLastSection("Returns".to_string()),
|
||||
CheckCode::D414 => CheckKind::NonEmptySection("Returns".to_string()),
|
||||
CheckCode::D415 => CheckKind::EndsInPunctuation,
|
||||
CheckCode::D201 => CheckKind::NoBlankLineBeforeFunction(1),
|
||||
CheckCode::D202 => CheckKind::NoBlankLineAfterFunction(1),
|
||||
CheckCode::D211 => CheckKind::NoBlankLineBeforeClass(1),
|
||||
CheckCode::D203 => CheckKind::OneBlankLineBeforeClass(0),
|
||||
CheckCode::D204 => CheckKind::OneBlankLineAfterClass(0),
|
||||
CheckCode::D418 => CheckKind::SkipDocstring,
|
||||
CheckCode::D419 => CheckKind::NonEmpty,
|
||||
// Meta
|
||||
CheckCode::M001 => CheckKind::UnusedNOQA(None),
|
||||
}
|
||||
@@ -527,31 +567,43 @@ impl CheckKind {
|
||||
CheckKind::UselessObjectInheritance(_) => &CheckCode::U004,
|
||||
CheckKind::SuperCallWithParameters => &CheckCode::U008,
|
||||
// pydocstyle
|
||||
CheckKind::PublicModule => &CheckCode::D100,
|
||||
CheckKind::PublicClass => &CheckCode::D101,
|
||||
CheckKind::PublicMethod => &CheckCode::D102,
|
||||
CheckKind::PublicFunction => &CheckCode::D103,
|
||||
CheckKind::PublicPackage => &CheckCode::D104,
|
||||
CheckKind::MagicMethod => &CheckCode::D105,
|
||||
CheckKind::PublicNestedClass => &CheckCode::D106,
|
||||
CheckKind::PublicInit => &CheckCode::D107,
|
||||
CheckKind::FitsOnOneLine => &CheckCode::D200,
|
||||
CheckKind::NoBlankLineAfterSummary => &CheckCode::D205,
|
||||
CheckKind::NewLineAfterLastParagraph => &CheckCode::D209,
|
||||
CheckKind::NoSurroundingWhitespace => &CheckCode::D210,
|
||||
CheckKind::BlankLineAfterLastSection(_) => &CheckCode::D413,
|
||||
CheckKind::BlankLineAfterSection(_) => &CheckCode::D410,
|
||||
CheckKind::BlankLineBeforeSection(_) => &CheckCode::D411,
|
||||
CheckKind::CapitalizeSectionName(_) => &CheckCode::D405,
|
||||
CheckKind::DashedUnderlineAfterSection(_) => &CheckCode::D407,
|
||||
CheckKind::EndsInPeriod => &CheckCode::D400,
|
||||
CheckKind::NonEmpty => &CheckCode::D419,
|
||||
CheckKind::EndsInPunctuation => &CheckCode::D415,
|
||||
CheckKind::FirstLineCapitalized => &CheckCode::D403,
|
||||
CheckKind::FitsOnOneLine => &CheckCode::D200,
|
||||
CheckKind::MagicMethod => &CheckCode::D105,
|
||||
CheckKind::MultiLineSummaryFirstLine => &CheckCode::D212,
|
||||
CheckKind::MultiLineSummarySecondLine => &CheckCode::D213,
|
||||
CheckKind::UsesTripleQuotes => &CheckCode::D300,
|
||||
CheckKind::NoSignature => &CheckCode::D402,
|
||||
CheckKind::FirstLineCapitalized => &CheckCode::D403,
|
||||
CheckKind::EndsInPunctuation => &CheckCode::D415,
|
||||
CheckKind::NoBlankLineBeforeFunction(_) => &CheckCode::D201,
|
||||
CheckKind::NewLineAfterLastParagraph => &CheckCode::D209,
|
||||
CheckKind::NewLineAfterSectionName(_) => &CheckCode::D406,
|
||||
CheckKind::NoBlankLineAfterFunction(_) => &CheckCode::D202,
|
||||
CheckKind::NoBlankLineAfterSummary => &CheckCode::D205,
|
||||
CheckKind::NoBlankLineBeforeClass(_) => &CheckCode::D211,
|
||||
CheckKind::OneBlankLineBeforeClass(_) => &CheckCode::D203,
|
||||
CheckKind::NoBlankLineBeforeFunction(_) => &CheckCode::D201,
|
||||
CheckKind::NoBlankLinesBetweenHeaderAndContent(_) => &CheckCode::D412,
|
||||
CheckKind::NoSignature => &CheckCode::D402,
|
||||
CheckKind::NoSurroundingWhitespace => &CheckCode::D210,
|
||||
CheckKind::NoThisPrefix => &CheckCode::D404,
|
||||
CheckKind::NonEmpty => &CheckCode::D419,
|
||||
CheckKind::NonEmptySection(_) => &CheckCode::D414,
|
||||
CheckKind::OneBlankLineAfterClass(_) => &CheckCode::D204,
|
||||
CheckKind::OneBlankLineBeforeClass(_) => &CheckCode::D203,
|
||||
CheckKind::PublicClass => &CheckCode::D101,
|
||||
CheckKind::PublicFunction => &CheckCode::D103,
|
||||
CheckKind::PublicInit => &CheckCode::D107,
|
||||
CheckKind::PublicMethod => &CheckCode::D102,
|
||||
CheckKind::PublicModule => &CheckCode::D100,
|
||||
CheckKind::PublicNestedClass => &CheckCode::D106,
|
||||
CheckKind::PublicPackage => &CheckCode::D104,
|
||||
CheckKind::SectionUnderlineAfterName(_) => &CheckCode::D408,
|
||||
CheckKind::SectionUnderlineMatchesSectionLength(_) => &CheckCode::D409,
|
||||
CheckKind::SkipDocstring => &CheckCode::D418,
|
||||
CheckKind::UsesTripleQuotes => &CheckCode::D300,
|
||||
// Meta
|
||||
CheckKind::UnusedNOQA(_) => &CheckCode::M001,
|
||||
}
|
||||
@@ -851,6 +903,42 @@ impl CheckKind {
|
||||
CheckKind::MagicMethod => "Missing docstring in magic method".to_string(),
|
||||
CheckKind::PublicNestedClass => "Missing docstring in public nested class".to_string(),
|
||||
CheckKind::PublicInit => "Missing docstring in __init__".to_string(),
|
||||
CheckKind::NoThisPrefix => {
|
||||
"First word of the docstring should not be `This`".to_string()
|
||||
}
|
||||
CheckKind::SkipDocstring => {
|
||||
"Function decorated with @overload shouldn't contain a docstring".to_string()
|
||||
}
|
||||
CheckKind::CapitalizeSectionName(name) => {
|
||||
format!("Section name should be properly capitalized (\"{name}\")")
|
||||
}
|
||||
CheckKind::BlankLineAfterLastSection(name) => {
|
||||
format!("Missing blank line after last section (\"{name}\")")
|
||||
}
|
||||
CheckKind::BlankLineAfterSection(name) => {
|
||||
format!("Missing blank line after section (\"{name}\")")
|
||||
}
|
||||
CheckKind::BlankLineBeforeSection(name) => {
|
||||
format!("Missing blank line before section (\"{name}\")")
|
||||
}
|
||||
CheckKind::NewLineAfterSectionName(name) => {
|
||||
format!("Section name should end with a newline (\"{name}\")")
|
||||
}
|
||||
CheckKind::DashedUnderlineAfterSection(name) => {
|
||||
format!("Missing dashed underline after section (\"{name}\")")
|
||||
}
|
||||
CheckKind::SectionUnderlineAfterName(name) => {
|
||||
format!("Section underline should be in the line following the section's name (\"{name}\")")
|
||||
}
|
||||
CheckKind::SectionUnderlineMatchesSectionLength(name) => {
|
||||
format!("Section underline should match the length of its name (\"{name}\")")
|
||||
}
|
||||
CheckKind::NoBlankLinesBetweenHeaderAndContent(name) => {
|
||||
format!(
|
||||
"No blank lines allowed between a section header and its content (\"{name}\")"
|
||||
)
|
||||
}
|
||||
CheckKind::NonEmptySection(name) => format!("Section has no content (\"{name}\")"),
|
||||
// Meta
|
||||
CheckKind::UnusedNOQA(codes) => match codes {
|
||||
None => "Unused `noqa` directive".to_string(),
|
||||
|
||||
@@ -1,633 +1,3 @@
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use rustpython_ast::{Constant, Expr, ExprKind, Location, Stmt, StmtKind};
|
||||
|
||||
use crate::ast::types::Range;
|
||||
use crate::check_ast::Checker;
|
||||
use crate::checks::{Check, CheckCode, CheckKind};
|
||||
use crate::visibility::{is_init, is_magic, is_overload, Modifier, Visibility, VisibleScope};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum DefinitionKind<'a> {
|
||||
Module,
|
||||
Package,
|
||||
Class(&'a Stmt),
|
||||
NestedClass(&'a Stmt),
|
||||
Function(&'a Stmt),
|
||||
NestedFunction(&'a Stmt),
|
||||
Method(&'a Stmt),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Definition<'a> {
|
||||
pub kind: DefinitionKind<'a>,
|
||||
pub docstring: Option<&'a Expr>,
|
||||
}
|
||||
|
||||
pub enum Documentable {
|
||||
Class,
|
||||
Function,
|
||||
}
|
||||
|
||||
/// 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.
|
||||
fn range_for(docstring: &Expr) -> Range {
|
||||
// RustPython currently omits the first quotation mark in a string, so offset the location.
|
||||
Range {
|
||||
location: Location::new(docstring.location.row(), docstring.location.column() - 1),
|
||||
end_location: docstring.end_location,
|
||||
}
|
||||
}
|
||||
|
||||
/// D100, D101, D102, D103, D104, D105, D106, D107
|
||||
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_for(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_for(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 {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::NoBlankLineBeforeFunction(blank_lines_before),
|
||||
range_for(docstring),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if checker.settings.enabled.contains(&CheckCode::D202) {
|
||||
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));
|
||||
// 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.
|
||||
if !all_blank_after
|
||||
&& blank_lines_after != 0
|
||||
&& !(blank_lines_after == 1
|
||||
&& INNER_FUNCTION_OR_CLASS_REGEX.is_match(after))
|
||||
{
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::NoBlankLineAfterFunction(blank_lines_after),
|
||||
range_for(docstring),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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_for(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 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) {
|
||||
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),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::NoBlankLineAfterSummary,
|
||||
range_for(docstring),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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_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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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(
|
||||
CheckKind::NoSurroundingWhitespace,
|
||||
range_for(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
|
||||
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
|
||||
{
|
||||
let content = checker
|
||||
.locator
|
||||
.slice_source_code_range(&range_for(docstring));
|
||||
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(
|
||||
CheckKind::UsesTripleQuotes,
|
||||
range_for(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_for(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_for(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_for(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_for(docstring),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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_for(docstring)));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
pub mod docstring_checks;
|
||||
pub mod sections;
|
||||
pub mod types;
|
||||
|
||||
676
src/docstrings/docstring_checks.rs
Normal file
676
src/docstrings/docstring_checks.rs
Normal file
@@ -0,0 +1,676 @@
|
||||
//! Abstractions for tracking and validating docstrings in Python code.
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use rustpython_ast::{Constant, Expr, ExprKind, Location, Stmt, 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,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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_for(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_for(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 {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::NoBlankLineBeforeFunction(blank_lines_before),
|
||||
range_for(docstring),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if checker.settings.enabled.contains(&CheckCode::D202) {
|
||||
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));
|
||||
// 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.
|
||||
if !all_blank_after
|
||||
&& blank_lines_after != 0
|
||||
&& !(blank_lines_after == 1
|
||||
&& INNER_FUNCTION_OR_CLASS_REGEX.is_match(after))
|
||||
{
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::NoBlankLineAfterFunction(blank_lines_after),
|
||||
range_for(docstring),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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_for(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 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) {
|
||||
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),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::NoBlankLineAfterSummary,
|
||||
range_for(docstring),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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_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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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(
|
||||
CheckKind::NoSurroundingWhitespace,
|
||||
range_for(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
|
||||
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
|
||||
{
|
||||
let content = checker
|
||||
.locator
|
||||
.slice_source_code_range(&range_for(docstring));
|
||||
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(
|
||||
CheckKind::UsesTripleQuotes,
|
||||
range_for(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_for(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_for(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_for(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_for(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_for(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_for(docstring)));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn check_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;
|
||||
}
|
||||
for context in §ion_contexts(&lines) {
|
||||
check_numpy_section(checker, definition, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
384
src/docstrings/sections.rs
Normal file
384
src/docstrings/sections.rs
Normal file
@@ -0,0 +1,384 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use titlecase::titlecase;
|
||||
|
||||
use crate::check_ast::Checker;
|
||||
use crate::checks::{Check, CheckCode, CheckKind};
|
||||
use crate::docstrings::docstring_checks::range_for;
|
||||
use crate::docstrings::types::Definition;
|
||||
|
||||
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 get_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(&get_leading_words(line).to_lowercase().as_str())
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SectionContext<'a> {
|
||||
section_name: String,
|
||||
previous_line: &'a str,
|
||||
line: &'a str,
|
||||
following_lines: &'a [&'a str],
|
||||
original_index: usize,
|
||||
is_last_section: bool,
|
||||
}
|
||||
|
||||
/// Check if the suspected context is really a section header.
|
||||
fn is_docstring_section(context: &SectionContext) -> bool {
|
||||
let section_name_suffix = context
|
||||
.line
|
||||
.trim()
|
||||
.strip_prefix(&context.section_name)
|
||||
.unwrap()
|
||||
.trim();
|
||||
let this_looks_like_a_section_name =
|
||||
section_name_suffix == ":" || section_name_suffix.is_empty();
|
||||
if !this_looks_like_a_section_name {
|
||||
return false;
|
||||
}
|
||||
|
||||
let prev_line = context.previous_line.trim();
|
||||
let prev_line_ends_with_punctuation = [',', ';', '.', '-', '\\', '/', ']', '}', ')']
|
||||
.into_iter()
|
||||
.any(|char| prev_line.ends_with(char));
|
||||
let prev_line_looks_like_end_of_paragraph =
|
||||
prev_line_ends_with_punctuation || prev_line.is_empty();
|
||||
if !prev_line_looks_like_end_of_paragraph {
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// Extract all `SectionContext` values from a docstring.
|
||||
pub fn section_contexts<'a>(lines: &'a [&'a str]) -> Vec<SectionContext<'a>> {
|
||||
let suspected_section_indices: Vec<usize> = lines
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(lineno, line)| {
|
||||
if lineno > 0 && suspected_as_section(line) {
|
||||
Some(lineno)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut contexts = vec![];
|
||||
for lineno in suspected_section_indices {
|
||||
let context = SectionContext {
|
||||
section_name: get_leading_words(lines[lineno]),
|
||||
previous_line: lines[lineno - 1],
|
||||
line: lines[lineno],
|
||||
following_lines: &lines[lineno + 1..],
|
||||
original_index: lineno,
|
||||
is_last_section: false,
|
||||
};
|
||||
if is_docstring_section(&context) {
|
||||
contexts.push(context);
|
||||
}
|
||||
}
|
||||
|
||||
let mut truncated_contexts = vec![];
|
||||
let mut end: Option<usize> = None;
|
||||
for context in contexts.into_iter().rev() {
|
||||
let next_end = context.original_index;
|
||||
truncated_contexts.push(SectionContext {
|
||||
section_name: context.section_name,
|
||||
previous_line: context.previous_line,
|
||||
line: context.line,
|
||||
following_lines: if let Some(end) = end {
|
||||
&lines[context.original_index + 1..end]
|
||||
} else {
|
||||
context.following_lines
|
||||
},
|
||||
original_index: context.original_index,
|
||||
is_last_section: end.is_none(),
|
||||
});
|
||||
end = Some(next_end);
|
||||
}
|
||||
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() {
|
||||
// D407
|
||||
if checker.settings.enabled.contains(&CheckCode::D407) {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::DashedUnderlineAfterSection(context.section_name.to_string()),
|
||||
range_for(docstring),
|
||||
));
|
||||
}
|
||||
// D414
|
||||
if checker.settings.enabled.contains(&CheckCode::D414) {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::NonEmptySection(context.section_name.to_string()),
|
||||
range_for(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 {
|
||||
// D407
|
||||
if checker.settings.enabled.contains(&CheckCode::D407) {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::DashedUnderlineAfterSection(context.section_name.to_string()),
|
||||
range_for(docstring),
|
||||
));
|
||||
}
|
||||
if blank_lines_after_header > 0 {
|
||||
// D212
|
||||
if checker.settings.enabled.contains(&CheckCode::D212) {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::NoBlankLinesBetweenHeaderAndContent(
|
||||
context.section_name.to_string(),
|
||||
),
|
||||
range_for(docstring),
|
||||
));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if blank_lines_after_header > 0 {
|
||||
// D408
|
||||
if checker.settings.enabled.contains(&CheckCode::D408) {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::SectionUnderlineAfterName(context.section_name.to_string()),
|
||||
range_for(docstring),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if non_empty_line
|
||||
.trim()
|
||||
.chars()
|
||||
.filter(|char| *char == '-')
|
||||
.count()
|
||||
!= context.section_name.len()
|
||||
{
|
||||
// D409
|
||||
if checker.settings.enabled.contains(&CheckCode::D409) {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::SectionUnderlineMatchesSectionLength(
|
||||
context.section_name.to_string(),
|
||||
),
|
||||
range_for(docstring),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(charlie): Implement D215, which requires indentation and leading space tracking.
|
||||
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()) {
|
||||
// D414
|
||||
if checker.settings.enabled.contains(&CheckCode::D414) {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::NonEmptySection(context.section_name.to_string()),
|
||||
range_for(docstring),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
// 412
|
||||
if checker.settings.enabled.contains(&CheckCode::D412) {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::NoBlankLinesBetweenHeaderAndContent(
|
||||
context.section_name.to_string(),
|
||||
),
|
||||
range_for(docstring),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// D414
|
||||
if checker.settings.enabled.contains(&CheckCode::D414) {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::NonEmptySection(context.section_name.to_string()),
|
||||
range_for(docstring),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn check_common_section(checker: &mut Checker, definition: &Definition, context: &SectionContext) {
|
||||
// TODO(charlie): Implement D214, which requires indentation and leading space tracking.
|
||||
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())
|
||||
{
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::CapitalizeSectionName(context.section_name.to_string()),
|
||||
range_for(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) {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::BlankLineAfterLastSection(context.section_name.to_string()),
|
||||
range_for(docstring),
|
||||
))
|
||||
}
|
||||
} else {
|
||||
if checker.settings.enabled.contains(&CheckCode::D410) {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::BlankLineAfterSection(context.section_name.to_string()),
|
||||
range_for(docstring),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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_for(docstring),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_numpy_section(
|
||||
checker: &mut Checker,
|
||||
definition: &Definition,
|
||||
context: &SectionContext,
|
||||
) {
|
||||
// TODO(charlie): Implement `_check_parameters_section`.
|
||||
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),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
23
src/docstrings/types.rs
Normal file
23
src/docstrings/types.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
use rustpython_ast::{Expr, Stmt};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum DefinitionKind<'a> {
|
||||
Module,
|
||||
Package,
|
||||
Class(&'a Stmt),
|
||||
NestedClass(&'a Stmt),
|
||||
Function(&'a Stmt),
|
||||
NestedFunction(&'a Stmt),
|
||||
Method(&'a Stmt),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Definition<'a> {
|
||||
pub kind: DefinitionKind<'a>,
|
||||
pub docstring: Option<&'a Expr>,
|
||||
}
|
||||
|
||||
pub enum Documentable {
|
||||
Class,
|
||||
Function,
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
#![allow(clippy::collapsible_if, clippy::collapsible_else_if)]
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
@@ -17,7 +18,7 @@ mod check_lines;
|
||||
pub mod checks;
|
||||
pub mod cli;
|
||||
pub mod code_gen;
|
||||
pub mod docstrings;
|
||||
mod docstrings;
|
||||
pub mod fs;
|
||||
pub mod linter;
|
||||
pub mod logging;
|
||||
|
||||
175
src/linter.rs
175
src/linter.rs
@@ -1,4 +1,6 @@
|
||||
use std::fs::write;
|
||||
use std::io;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
@@ -83,7 +85,12 @@ pub(crate) fn check_path(
|
||||
Ok(checks)
|
||||
}
|
||||
|
||||
pub fn lint_stdin(path: &Path, stdin: &str, settings: &Settings) -> Result<Vec<Message>> {
|
||||
pub fn lint_stdin(
|
||||
path: &Path,
|
||||
stdin: &str,
|
||||
settings: &Settings,
|
||||
autofix: &fixer::Mode,
|
||||
) -> Result<Vec<Message>> {
|
||||
// Tokenize once.
|
||||
let tokens: Vec<LexResult> = tokenize(stdin);
|
||||
|
||||
@@ -91,14 +98,16 @@ pub fn lint_stdin(path: &Path, stdin: &str, settings: &Settings) -> Result<Vec<M
|
||||
let noqa_line_for = noqa::extract_noqa_line_for(&tokens);
|
||||
|
||||
// Generate checks.
|
||||
let checks = check_path(
|
||||
path,
|
||||
stdin,
|
||||
tokens,
|
||||
&noqa_line_for,
|
||||
settings,
|
||||
&fixer::Mode::None,
|
||||
)?;
|
||||
let mut checks = check_path(path, stdin, tokens, &noqa_line_for, settings, autofix)?;
|
||||
|
||||
// Apply autofix, write results to stdout.
|
||||
if matches!(autofix, fixer::Mode::Apply) {
|
||||
let output = match fix_file(&mut checks, stdin) {
|
||||
None => stdin.to_string(),
|
||||
Some(content) => content,
|
||||
};
|
||||
io::stdout().write_all(output.as_bytes())?;
|
||||
}
|
||||
|
||||
// Convert to messages.
|
||||
Ok(checks
|
||||
@@ -141,7 +150,9 @@ pub fn lint_path(
|
||||
|
||||
// Apply autofix.
|
||||
if matches!(autofix, fixer::Mode::Apply) {
|
||||
fix_file(&mut checks, &contents, path)?;
|
||||
if let Some(fixed_contents) = fix_file(&mut checks, &contents) {
|
||||
write(path, fixed_contents)?;
|
||||
}
|
||||
};
|
||||
|
||||
// Convert to messages.
|
||||
@@ -1284,6 +1295,138 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn d404() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/D.py"),
|
||||
&settings::Settings::for_rule(CheckCode::D404),
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn d405() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/sections.py"),
|
||||
&settings::Settings::for_rule(CheckCode::D405),
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn d406() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/sections.py"),
|
||||
&settings::Settings::for_rule(CheckCode::D406),
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn d407() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/sections.py"),
|
||||
&settings::Settings::for_rule(CheckCode::D407),
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn d408() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/sections.py"),
|
||||
&settings::Settings::for_rule(CheckCode::D408),
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn d409() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/sections.py"),
|
||||
&settings::Settings::for_rule(CheckCode::D409),
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn d410() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/sections.py"),
|
||||
&settings::Settings::for_rule(CheckCode::D410),
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn d411() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/sections.py"),
|
||||
&settings::Settings::for_rule(CheckCode::D411),
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn d412() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/sections.py"),
|
||||
&settings::Settings::for_rule(CheckCode::D412),
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn d413() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/sections.py"),
|
||||
&settings::Settings::for_rule(CheckCode::D413),
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn d414() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/sections.py"),
|
||||
&settings::Settings::for_rule(CheckCode::D414),
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn d415() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
@@ -1296,6 +1439,18 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn d418() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/D.py"),
|
||||
&settings::Settings::for_rule(CheckCode::D418),
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn d419() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
|
||||
22
src/main.rs
22
src/main.rs
@@ -81,9 +81,9 @@ fn read_from_stdin() -> Result<String> {
|
||||
Ok(buffer)
|
||||
}
|
||||
|
||||
fn run_once_stdin(settings: &Settings, filename: &Path) -> Result<Vec<Message>> {
|
||||
fn run_once_stdin(settings: &Settings, filename: &Path, autofix: bool) -> Result<Vec<Message>> {
|
||||
let stdin = read_from_stdin()?;
|
||||
let mut messages = lint_stdin(filename, &stdin, settings)?;
|
||||
let mut messages = lint_stdin(filename, &stdin, settings, &autofix.into())?;
|
||||
messages.sort_unstable();
|
||||
Ok(messages)
|
||||
}
|
||||
@@ -365,18 +365,20 @@ fn inner_main() -> Result<ExitCode> {
|
||||
println!("Formatted {modifications} files.");
|
||||
}
|
||||
} else {
|
||||
let messages = if cli.files == vec![PathBuf::from("-")] {
|
||||
if cli.fix {
|
||||
eprintln!("Warning: --fix is not enabled when reading from stdin.");
|
||||
}
|
||||
|
||||
let (messages, print_messages) = if cli.files == vec![PathBuf::from("-")] {
|
||||
let filename = cli.stdin_filename.unwrap_or_else(|| "-".to_string());
|
||||
let path = Path::new(&filename);
|
||||
run_once_stdin(&settings, path)?
|
||||
(
|
||||
run_once_stdin(&settings, path, cli.fix)?,
|
||||
!cli.quiet && !cli.fix,
|
||||
)
|
||||
} else {
|
||||
run_once(&cli.files, &settings, !cli.no_cache, cli.fix)?
|
||||
(
|
||||
run_once(&cli.files, &settings, !cli.no_cache, cli.fix)?,
|
||||
!cli.quiet,
|
||||
)
|
||||
};
|
||||
if !cli.quiet {
|
||||
if print_messages {
|
||||
printer.write_once(&messages)?;
|
||||
}
|
||||
|
||||
|
||||
6
src/snapshots/ruff__linter__tests__d404.snap
Normal file
6
src/snapshots/ruff__linter__tests__d404.snap
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
source: src/linter.rs
|
||||
expression: checks
|
||||
---
|
||||
[]
|
||||
|
||||
23
src/snapshots/ruff__linter__tests__d405.snap
Normal file
23
src/snapshots/ruff__linter__tests__d405.snap
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
source: src/linter.rs
|
||||
expression: checks
|
||||
---
|
||||
- kind:
|
||||
CapitalizeSectionName: returns
|
||||
location:
|
||||
row: 17
|
||||
column: 5
|
||||
end_location:
|
||||
row: 23
|
||||
column: 8
|
||||
fix: ~
|
||||
- kind:
|
||||
CapitalizeSectionName: Short summary
|
||||
location:
|
||||
row: 207
|
||||
column: 5
|
||||
end_location:
|
||||
row: 221
|
||||
column: 8
|
||||
fix: ~
|
||||
|
||||
41
src/snapshots/ruff__linter__tests__d406.snap
Normal file
41
src/snapshots/ruff__linter__tests__d406.snap
Normal file
@@ -0,0 +1,41 @@
|
||||
---
|
||||
source: src/linter.rs
|
||||
expression: checks
|
||||
---
|
||||
- kind:
|
||||
NewLineAfterSectionName: Returns
|
||||
location:
|
||||
row: 30
|
||||
column: 5
|
||||
end_location:
|
||||
row: 36
|
||||
column: 8
|
||||
fix: ~
|
||||
- kind:
|
||||
NewLineAfterSectionName: Raises
|
||||
location:
|
||||
row: 207
|
||||
column: 5
|
||||
end_location:
|
||||
row: 221
|
||||
column: 8
|
||||
fix: ~
|
||||
- kind:
|
||||
NewLineAfterSectionName: Returns
|
||||
location:
|
||||
row: 252
|
||||
column: 5
|
||||
end_location:
|
||||
row: 262
|
||||
column: 8
|
||||
fix: ~
|
||||
- kind:
|
||||
NewLineAfterSectionName: Raises
|
||||
location:
|
||||
row: 252
|
||||
column: 5
|
||||
end_location:
|
||||
row: 262
|
||||
column: 8
|
||||
fix: ~
|
||||
|
||||
50
src/snapshots/ruff__linter__tests__d407.snap
Normal file
50
src/snapshots/ruff__linter__tests__d407.snap
Normal file
@@ -0,0 +1,50 @@
|
||||
---
|
||||
source: src/linter.rs
|
||||
expression: checks
|
||||
---
|
||||
- kind:
|
||||
DashedUnderlineAfterSection: Returns
|
||||
location:
|
||||
row: 42
|
||||
column: 5
|
||||
end_location:
|
||||
row: 47
|
||||
column: 8
|
||||
fix: ~
|
||||
- kind:
|
||||
DashedUnderlineAfterSection: Returns
|
||||
location:
|
||||
row: 54
|
||||
column: 5
|
||||
end_location:
|
||||
row: 58
|
||||
column: 8
|
||||
fix: ~
|
||||
- kind:
|
||||
DashedUnderlineAfterSection: Raises
|
||||
location:
|
||||
row: 207
|
||||
column: 5
|
||||
end_location:
|
||||
row: 221
|
||||
column: 8
|
||||
fix: ~
|
||||
- kind:
|
||||
DashedUnderlineAfterSection: Returns
|
||||
location:
|
||||
row: 252
|
||||
column: 5
|
||||
end_location:
|
||||
row: 262
|
||||
column: 8
|
||||
fix: ~
|
||||
- kind:
|
||||
DashedUnderlineAfterSection: Raises
|
||||
location:
|
||||
row: 252
|
||||
column: 5
|
||||
end_location:
|
||||
row: 262
|
||||
column: 8
|
||||
fix: ~
|
||||
|
||||
14
src/snapshots/ruff__linter__tests__d408.snap
Normal file
14
src/snapshots/ruff__linter__tests__d408.snap
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
source: src/linter.rs
|
||||
expression: checks
|
||||
---
|
||||
- kind:
|
||||
SectionUnderlineAfterName: Returns
|
||||
location:
|
||||
row: 85
|
||||
column: 5
|
||||
end_location:
|
||||
row: 92
|
||||
column: 8
|
||||
fix: ~
|
||||
|
||||
23
src/snapshots/ruff__linter__tests__d409.snap
Normal file
23
src/snapshots/ruff__linter__tests__d409.snap
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
source: src/linter.rs
|
||||
expression: checks
|
||||
---
|
||||
- kind:
|
||||
SectionUnderlineMatchesSectionLength: Returns
|
||||
location:
|
||||
row: 99
|
||||
column: 5
|
||||
end_location:
|
||||
row: 105
|
||||
column: 8
|
||||
fix: ~
|
||||
- kind:
|
||||
SectionUnderlineMatchesSectionLength: Returns
|
||||
location:
|
||||
row: 207
|
||||
column: 5
|
||||
end_location:
|
||||
row: 221
|
||||
column: 8
|
||||
fix: ~
|
||||
|
||||
23
src/snapshots/ruff__linter__tests__d410.snap
Normal file
23
src/snapshots/ruff__linter__tests__d410.snap
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
source: src/linter.rs
|
||||
expression: checks
|
||||
---
|
||||
- kind:
|
||||
BlankLineAfterSection: Returns
|
||||
location:
|
||||
row: 67
|
||||
column: 5
|
||||
end_location:
|
||||
row: 78
|
||||
column: 8
|
||||
fix: ~
|
||||
- kind:
|
||||
BlankLineAfterSection: Returns
|
||||
location:
|
||||
row: 207
|
||||
column: 5
|
||||
end_location:
|
||||
row: 221
|
||||
column: 8
|
||||
fix: ~
|
||||
|
||||
32
src/snapshots/ruff__linter__tests__d411.snap
Normal file
32
src/snapshots/ruff__linter__tests__d411.snap
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
source: src/linter.rs
|
||||
expression: checks
|
||||
---
|
||||
- kind:
|
||||
BlankLineBeforeSection: Yields
|
||||
location:
|
||||
row: 67
|
||||
column: 5
|
||||
end_location:
|
||||
row: 78
|
||||
column: 8
|
||||
fix: ~
|
||||
- kind:
|
||||
BlankLineBeforeSection: Returns
|
||||
location:
|
||||
row: 122
|
||||
column: 5
|
||||
end_location:
|
||||
row: 129
|
||||
column: 8
|
||||
fix: ~
|
||||
- kind:
|
||||
BlankLineBeforeSection: Raises
|
||||
location:
|
||||
row: 207
|
||||
column: 5
|
||||
end_location:
|
||||
row: 221
|
||||
column: 8
|
||||
fix: ~
|
||||
|
||||
14
src/snapshots/ruff__linter__tests__d412.snap
Normal file
14
src/snapshots/ruff__linter__tests__d412.snap
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
source: src/linter.rs
|
||||
expression: checks
|
||||
---
|
||||
- kind:
|
||||
NoBlankLinesBetweenHeaderAndContent: Short summary
|
||||
location:
|
||||
row: 207
|
||||
column: 5
|
||||
end_location:
|
||||
row: 221
|
||||
column: 8
|
||||
fix: ~
|
||||
|
||||
6
src/snapshots/ruff__linter__tests__d413.snap
Normal file
6
src/snapshots/ruff__linter__tests__d413.snap
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
source: src/linter.rs
|
||||
expression: checks
|
||||
---
|
||||
[]
|
||||
|
||||
50
src/snapshots/ruff__linter__tests__d414.snap
Normal file
50
src/snapshots/ruff__linter__tests__d414.snap
Normal file
@@ -0,0 +1,50 @@
|
||||
---
|
||||
source: src/linter.rs
|
||||
expression: checks
|
||||
---
|
||||
- kind:
|
||||
NonEmptySection: Returns
|
||||
location:
|
||||
row: 54
|
||||
column: 5
|
||||
end_location:
|
||||
row: 58
|
||||
column: 8
|
||||
fix: ~
|
||||
- kind:
|
||||
NonEmptySection: Returns
|
||||
location:
|
||||
row: 67
|
||||
column: 5
|
||||
end_location:
|
||||
row: 78
|
||||
column: 8
|
||||
fix: ~
|
||||
- kind:
|
||||
NonEmptySection: Yields
|
||||
location:
|
||||
row: 67
|
||||
column: 5
|
||||
end_location:
|
||||
row: 78
|
||||
column: 8
|
||||
fix: ~
|
||||
- kind:
|
||||
NonEmptySection: Returns
|
||||
location:
|
||||
row: 161
|
||||
column: 5
|
||||
end_location:
|
||||
row: 165
|
||||
column: 8
|
||||
fix: ~
|
||||
- kind:
|
||||
NonEmptySection: Returns
|
||||
location:
|
||||
row: 252
|
||||
column: 5
|
||||
end_location:
|
||||
row: 262
|
||||
column: 8
|
||||
fix: ~
|
||||
|
||||
29
src/snapshots/ruff__linter__tests__d418.snap
Normal file
29
src/snapshots/ruff__linter__tests__d418.snap
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
source: src/linter.rs
|
||||
expression: checks
|
||||
---
|
||||
- kind: SkipDocstring
|
||||
location:
|
||||
row: 33
|
||||
column: 5
|
||||
end_location:
|
||||
row: 37
|
||||
column: 5
|
||||
fix: ~
|
||||
- kind: SkipDocstring
|
||||
location:
|
||||
row: 85
|
||||
column: 5
|
||||
end_location:
|
||||
row: 89
|
||||
column: 5
|
||||
fix: ~
|
||||
- kind: SkipDocstring
|
||||
location:
|
||||
row: 105
|
||||
column: 1
|
||||
end_location:
|
||||
row: 110
|
||||
column: 1
|
||||
fix: ~
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
//! Abstractions for tracking public and private visibility across modules, classes, and functions.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use crate::ast::helpers::match_name_or_attr;
|
||||
use rustpython_ast::{Stmt, StmtKind};
|
||||
|
||||
use crate::docstrings::Documentable;
|
||||
use crate::ast::helpers::match_name_or_attr;
|
||||
use crate::docstrings::types::Documentable;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Modifier {
|
||||
@@ -24,6 +26,7 @@ pub struct VisibleScope {
|
||||
pub visibility: Visibility,
|
||||
}
|
||||
|
||||
/// Returns `true` if a function definition is an `@overload`.
|
||||
pub fn is_overload(stmt: &Stmt) -> bool {
|
||||
match &stmt.node {
|
||||
StmtKind::FunctionDef { decorator_list, .. }
|
||||
@@ -34,6 +37,7 @@ pub fn is_overload(stmt: &Stmt) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if a function is a "magic method".
|
||||
pub fn is_magic(stmt: &Stmt) -> bool {
|
||||
match &stmt.node {
|
||||
StmtKind::FunctionDef { name, .. } | StmtKind::AsyncFunctionDef { name, .. } => {
|
||||
@@ -47,6 +51,7 @@ pub fn is_magic(stmt: &Stmt) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if a function is an `__init__`.
|
||||
pub fn is_init(stmt: &Stmt) -> bool {
|
||||
match &stmt.node {
|
||||
StmtKind::FunctionDef { name, .. } | StmtKind::AsyncFunctionDef { name, .. } => {
|
||||
@@ -56,13 +61,14 @@ pub fn is_init(stmt: &Stmt) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
fn is_private_name(module_name: &str) -> bool {
|
||||
/// Returns `true` if a module name indicates private visibility.
|
||||
fn is_private_module(module_name: &str) -> bool {
|
||||
module_name.starts_with('_') || (module_name.starts_with("__") && module_name.ends_with("__"))
|
||||
}
|
||||
|
||||
pub fn module_visibility(path: &Path) -> Visibility {
|
||||
for component in path.iter().rev() {
|
||||
if is_private_name(&component.to_string_lossy()) {
|
||||
if is_private_module(&component.to_string_lossy()) {
|
||||
return Visibility::Private;
|
||||
}
|
||||
}
|
||||
@@ -115,6 +121,9 @@ fn class_visibility(stmt: &Stmt) -> Visibility {
|
||||
}
|
||||
|
||||
/// Transition a `VisibleScope` based on a new `Documentable` definition.
|
||||
///
|
||||
/// `scope` is the current `VisibleScope`, while `Documentable` and `Stmt` describe the current
|
||||
/// node used to modify visibility.
|
||||
pub fn transition_scope(scope: &VisibleScope, stmt: &Stmt, kind: &Documentable) -> VisibleScope {
|
||||
match kind {
|
||||
Documentable::Function => VisibleScope {
|
||||
|
||||
@@ -39,9 +39,42 @@ fn test_stdin_autofix() -> Result<()> {
|
||||
let mut cmd = Command::cargo_bin(crate_name!())?;
|
||||
let output = cmd
|
||||
.args(&["-", "--fix"])
|
||||
.write_stdin("import os\n")
|
||||
.write_stdin("import os\nimport sys\n\nprint(sys.version)\n")
|
||||
.assert()
|
||||
.failure();
|
||||
assert!(str::from_utf8(&output.get_output().stdout)?.contains("-:1:1: F401"));
|
||||
.success();
|
||||
assert_eq!(
|
||||
str::from_utf8(&output.get_output().stdout)?,
|
||||
"import sys\n\nprint(sys.version)\n"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stdin_autofix_when_not_fixable_should_still_print_contents() -> Result<()> {
|
||||
let mut cmd = Command::cargo_bin(crate_name!())?;
|
||||
let output = cmd
|
||||
.args(&["-", "--fix"])
|
||||
.write_stdin("import os\nimport sys\n\nif (1, 2):\n print(sys.version)\n")
|
||||
.assert()
|
||||
.failure();
|
||||
assert_eq!(
|
||||
str::from_utf8(&output.get_output().stdout)?,
|
||||
"import sys\n\nif (1, 2):\n print(sys.version)\n"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stdin_autofix_when_no_issues_should_still_print_contents() -> Result<()> {
|
||||
let mut cmd = Command::cargo_bin(crate_name!())?;
|
||||
let output = cmd
|
||||
.args(&["-", "--fix"])
|
||||
.write_stdin("import sys\n\nprint(sys.version)\n")
|
||||
.assert()
|
||||
.success();
|
||||
assert_eq!(
|
||||
str::from_utf8(&output.get_output().stdout)?,
|
||||
"import sys\n\nprint(sys.version)\n"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user