mirror of
https://github.com/astral-sh/ruff.git
synced 2025-07-07 21:25:08 +00:00
[pydoclint
] Implement docstring-missing-exception
and docstring-extraneous-exception
(DOC501
, DOC502
) (#11471)
## Summary These are the first rules implemented as part of #458, but I plan to implement more. Specifically, this implements `docstring-missing-exception` which checks for raised exceptions not documented in the docstring, and `docstring-extraneous-exception` which checks for exceptions in the docstring not present in the body. ## Test Plan Test fixtures added for both google and numpy style.
This commit is contained in:
parent
53b84ab054
commit
4bc73dd87e
21 changed files with 1161 additions and 67 deletions
25
LICENSE
25
LICENSE
|
@ -1371,3 +1371,28 @@ are:
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
SOFTWARE.
|
SOFTWARE.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
- pydoclint, licensed as follows:
|
||||||
|
"""
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2023 jsh9
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
"""
|
||||||
|
|
192
crates/ruff_linter/resources/test/fixtures/pydoclint/DOC501_google.py
vendored
Normal file
192
crates/ruff_linter/resources/test/fixtures/pydoclint/DOC501_google.py
vendored
Normal file
|
@ -0,0 +1,192 @@
|
||||||
|
import something
|
||||||
|
from somewhere import AnotherError
|
||||||
|
|
||||||
|
|
||||||
|
class FasterThanLightError(Exception):
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
_some_error = Exception
|
||||||
|
|
||||||
|
|
||||||
|
# OK
|
||||||
|
def calculate_speed(distance: float, time: float) -> float:
|
||||||
|
"""Calculate speed as distance divided by time.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
distance: Distance traveled.
|
||||||
|
time: Time spent traveling.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Speed as distance divided by time.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
FasterThanLightError: If speed is greater than the speed of light.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return distance / time
|
||||||
|
except ZeroDivisionError as exc:
|
||||||
|
raise FasterThanLightError from exc
|
||||||
|
|
||||||
|
|
||||||
|
# DOC501
|
||||||
|
def calculate_speed(distance: float, time: float) -> float:
|
||||||
|
"""Calculate speed as distance divided by time.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
distance: Distance traveled.
|
||||||
|
time: Time spent traveling.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Speed as distance divided by time.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return distance / time
|
||||||
|
except ZeroDivisionError as exc:
|
||||||
|
raise FasterThanLightError from exc
|
||||||
|
|
||||||
|
|
||||||
|
# DOC501
|
||||||
|
def calculate_speed(distance: float, time: float) -> float:
|
||||||
|
"""Calculate speed as distance divided by time.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
distance: Distance traveled.
|
||||||
|
time: Time spent traveling.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Speed as distance divided by time.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return distance / time
|
||||||
|
except ZeroDivisionError as exc:
|
||||||
|
raise FasterThanLightError from exc
|
||||||
|
except:
|
||||||
|
raise ValueError
|
||||||
|
|
||||||
|
|
||||||
|
# DOC501
|
||||||
|
def calculate_speed(distance: float, time: float) -> float:
|
||||||
|
"""Calculate speed as distance divided by time.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
distance: Distance traveled.
|
||||||
|
time: Time spent traveling.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Speed as distance divided by time.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return distance / time
|
||||||
|
except ZeroDivisionError as exc:
|
||||||
|
print('oops')
|
||||||
|
raise exc
|
||||||
|
|
||||||
|
|
||||||
|
# DOC501
|
||||||
|
def calculate_speed(distance: float, time: float) -> float:
|
||||||
|
"""Calculate speed as distance divided by time.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
distance: Distance traveled.
|
||||||
|
time: Time spent traveling.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Speed as distance divided by time.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return distance / time
|
||||||
|
except (ZeroDivisionError, ValueError) as exc:
|
||||||
|
print('oops')
|
||||||
|
raise exc
|
||||||
|
|
||||||
|
|
||||||
|
# DOC501
|
||||||
|
def calculate_speed(distance: float, time: float) -> float:
|
||||||
|
"""Calculate speed as distance divided by time.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
distance: Distance traveled.
|
||||||
|
time: Time spent traveling.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Speed as distance divided by time.
|
||||||
|
"""
|
||||||
|
raise AnotherError
|
||||||
|
|
||||||
|
|
||||||
|
# DOC501
|
||||||
|
def calculate_speed(distance: float, time: float) -> float:
|
||||||
|
"""Calculate speed as distance divided by time.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
distance: Distance traveled.
|
||||||
|
time: Time spent traveling.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Speed as distance divided by time.
|
||||||
|
"""
|
||||||
|
raise AnotherError()
|
||||||
|
|
||||||
|
|
||||||
|
# DOC501
|
||||||
|
def foo(bar: int):
|
||||||
|
"""Foo.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bar: Bar.
|
||||||
|
"""
|
||||||
|
raise something.SomeError
|
||||||
|
|
||||||
|
|
||||||
|
# DOC501, but can't resolve the error
|
||||||
|
def calculate_speed(distance: float, time: float) -> float:
|
||||||
|
"""Calculate speed as distance divided by time.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
distance: Distance traveled.
|
||||||
|
time: Time spent traveling.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Speed as distance divided by time.
|
||||||
|
"""
|
||||||
|
raise _some_error
|
||||||
|
|
||||||
|
|
||||||
|
# OK
|
||||||
|
def calculate_speed(distance: float, time: float) -> float:
|
||||||
|
try:
|
||||||
|
return distance / time
|
||||||
|
except ZeroDivisionError as exc:
|
||||||
|
raise FasterThanLightError from exc
|
||||||
|
|
||||||
|
|
||||||
|
# OK
|
||||||
|
def calculate_speed(distance: float, time: float) -> float:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
# OK
|
||||||
|
def foo(bar: int):
|
||||||
|
"""Foo.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bar: Bar.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
SomeError: Wow.
|
||||||
|
"""
|
||||||
|
raise something.SomeError
|
||||||
|
|
||||||
|
|
||||||
|
# OK
|
||||||
|
def foo(bar: int):
|
||||||
|
"""Foo.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bar: Bar.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
something.SomeError: Wow.
|
||||||
|
"""
|
||||||
|
raise something.SomeError
|
78
crates/ruff_linter/resources/test/fixtures/pydoclint/DOC501_numpy.py
vendored
Normal file
78
crates/ruff_linter/resources/test/fixtures/pydoclint/DOC501_numpy.py
vendored
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
class FasterThanLightError(Exception):
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
# OK
|
||||||
|
def calculate_speed(distance: float, time: float) -> float:
|
||||||
|
"""
|
||||||
|
Calculate speed as distance divided by time.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
distance : float
|
||||||
|
Distance traveled.
|
||||||
|
time : float
|
||||||
|
Time spent traveling.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
float
|
||||||
|
Speed as distance divided by time.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
FasterThanLightError
|
||||||
|
If speed is greater than the speed of light.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return distance / time
|
||||||
|
except ZeroDivisionError as exc:
|
||||||
|
raise FasterThanLightError from exc
|
||||||
|
|
||||||
|
|
||||||
|
# DOC501
|
||||||
|
def calculate_speed(distance: float, time: float) -> float:
|
||||||
|
"""
|
||||||
|
Calculate speed as distance divided by time.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
distance : float
|
||||||
|
Distance traveled.
|
||||||
|
time : float
|
||||||
|
Time spent traveling.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
float
|
||||||
|
Speed as distance divided by time.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return distance / time
|
||||||
|
except ZeroDivisionError as exc:
|
||||||
|
raise FasterThanLightError from exc
|
||||||
|
|
||||||
|
|
||||||
|
# DOC501
|
||||||
|
def calculate_speed(distance: float, time: float) -> float:
|
||||||
|
"""
|
||||||
|
Calculate speed as distance divided by time.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
distance : float
|
||||||
|
Distance traveled.
|
||||||
|
time : float
|
||||||
|
Time spent traveling.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
float
|
||||||
|
Speed as distance divided by time.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return distance / time
|
||||||
|
except ZeroDivisionError as exc:
|
||||||
|
raise FasterThanLightError from exc
|
||||||
|
except:
|
||||||
|
raise ValueError
|
58
crates/ruff_linter/resources/test/fixtures/pydoclint/DOC502_google.py
vendored
Normal file
58
crates/ruff_linter/resources/test/fixtures/pydoclint/DOC502_google.py
vendored
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
class FasterThanLightError(Exception):
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
# DOC502
|
||||||
|
def calculate_speed(distance: float, time: float) -> float:
|
||||||
|
"""Calculate speed as distance divided by time.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
distance: Distance traveled.
|
||||||
|
time: Time spent traveling.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Speed as distance divided by time.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
FasterThanLightError: If speed is greater than the speed of light.
|
||||||
|
"""
|
||||||
|
return distance / time
|
||||||
|
|
||||||
|
|
||||||
|
# DOC502
|
||||||
|
def calculate_speed(distance: float, time: float) -> float:
|
||||||
|
"""Calculate speed as distance divided by time.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
distance: Distance traveled.
|
||||||
|
time: Time spent traveling.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Speed as distance divided by time.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
FasterThanLightError: If speed is greater than the speed of light.
|
||||||
|
DivisionByZero: Divide by zero.
|
||||||
|
"""
|
||||||
|
return distance / time
|
||||||
|
|
||||||
|
|
||||||
|
# DOC502
|
||||||
|
def calculate_speed(distance: float, time: float) -> float:
|
||||||
|
"""Calculate speed as distance divided by time.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
distance: Distance traveled.
|
||||||
|
time: Time spent traveling.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Speed as distance divided by time.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
FasterThanLightError: If speed is greater than the speed of light.
|
||||||
|
DivisionByZero: Divide by zero.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return distance / time
|
||||||
|
except ZeroDivisionError as exc:
|
||||||
|
raise FasterThanLightError from exc
|
84
crates/ruff_linter/resources/test/fixtures/pydoclint/DOC502_numpy.py
vendored
Normal file
84
crates/ruff_linter/resources/test/fixtures/pydoclint/DOC502_numpy.py
vendored
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
class FasterThanLightError(Exception):
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
# DOC502
|
||||||
|
def calculate_speed(distance: float, time: float) -> float:
|
||||||
|
"""
|
||||||
|
Calculate speed as distance divided by time.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
distance : float
|
||||||
|
Distance traveled.
|
||||||
|
time : float
|
||||||
|
Time spent traveling.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
float
|
||||||
|
Speed as distance divided by time.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
FasterThanLightError
|
||||||
|
If speed is greater than the speed of light.
|
||||||
|
"""
|
||||||
|
return distance / time
|
||||||
|
|
||||||
|
|
||||||
|
# DOC502
|
||||||
|
def calculate_speed(distance: float, time: float) -> float:
|
||||||
|
"""
|
||||||
|
Calculate speed as distance divided by time.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
distance : float
|
||||||
|
Distance traveled.
|
||||||
|
time : float
|
||||||
|
Time spent traveling.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
float
|
||||||
|
Speed as distance divided by time.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
FasterThanLightError
|
||||||
|
If speed is greater than the speed of light.
|
||||||
|
DivisionByZero
|
||||||
|
If attempting to divide by zero.
|
||||||
|
"""
|
||||||
|
return distance / time
|
||||||
|
|
||||||
|
|
||||||
|
# DOC502
|
||||||
|
def calculate_speed(distance: float, time: float) -> float:
|
||||||
|
"""
|
||||||
|
Calculate speed as distance divided by time.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
distance : float
|
||||||
|
Distance traveled.
|
||||||
|
time : float
|
||||||
|
Time spent traveling.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
float
|
||||||
|
Speed as distance divided by time.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
FasterThanLightError
|
||||||
|
If speed is greater than the speed of light.
|
||||||
|
DivisionByZero
|
||||||
|
If attempting to divide by zero.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return distance / time
|
||||||
|
except ZeroDivisionError as exc:
|
||||||
|
raise FasterThanLightError from exc
|
|
@ -10,7 +10,7 @@ use crate::checkers::ast::Checker;
|
||||||
use crate::codes::Rule;
|
use crate::codes::Rule;
|
||||||
use crate::docstrings::Docstring;
|
use crate::docstrings::Docstring;
|
||||||
use crate::fs::relativize_path;
|
use crate::fs::relativize_path;
|
||||||
use crate::rules::{flake8_annotations, flake8_pyi, pydocstyle, pylint};
|
use crate::rules::{flake8_annotations, flake8_pyi, pydoclint, pydocstyle, pylint};
|
||||||
use crate::{docstrings, warn_user};
|
use crate::{docstrings, warn_user};
|
||||||
|
|
||||||
/// Run lint rules over all [`Definition`] nodes in the [`SemanticModel`].
|
/// Run lint rules over all [`Definition`] nodes in the [`SemanticModel`].
|
||||||
|
@ -83,12 +83,17 @@ pub(crate) fn definitions(checker: &mut Checker) {
|
||||||
Rule::UndocumentedPublicNestedClass,
|
Rule::UndocumentedPublicNestedClass,
|
||||||
Rule::UndocumentedPublicPackage,
|
Rule::UndocumentedPublicPackage,
|
||||||
]);
|
]);
|
||||||
|
let enforce_pydoclint = checker.any_enabled(&[
|
||||||
|
Rule::DocstringMissingException,
|
||||||
|
Rule::DocstringExtraneousException,
|
||||||
|
]);
|
||||||
|
|
||||||
if !enforce_annotations
|
if !enforce_annotations
|
||||||
&& !enforce_docstrings
|
&& !enforce_docstrings
|
||||||
&& !enforce_stubs
|
&& !enforce_stubs
|
||||||
&& !enforce_stubs_and_runtime
|
&& !enforce_stubs_and_runtime
|
||||||
&& !enforce_dunder_method
|
&& !enforce_dunder_method
|
||||||
|
&& !enforce_pydoclint
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -163,8 +168,8 @@ pub(crate) fn definitions(checker: &mut Checker) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// pydocstyle
|
// pydocstyle, pydoclint
|
||||||
if enforce_docstrings {
|
if enforce_docstrings || enforce_pydoclint {
|
||||||
if pydocstyle::helpers::should_ignore_definition(
|
if pydocstyle::helpers::should_ignore_definition(
|
||||||
definition,
|
definition,
|
||||||
&checker.settings.pydocstyle.ignore_decorators,
|
&checker.settings.pydocstyle.ignore_decorators,
|
||||||
|
@ -282,7 +287,8 @@ pub(crate) fn definitions(checker: &mut Checker) {
|
||||||
if checker.enabled(Rule::OverloadWithDocstring) {
|
if checker.enabled(Rule::OverloadWithDocstring) {
|
||||||
pydocstyle::rules::if_needed(checker, &docstring);
|
pydocstyle::rules::if_needed(checker, &docstring);
|
||||||
}
|
}
|
||||||
if checker.any_enabled(&[
|
|
||||||
|
let enforce_sections = checker.any_enabled(&[
|
||||||
Rule::BlankLineAfterLastSection,
|
Rule::BlankLineAfterLastSection,
|
||||||
Rule::BlankLinesBetweenHeaderAndContent,
|
Rule::BlankLinesBetweenHeaderAndContent,
|
||||||
Rule::CapitalizeSectionName,
|
Rule::CapitalizeSectionName,
|
||||||
|
@ -298,12 +304,30 @@ pub(crate) fn definitions(checker: &mut Checker) {
|
||||||
Rule::SectionUnderlineMatchesSectionLength,
|
Rule::SectionUnderlineMatchesSectionLength,
|
||||||
Rule::SectionUnderlineNotOverIndented,
|
Rule::SectionUnderlineNotOverIndented,
|
||||||
Rule::UndocumentedParam,
|
Rule::UndocumentedParam,
|
||||||
]) {
|
]);
|
||||||
pydocstyle::rules::sections(
|
if enforce_sections || enforce_pydoclint {
|
||||||
checker,
|
let section_contexts = pydocstyle::helpers::get_section_contexts(
|
||||||
&docstring,
|
&docstring,
|
||||||
checker.settings.pydocstyle.convention.as_ref(),
|
checker.settings.pydocstyle.convention.as_ref(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if enforce_sections {
|
||||||
|
pydocstyle::rules::sections(
|
||||||
|
checker,
|
||||||
|
&docstring,
|
||||||
|
§ion_contexts,
|
||||||
|
checker.settings.pydocstyle.convention.as_ref(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if enforce_pydoclint {
|
||||||
|
pydoclint::rules::check_docstring(
|
||||||
|
checker,
|
||||||
|
definition,
|
||||||
|
§ion_contexts,
|
||||||
|
checker.settings.pydocstyle.convention.as_ref(),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -912,6 +912,10 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||||
(Numpy, "003") => (RuleGroup::Stable, rules::numpy::rules::NumpyDeprecatedFunction),
|
(Numpy, "003") => (RuleGroup::Stable, rules::numpy::rules::NumpyDeprecatedFunction),
|
||||||
(Numpy, "201") => (RuleGroup::Stable, rules::numpy::rules::Numpy2Deprecation),
|
(Numpy, "201") => (RuleGroup::Stable, rules::numpy::rules::Numpy2Deprecation),
|
||||||
|
|
||||||
|
// pydoclint
|
||||||
|
(Pydoclint, "501") => (RuleGroup::Preview, rules::pydoclint::rules::DocstringMissingException),
|
||||||
|
(Pydoclint, "502") => (RuleGroup::Preview, rules::pydoclint::rules::DocstringExtraneousException),
|
||||||
|
|
||||||
// ruff
|
// ruff
|
||||||
(Ruff, "001") => (RuleGroup::Stable, rules::ruff::rules::AmbiguousUnicodeCharacterString),
|
(Ruff, "001") => (RuleGroup::Stable, rules::ruff::rules::AmbiguousUnicodeCharacterString),
|
||||||
(Ruff, "002") => (RuleGroup::Stable, rules::ruff::rules::AmbiguousUnicodeCharacterDocstring),
|
(Ruff, "002") => (RuleGroup::Stable, rules::ruff::rules::AmbiguousUnicodeCharacterDocstring),
|
||||||
|
|
|
@ -163,6 +163,7 @@ impl SectionKind {
|
||||||
pub(crate) struct SectionContexts<'a> {
|
pub(crate) struct SectionContexts<'a> {
|
||||||
contexts: Vec<SectionContextData>,
|
contexts: Vec<SectionContextData>,
|
||||||
docstring: &'a Docstring<'a>,
|
docstring: &'a Docstring<'a>,
|
||||||
|
style: SectionStyle,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> SectionContexts<'a> {
|
impl<'a> SectionContexts<'a> {
|
||||||
|
@ -221,9 +222,14 @@ impl<'a> SectionContexts<'a> {
|
||||||
Self {
|
Self {
|
||||||
contexts,
|
contexts,
|
||||||
docstring,
|
docstring,
|
||||||
|
style,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn style(&self) -> SectionStyle {
|
||||||
|
self.style
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn len(&self) -> usize {
|
pub(crate) fn len(&self) -> usize {
|
||||||
self.contexts.len()
|
self.contexts.len()
|
||||||
}
|
}
|
||||||
|
@ -396,7 +402,7 @@ impl<'a> SectionContext<'a> {
|
||||||
NewlineWithTrailingNewline::with_offset(lines, self.offset() + self.data.summary_full_end)
|
NewlineWithTrailingNewline::with_offset(lines, self.offset() + self.data.summary_full_end)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn following_lines_str(&self) -> &'a str {
|
pub(crate) fn following_lines_str(&self) -> &'a str {
|
||||||
&self.docstring_body.as_str()[self.following_range_relative()]
|
&self.docstring_body.as_str()[self.following_range_relative()]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -202,6 +202,9 @@ pub enum Linter {
|
||||||
/// [refurb](https://pypi.org/project/refurb/)
|
/// [refurb](https://pypi.org/project/refurb/)
|
||||||
#[prefix = "FURB"]
|
#[prefix = "FURB"]
|
||||||
Refurb,
|
Refurb,
|
||||||
|
/// [pydoclint](https://pypi.org/project/pydoclint/)
|
||||||
|
#[prefix = "DOC"]
|
||||||
|
Pydoclint,
|
||||||
/// Ruff-specific rules
|
/// Ruff-specific rules
|
||||||
#[prefix = "RUF"]
|
#[prefix = "RUF"]
|
||||||
Ruff,
|
Ruff,
|
||||||
|
|
|
@ -48,6 +48,7 @@ pub mod pandas_vet;
|
||||||
pub mod pep8_naming;
|
pub mod pep8_naming;
|
||||||
pub mod perflint;
|
pub mod perflint;
|
||||||
pub mod pycodestyle;
|
pub mod pycodestyle;
|
||||||
|
pub mod pydoclint;
|
||||||
pub mod pydocstyle;
|
pub mod pydocstyle;
|
||||||
pub mod pyflakes;
|
pub mod pyflakes;
|
||||||
pub mod pygrep_hooks;
|
pub mod pygrep_hooks;
|
||||||
|
|
55
crates/ruff_linter/src/rules/pydoclint/mod.rs
Normal file
55
crates/ruff_linter/src/rules/pydoclint/mod.rs
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
//! Rules from [pydoclint](https://pypi.org/project/pydoclint/).
|
||||||
|
pub(crate) mod rules;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::collections::BTreeSet;
|
||||||
|
use std::convert::AsRef;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use test_case::test_case;
|
||||||
|
|
||||||
|
use crate::registry::Rule;
|
||||||
|
use crate::rules::pydocstyle::settings::{Convention, Settings};
|
||||||
|
use crate::test::test_path;
|
||||||
|
use crate::{assert_messages, settings};
|
||||||
|
|
||||||
|
#[test_case(Rule::DocstringMissingException, Path::new("DOC501_google.py"))]
|
||||||
|
#[test_case(Rule::DocstringExtraneousException, Path::new("DOC502_google.py"))]
|
||||||
|
fn rules_google_style(rule_code: Rule, path: &Path) -> Result<()> {
|
||||||
|
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
|
||||||
|
let diagnostics = test_path(
|
||||||
|
Path::new("pydoclint").join(path).as_path(),
|
||||||
|
&settings::LinterSettings {
|
||||||
|
pydocstyle: Settings {
|
||||||
|
convention: Some(Convention::Google),
|
||||||
|
ignore_decorators: BTreeSet::new(),
|
||||||
|
property_decorators: BTreeSet::new(),
|
||||||
|
},
|
||||||
|
..settings::LinterSettings::for_rule(rule_code)
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
assert_messages!(snapshot, diagnostics);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_case(Rule::DocstringMissingException, Path::new("DOC501_numpy.py"))]
|
||||||
|
#[test_case(Rule::DocstringExtraneousException, Path::new("DOC502_numpy.py"))]
|
||||||
|
fn rules_numpy_style(rule_code: Rule, path: &Path) -> Result<()> {
|
||||||
|
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
|
||||||
|
let diagnostics = test_path(
|
||||||
|
Path::new("pydoclint").join(path).as_path(),
|
||||||
|
&settings::LinterSettings {
|
||||||
|
pydocstyle: Settings {
|
||||||
|
convention: Some(Convention::Numpy),
|
||||||
|
ignore_decorators: BTreeSet::new(),
|
||||||
|
property_decorators: BTreeSet::new(),
|
||||||
|
},
|
||||||
|
..settings::LinterSettings::for_rule(rule_code)
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
assert_messages!(snapshot, diagnostics);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
382
crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs
Normal file
382
crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs
Normal file
|
@ -0,0 +1,382 @@
|
||||||
|
use itertools::Itertools;
|
||||||
|
use ruff_diagnostics::Diagnostic;
|
||||||
|
use ruff_diagnostics::Violation;
|
||||||
|
use ruff_macros::{derive_message_formats, violation};
|
||||||
|
use ruff_python_ast::name::QualifiedName;
|
||||||
|
use ruff_python_ast::visitor::{self, Visitor};
|
||||||
|
use ruff_python_ast::{self as ast, Expr, Stmt};
|
||||||
|
use ruff_python_semantic::{Definition, MemberKind, SemanticModel};
|
||||||
|
use ruff_text_size::{Ranged, TextRange};
|
||||||
|
|
||||||
|
use crate::checkers::ast::Checker;
|
||||||
|
use crate::docstrings::sections::{SectionContexts, SectionKind};
|
||||||
|
use crate::docstrings::styles::SectionStyle;
|
||||||
|
use crate::registry::Rule;
|
||||||
|
use crate::rules::pydocstyle::settings::Convention;
|
||||||
|
|
||||||
|
/// ## What it does
|
||||||
|
/// Checks for function docstrings that do not include documentation for all
|
||||||
|
/// explicitly-raised exceptions.
|
||||||
|
///
|
||||||
|
/// ## Why is this bad?
|
||||||
|
/// If a raise is mentioned in a docstring, but the function itself does not
|
||||||
|
/// explicitly raise it, it can be misleading to users and/or a sign of
|
||||||
|
/// incomplete documentation or refactors.
|
||||||
|
///
|
||||||
|
/// ## Example
|
||||||
|
/// ```python
|
||||||
|
/// def calculate_speed(distance: float, time: float) -> float:
|
||||||
|
/// """Calculate speed as distance divided by time.
|
||||||
|
///
|
||||||
|
/// Args:
|
||||||
|
/// distance: Distance traveled.
|
||||||
|
/// time: Time spent traveling.
|
||||||
|
///
|
||||||
|
/// Returns:
|
||||||
|
/// Speed as distance divided by time.
|
||||||
|
/// """
|
||||||
|
/// try:
|
||||||
|
/// return distance / time
|
||||||
|
/// except ZeroDivisionError as exc:
|
||||||
|
/// raise FasterThanLightError from exc
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Use instead:
|
||||||
|
/// ```python
|
||||||
|
/// def calculate_speed(distance: float, time: float) -> float:
|
||||||
|
/// """Calculate speed as distance divided by time.
|
||||||
|
///
|
||||||
|
/// Args:
|
||||||
|
/// distance: Distance traveled.
|
||||||
|
/// time: Time spent traveling.
|
||||||
|
///
|
||||||
|
/// Returns:
|
||||||
|
/// Speed as distance divided by time.
|
||||||
|
///
|
||||||
|
/// Raises:
|
||||||
|
/// FasterThanLightError: If speed is greater than the speed of light.
|
||||||
|
/// """
|
||||||
|
/// try:
|
||||||
|
/// return distance / time
|
||||||
|
/// except ZeroDivisionError as exc:
|
||||||
|
/// raise FasterThanLightError from exc
|
||||||
|
/// ```
|
||||||
|
#[violation]
|
||||||
|
pub struct DocstringMissingException {
|
||||||
|
id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Violation for DocstringMissingException {
|
||||||
|
#[derive_message_formats]
|
||||||
|
fn message(&self) -> String {
|
||||||
|
let DocstringMissingException { id } = self;
|
||||||
|
format!("Raised exception `{id}` missing from docstring")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ## What it does
|
||||||
|
/// Checks for function docstrings that include exceptions which are not
|
||||||
|
/// explicitly raised.
|
||||||
|
///
|
||||||
|
/// ## Why is this bad?
|
||||||
|
/// Some conventions prefer non-explicit exceptions be omitted from the
|
||||||
|
/// docstring.
|
||||||
|
///
|
||||||
|
/// ## Example
|
||||||
|
/// ```python
|
||||||
|
/// def calculate_speed(distance: float, time: float) -> float:
|
||||||
|
/// """Calculate speed as distance divided by time.
|
||||||
|
///
|
||||||
|
/// Args:
|
||||||
|
/// distance: Distance traveled.
|
||||||
|
/// time: Time spent traveling.
|
||||||
|
///
|
||||||
|
/// Returns:
|
||||||
|
/// Speed as distance divided by time.
|
||||||
|
///
|
||||||
|
/// Raises:
|
||||||
|
/// ZeroDivisionError: Divided by zero.
|
||||||
|
/// """
|
||||||
|
/// return distance / time
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Use instead:
|
||||||
|
/// ```python
|
||||||
|
/// def calculate_speed(distance: float, time: float) -> float:
|
||||||
|
/// """Calculate speed as distance divided by time.
|
||||||
|
///
|
||||||
|
/// Args:
|
||||||
|
/// distance: Distance traveled.
|
||||||
|
/// time: Time spent traveling.
|
||||||
|
///
|
||||||
|
/// Returns:
|
||||||
|
/// Speed as distance divided by time.
|
||||||
|
/// """
|
||||||
|
/// return distance / time
|
||||||
|
/// ```
|
||||||
|
#[violation]
|
||||||
|
pub struct DocstringExtraneousException {
|
||||||
|
ids: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Violation for DocstringExtraneousException {
|
||||||
|
#[derive_message_formats]
|
||||||
|
fn message(&self) -> String {
|
||||||
|
let DocstringExtraneousException { ids } = self;
|
||||||
|
|
||||||
|
if let [id] = ids.as_slice() {
|
||||||
|
format!("Raised exception is not explicitly raised: `{id}`")
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"Raised exceptions are not explicitly raised: {}",
|
||||||
|
ids.iter().map(|id| format!("`{id}`")).join(", ")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct DocstringEntries<'a> {
|
||||||
|
raised_exceptions: Vec<QualifiedName<'a>>,
|
||||||
|
raised_exceptions_range: TextRange,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> DocstringEntries<'a> {
|
||||||
|
/// Return the raised exceptions for the docstring, or `None` if the docstring does not contain
|
||||||
|
/// a `Raises` section.
|
||||||
|
fn from_sections(sections: &'a SectionContexts, style: SectionStyle) -> Option<Self> {
|
||||||
|
for section in sections.iter() {
|
||||||
|
if section.kind() == SectionKind::Raises {
|
||||||
|
return Some(Self {
|
||||||
|
raised_exceptions: parse_entries(section.following_lines_str(), style),
|
||||||
|
raised_exceptions_range: section.range(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ranged for DocstringEntries<'_> {
|
||||||
|
fn range(&self) -> TextRange {
|
||||||
|
self.raised_exceptions_range
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse the entries in a `Raises` section of a docstring.
|
||||||
|
fn parse_entries(content: &str, style: SectionStyle) -> Vec<QualifiedName> {
|
||||||
|
match style {
|
||||||
|
SectionStyle::Google => parse_entries_google(content),
|
||||||
|
SectionStyle::Numpy => parse_entries_numpy(content),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses Google-style docstring sections of the form:
|
||||||
|
///
|
||||||
|
/// ```python
|
||||||
|
/// Raises:
|
||||||
|
/// FasterThanLightError: If speed is greater than the speed of light.
|
||||||
|
/// DivisionByZero: If attempting to divide by zero.
|
||||||
|
/// ```
|
||||||
|
fn parse_entries_google(content: &str) -> Vec<QualifiedName> {
|
||||||
|
let mut entries: Vec<QualifiedName> = Vec::new();
|
||||||
|
for potential in content.split('\n') {
|
||||||
|
let Some(colon_idx) = potential.find(':') else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let entry = potential[..colon_idx].trim();
|
||||||
|
entries.push(QualifiedName::user_defined(entry));
|
||||||
|
}
|
||||||
|
entries
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses NumPy-style docstring sections of the form:
|
||||||
|
///
|
||||||
|
/// ```python
|
||||||
|
/// Raises
|
||||||
|
/// ------
|
||||||
|
/// FasterThanLightError
|
||||||
|
/// If speed is greater than the speed of light.
|
||||||
|
/// DivisionByZero
|
||||||
|
/// If attempting to divide by zero.
|
||||||
|
/// ```
|
||||||
|
fn parse_entries_numpy(content: &str) -> Vec<QualifiedName> {
|
||||||
|
let mut entries: Vec<QualifiedName> = Vec::new();
|
||||||
|
let mut split = content.split('\n');
|
||||||
|
let Some(dashes) = split.next() else {
|
||||||
|
return entries;
|
||||||
|
};
|
||||||
|
let indentation = dashes.len() - dashes.trim_start().len();
|
||||||
|
for potential in split {
|
||||||
|
if let Some(first_char) = potential.chars().nth(indentation) {
|
||||||
|
if !first_char.is_whitespace() {
|
||||||
|
let entry = potential[indentation..].trim();
|
||||||
|
entries.push(QualifiedName::user_defined(entry));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
entries
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An individual exception raised in a function body.
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct Entry<'a> {
|
||||||
|
qualified_name: QualifiedName<'a>,
|
||||||
|
range: TextRange,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ranged for Entry<'_> {
|
||||||
|
fn range(&self) -> TextRange {
|
||||||
|
self.range
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The exceptions raised in a function body.
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct BodyEntries<'a> {
|
||||||
|
raised_exceptions: Vec<Entry<'a>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An AST visitor to extract the raised exceptions from a function body.
|
||||||
|
struct BodyVisitor<'a> {
|
||||||
|
raised_exceptions: Vec<Entry<'a>>,
|
||||||
|
semantic: &'a SemanticModel<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> BodyVisitor<'a> {
|
||||||
|
fn new(semantic: &'a SemanticModel) -> Self {
|
||||||
|
Self {
|
||||||
|
raised_exceptions: Vec::new(),
|
||||||
|
semantic,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish(self) -> BodyEntries<'a> {
|
||||||
|
BodyEntries {
|
||||||
|
raised_exceptions: self.raised_exceptions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Visitor<'a> for BodyVisitor<'a> {
|
||||||
|
fn visit_stmt(&mut self, stmt: &'a Stmt) {
|
||||||
|
if let Stmt::Raise(ast::StmtRaise { exc: Some(exc), .. }) = stmt {
|
||||||
|
if let Some(qualified_name) = extract_raised_exception(self.semantic, exc.as_ref()) {
|
||||||
|
self.raised_exceptions.push(Entry {
|
||||||
|
qualified_name,
|
||||||
|
range: exc.as_ref().range(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
visitor::walk_stmt(self, stmt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_raised_exception<'a>(
|
||||||
|
semantic: &SemanticModel<'a>,
|
||||||
|
exc: &'a Expr,
|
||||||
|
) -> Option<QualifiedName<'a>> {
|
||||||
|
if let Some(qualified_name) = semantic.resolve_qualified_name(exc) {
|
||||||
|
return Some(qualified_name);
|
||||||
|
}
|
||||||
|
if let Expr::Call(ast::ExprCall { func, .. }) = exc {
|
||||||
|
return extract_raised_exception(semantic, func.as_ref());
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// DOC501, DOC502
|
||||||
|
pub(crate) fn check_docstring(
|
||||||
|
checker: &mut Checker,
|
||||||
|
definition: &Definition,
|
||||||
|
section_contexts: &SectionContexts,
|
||||||
|
convention: Option<&Convention>,
|
||||||
|
) {
|
||||||
|
let mut diagnostics = Vec::new();
|
||||||
|
let Definition::Member(member) = definition else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only check function docstrings.
|
||||||
|
if matches!(
|
||||||
|
member.kind,
|
||||||
|
MemberKind::Class(_) | MemberKind::NestedClass(_)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prioritize the specified convention over the determined style.
|
||||||
|
let docstring_entries = match convention {
|
||||||
|
Some(Convention::Google) => {
|
||||||
|
DocstringEntries::from_sections(section_contexts, SectionStyle::Google)
|
||||||
|
}
|
||||||
|
Some(Convention::Numpy) => {
|
||||||
|
DocstringEntries::from_sections(section_contexts, SectionStyle::Numpy)
|
||||||
|
}
|
||||||
|
_ => DocstringEntries::from_sections(section_contexts, section_contexts.style()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let body_entries = {
|
||||||
|
let mut visitor = BodyVisitor::new(checker.semantic());
|
||||||
|
visitor::walk_body(&mut visitor, member.body());
|
||||||
|
visitor.finish()
|
||||||
|
};
|
||||||
|
|
||||||
|
// DOC501
|
||||||
|
if checker.enabled(Rule::DocstringMissingException) {
|
||||||
|
for body_raise in &body_entries.raised_exceptions {
|
||||||
|
let Some(name) = body_raise.qualified_name.segments().last() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if *name == "NotImplementedError" {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !docstring_entries.as_ref().is_some_and(|entries| {
|
||||||
|
entries.raised_exceptions.iter().any(|exception| {
|
||||||
|
body_raise
|
||||||
|
.qualified_name
|
||||||
|
.segments()
|
||||||
|
.ends_with(exception.segments())
|
||||||
|
})
|
||||||
|
}) {
|
||||||
|
let diagnostic = Diagnostic::new(
|
||||||
|
DocstringMissingException {
|
||||||
|
id: (*name).to_string(),
|
||||||
|
},
|
||||||
|
body_raise.range(),
|
||||||
|
);
|
||||||
|
diagnostics.push(diagnostic);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DOC502
|
||||||
|
if checker.enabled(Rule::DocstringExtraneousException) {
|
||||||
|
if let Some(docstring_entries) = docstring_entries {
|
||||||
|
let mut extraneous_exceptions = Vec::new();
|
||||||
|
for docstring_raise in &docstring_entries.raised_exceptions {
|
||||||
|
if !body_entries.raised_exceptions.iter().any(|exception| {
|
||||||
|
exception
|
||||||
|
.qualified_name
|
||||||
|
.segments()
|
||||||
|
.ends_with(docstring_raise.segments())
|
||||||
|
}) {
|
||||||
|
extraneous_exceptions.push(docstring_raise.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !extraneous_exceptions.is_empty() {
|
||||||
|
let diagnostic = Diagnostic::new(
|
||||||
|
DocstringExtraneousException {
|
||||||
|
ids: extraneous_exceptions,
|
||||||
|
},
|
||||||
|
docstring_entries.range(),
|
||||||
|
);
|
||||||
|
diagnostics.push(diagnostic);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checker.diagnostics.extend(diagnostics);
|
||||||
|
}
|
3
crates/ruff_linter/src/rules/pydoclint/rules/mod.rs
Normal file
3
crates/ruff_linter/src/rules/pydoclint/rules/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
pub(crate) use check_docstring::*;
|
||||||
|
|
||||||
|
mod check_docstring;
|
|
@ -0,0 +1,38 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff_linter/src/rules/pydoclint/mod.rs
|
||||||
|
---
|
||||||
|
DOC502_google.py:16:1: DOC502 Raised exception is not explicitly raised: `FasterThanLightError`
|
||||||
|
|
|
||||||
|
14 | Speed as distance divided by time.
|
||||||
|
15 |
|
||||||
|
16 | / Raises:
|
||||||
|
17 | | FasterThanLightError: If speed is greater than the speed of light.
|
||||||
|
18 | | """
|
||||||
|
| |____^ DOC502
|
||||||
|
19 | return distance / time
|
||||||
|
|
|
||||||
|
|
||||||
|
DOC502_google.py:33:1: DOC502 Raised exceptions are not explicitly raised: `FasterThanLightError`, `DivisionByZero`
|
||||||
|
|
|
||||||
|
31 | Speed as distance divided by time.
|
||||||
|
32 |
|
||||||
|
33 | / Raises:
|
||||||
|
34 | | FasterThanLightError: If speed is greater than the speed of light.
|
||||||
|
35 | | DivisionByZero: Divide by zero.
|
||||||
|
36 | | """
|
||||||
|
| |____^ DOC502
|
||||||
|
37 | return distance / time
|
||||||
|
|
|
||||||
|
|
||||||
|
DOC502_google.py:51:1: DOC502 Raised exception is not explicitly raised: `DivisionByZero`
|
||||||
|
|
|
||||||
|
49 | Speed as distance divided by time.
|
||||||
|
50 |
|
||||||
|
51 | / Raises:
|
||||||
|
52 | | FasterThanLightError: If speed is greater than the speed of light.
|
||||||
|
53 | | DivisionByZero: Divide by zero.
|
||||||
|
54 | | """
|
||||||
|
| |____^ DOC502
|
||||||
|
55 | try:
|
||||||
|
56 | return distance / time
|
||||||
|
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff_linter/src/rules/pydoclint/mod.rs
|
||||||
|
---
|
||||||
|
DOC502_numpy.py:22:1: DOC502 Raised exception is not explicitly raised: `FasterThanLightError`
|
||||||
|
|
|
||||||
|
20 | Speed as distance divided by time.
|
||||||
|
21 |
|
||||||
|
22 | / Raises
|
||||||
|
23 | | ------
|
||||||
|
24 | | FasterThanLightError
|
||||||
|
25 | | If speed is greater than the speed of light.
|
||||||
|
26 | | """
|
||||||
|
| |____^ DOC502
|
||||||
|
27 | return distance / time
|
||||||
|
|
|
||||||
|
|
||||||
|
DOC502_numpy.py:47:1: DOC502 Raised exceptions are not explicitly raised: `FasterThanLightError`, `DivisionByZero`
|
||||||
|
|
|
||||||
|
45 | Speed as distance divided by time.
|
||||||
|
46 |
|
||||||
|
47 | / Raises
|
||||||
|
48 | | ------
|
||||||
|
49 | | FasterThanLightError
|
||||||
|
50 | | If speed is greater than the speed of light.
|
||||||
|
51 | | DivisionByZero
|
||||||
|
52 | | If attempting to divide by zero.
|
||||||
|
53 | | """
|
||||||
|
| |____^ DOC502
|
||||||
|
54 | return distance / time
|
||||||
|
|
|
||||||
|
|
||||||
|
DOC502_numpy.py:74:1: DOC502 Raised exception is not explicitly raised: `DivisionByZero`
|
||||||
|
|
|
||||||
|
72 | Speed as distance divided by time.
|
||||||
|
73 |
|
||||||
|
74 | / Raises
|
||||||
|
75 | | ------
|
||||||
|
76 | | FasterThanLightError
|
||||||
|
77 | | If speed is greater than the speed of light.
|
||||||
|
78 | | DivisionByZero
|
||||||
|
79 | | If attempting to divide by zero.
|
||||||
|
80 | | """
|
||||||
|
| |____^ DOC502
|
||||||
|
81 | try:
|
||||||
|
82 | return distance / time
|
||||||
|
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff_linter/src/rules/pydoclint/mod.rs
|
||||||
|
---
|
||||||
|
DOC501_google.py:46:15: DOC501 Raised exception `FasterThanLightError` missing from docstring
|
||||||
|
|
|
||||||
|
44 | return distance / time
|
||||||
|
45 | except ZeroDivisionError as exc:
|
||||||
|
46 | raise FasterThanLightError from exc
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^ DOC501
|
||||||
|
|
|
||||||
|
|
||||||
|
DOC501_google.py:63:15: DOC501 Raised exception `FasterThanLightError` missing from docstring
|
||||||
|
|
|
||||||
|
61 | return distance / time
|
||||||
|
62 | except ZeroDivisionError as exc:
|
||||||
|
63 | raise FasterThanLightError from exc
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^ DOC501
|
||||||
|
64 | except:
|
||||||
|
65 | raise ValueError
|
||||||
|
|
|
||||||
|
|
||||||
|
DOC501_google.py:65:15: DOC501 Raised exception `ValueError` missing from docstring
|
||||||
|
|
|
||||||
|
63 | raise FasterThanLightError from exc
|
||||||
|
64 | except:
|
||||||
|
65 | raise ValueError
|
||||||
|
| ^^^^^^^^^^ DOC501
|
||||||
|
|
|
||||||
|
|
||||||
|
DOC501_google.py:115:11: DOC501 Raised exception `AnotherError` missing from docstring
|
||||||
|
|
|
||||||
|
113 | Speed as distance divided by time.
|
||||||
|
114 | """
|
||||||
|
115 | raise AnotherError
|
||||||
|
| ^^^^^^^^^^^^ DOC501
|
||||||
|
|
|
||||||
|
|
||||||
|
DOC501_google.py:129:11: DOC501 Raised exception `AnotherError` missing from docstring
|
||||||
|
|
|
||||||
|
127 | Speed as distance divided by time.
|
||||||
|
128 | """
|
||||||
|
129 | raise AnotherError()
|
||||||
|
| ^^^^^^^^^^^^^^ DOC501
|
||||||
|
|
|
||||||
|
|
||||||
|
DOC501_google.py:139:11: DOC501 Raised exception `SomeError` missing from docstring
|
||||||
|
|
|
||||||
|
137 | bar: Bar.
|
||||||
|
138 | """
|
||||||
|
139 | raise something.SomeError
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^ DOC501
|
||||||
|
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff_linter/src/rules/pydoclint/mod.rs
|
||||||
|
---
|
||||||
|
DOC501_numpy.py:53:15: DOC501 Raised exception `FasterThanLightError` missing from docstring
|
||||||
|
|
|
||||||
|
51 | return distance / time
|
||||||
|
52 | except ZeroDivisionError as exc:
|
||||||
|
53 | raise FasterThanLightError from exc
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^ DOC501
|
||||||
|
|
|
||||||
|
|
||||||
|
DOC501_numpy.py:76:15: DOC501 Raised exception `FasterThanLightError` missing from docstring
|
||||||
|
|
|
||||||
|
74 | return distance / time
|
||||||
|
75 | except ZeroDivisionError as exc:
|
||||||
|
76 | raise FasterThanLightError from exc
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^ DOC501
|
||||||
|
77 | except:
|
||||||
|
78 | raise ValueError
|
||||||
|
|
|
||||||
|
|
||||||
|
DOC501_numpy.py:78:15: DOC501 Raised exception `ValueError` missing from docstring
|
||||||
|
|
|
||||||
|
76 | raise FasterThanLightError from exc
|
||||||
|
77 | except:
|
||||||
|
78 | raise ValueError
|
||||||
|
| ^^^^^^^^^^ DOC501
|
||||||
|
|
|
|
@ -5,6 +5,11 @@ use ruff_python_ast::name::QualifiedName;
|
||||||
use ruff_python_semantic::{Definition, SemanticModel};
|
use ruff_python_semantic::{Definition, SemanticModel};
|
||||||
use ruff_source_file::UniversalNewlines;
|
use ruff_source_file::UniversalNewlines;
|
||||||
|
|
||||||
|
use crate::docstrings::sections::{SectionContexts, SectionKind};
|
||||||
|
use crate::docstrings::styles::SectionStyle;
|
||||||
|
use crate::docstrings::Docstring;
|
||||||
|
use crate::rules::pydocstyle::settings::Convention;
|
||||||
|
|
||||||
/// Return the index of the first logical line in a string.
|
/// Return the index of the first logical line in a string.
|
||||||
pub(super) fn logical_line(content: &str) -> Option<usize> {
|
pub(super) fn logical_line(content: &str) -> Option<usize> {
|
||||||
// Find the first logical line.
|
// Find the first logical line.
|
||||||
|
@ -61,3 +66,59 @@ pub(crate) fn should_ignore_definition(
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_section_contexts<'a>(
|
||||||
|
docstring: &'a Docstring<'a>,
|
||||||
|
convention: Option<&'a Convention>,
|
||||||
|
) -> SectionContexts<'a> {
|
||||||
|
match convention {
|
||||||
|
Some(Convention::Google) => {
|
||||||
|
return SectionContexts::from_docstring(docstring, SectionStyle::Google);
|
||||||
|
}
|
||||||
|
Some(Convention::Numpy) => {
|
||||||
|
return SectionContexts::from_docstring(docstring, SectionStyle::Numpy);
|
||||||
|
}
|
||||||
|
Some(Convention::Pep257) | None => {
|
||||||
|
// There are some overlapping section names, between the Google and NumPy conventions
|
||||||
|
// (e.g., "Returns", "Raises"). Break ties by checking for the presence of some of the
|
||||||
|
// section names that are unique to each convention.
|
||||||
|
|
||||||
|
// If the docstring contains `Parameters:` or `Other Parameters:`, use the NumPy
|
||||||
|
// convention.
|
||||||
|
let numpy_sections = SectionContexts::from_docstring(docstring, SectionStyle::Numpy);
|
||||||
|
if numpy_sections.iter().any(|context| {
|
||||||
|
matches!(
|
||||||
|
context.kind(),
|
||||||
|
SectionKind::Parameters
|
||||||
|
| SectionKind::OtherParams
|
||||||
|
| SectionKind::OtherParameters
|
||||||
|
)
|
||||||
|
}) {
|
||||||
|
return numpy_sections;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the docstring contains any argument specifier, use the Google convention.
|
||||||
|
let google_sections = SectionContexts::from_docstring(docstring, SectionStyle::Google);
|
||||||
|
if google_sections.iter().any(|context| {
|
||||||
|
matches!(
|
||||||
|
context.kind(),
|
||||||
|
SectionKind::Args
|
||||||
|
| SectionKind::Arguments
|
||||||
|
| SectionKind::KeywordArgs
|
||||||
|
| SectionKind::KeywordArguments
|
||||||
|
| SectionKind::OtherArgs
|
||||||
|
| SectionKind::OtherArguments
|
||||||
|
)
|
||||||
|
}) {
|
||||||
|
return google_sections;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, use whichever convention matched more sections.
|
||||||
|
if google_sections.len() > numpy_sections.len() {
|
||||||
|
google_sections
|
||||||
|
} else {
|
||||||
|
numpy_sections
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1324,67 +1324,16 @@ impl AlwaysFixableViolation for BlankLinesBetweenHeaderAndContent {
|
||||||
pub(crate) fn sections(
|
pub(crate) fn sections(
|
||||||
checker: &mut Checker,
|
checker: &mut Checker,
|
||||||
docstring: &Docstring,
|
docstring: &Docstring,
|
||||||
|
section_contexts: &SectionContexts,
|
||||||
convention: Option<&Convention>,
|
convention: Option<&Convention>,
|
||||||
) {
|
) {
|
||||||
match convention {
|
match convention {
|
||||||
Some(Convention::Google) => {
|
Some(Convention::Google) => parse_google_sections(checker, docstring, section_contexts),
|
||||||
parse_google_sections(
|
Some(Convention::Numpy) => parse_numpy_sections(checker, docstring, section_contexts),
|
||||||
checker,
|
Some(Convention::Pep257) | None => match section_contexts.style() {
|
||||||
docstring,
|
SectionStyle::Google => parse_google_sections(checker, docstring, section_contexts),
|
||||||
&SectionContexts::from_docstring(docstring, SectionStyle::Google),
|
SectionStyle::Numpy => parse_numpy_sections(checker, docstring, section_contexts),
|
||||||
);
|
},
|
||||||
}
|
|
||||||
Some(Convention::Numpy) => {
|
|
||||||
parse_numpy_sections(
|
|
||||||
checker,
|
|
||||||
docstring,
|
|
||||||
&SectionContexts::from_docstring(docstring, SectionStyle::Numpy),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Some(Convention::Pep257) | None => {
|
|
||||||
// There are some overlapping section names, between the Google and NumPy conventions
|
|
||||||
// (e.g., "Returns", "Raises"). Break ties by checking for the presence of some of the
|
|
||||||
// section names that are unique to each convention.
|
|
||||||
|
|
||||||
// If the docstring contains `Parameters:` or `Other Parameters:`, use the NumPy
|
|
||||||
// convention.
|
|
||||||
let numpy_sections = SectionContexts::from_docstring(docstring, SectionStyle::Numpy);
|
|
||||||
if numpy_sections.iter().any(|context| {
|
|
||||||
matches!(
|
|
||||||
context.kind(),
|
|
||||||
SectionKind::Parameters
|
|
||||||
| SectionKind::OtherParams
|
|
||||||
| SectionKind::OtherParameters
|
|
||||||
)
|
|
||||||
}) {
|
|
||||||
parse_numpy_sections(checker, docstring, &numpy_sections);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the docstring contains any argument specifier, use the Google convention.
|
|
||||||
let google_sections = SectionContexts::from_docstring(docstring, SectionStyle::Google);
|
|
||||||
if google_sections.iter().any(|context| {
|
|
||||||
matches!(
|
|
||||||
context.kind(),
|
|
||||||
SectionKind::Args
|
|
||||||
| SectionKind::Arguments
|
|
||||||
| SectionKind::KeywordArgs
|
|
||||||
| SectionKind::KeywordArguments
|
|
||||||
| SectionKind::OtherArgs
|
|
||||||
| SectionKind::OtherArguments
|
|
||||||
)
|
|
||||||
}) {
|
|
||||||
parse_google_sections(checker, docstring, &google_sections);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, use whichever convention matched more sections.
|
|
||||||
if google_sections.len() > numpy_sections.len() {
|
|
||||||
parse_google_sections(checker, docstring, &google_sections);
|
|
||||||
} else {
|
|
||||||
parse_numpy_sections(checker, docstring, &numpy_sections);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
5
ruff.schema.json
generated
5
ruff.schema.json
generated
|
@ -2874,6 +2874,11 @@
|
||||||
"DJ01",
|
"DJ01",
|
||||||
"DJ012",
|
"DJ012",
|
||||||
"DJ013",
|
"DJ013",
|
||||||
|
"DOC",
|
||||||
|
"DOC5",
|
||||||
|
"DOC50",
|
||||||
|
"DOC501",
|
||||||
|
"DOC502",
|
||||||
"DTZ",
|
"DTZ",
|
||||||
"DTZ0",
|
"DTZ0",
|
||||||
"DTZ00",
|
"DTZ00",
|
||||||
|
|
|
@ -48,7 +48,7 @@ mod tests {
|
||||||
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
|
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
|
||||||
let diagnostics = test_path(
|
let diagnostics = test_path(
|
||||||
Path::new("%s").join(path).as_path(),
|
Path::new("%s").join(path).as_path(),
|
||||||
&settings::Settings::for_rule(rule_code),
|
&settings::LinterSettings::for_rule(rule_code),
|
||||||
)?;
|
)?;
|
||||||
assert_messages!(snapshot, diagnostics);
|
assert_messages!(snapshot, diagnostics);
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue