From 44c9adfd7d117cd3a22cd787e5c1dc114d32a4e6 Mon Sep 17 00:00:00 2001 From: augustelalande Date: Sun, 19 May 2024 15:22:35 -0400 Subject: [PATCH 01/24] implement DAR401 and DAR402 --- .../test/fixtures/darglint/DAR401.py | 51 ++++ .../test/fixtures/darglint/DAR402.py | 51 ++++ .../src/checkers/ast/analyze/definitions.rs | 23 +- crates/ruff_linter/src/codes.rs | 4 + crates/ruff_linter/src/docstrings/sections.rs | 2 +- crates/ruff_linter/src/registry.rs | 3 + crates/ruff_linter/src/rules/darglint/mod.rs | 27 ++ .../rules/darglint/rules/check_docstring.rs | 268 ++++++++++++++++++ .../src/rules/darglint/rules/mod.rs | 3 + ...string-extraneous-exception_DAR402.py.snap | 38 +++ ...docstring-missing-exception_DAR401.py.snap | 28 ++ crates/ruff_linter/src/rules/mod.rs | 1 + ruff.schema.json | 5 + scripts/add_plugin.py | 2 +- 14 files changed, 501 insertions(+), 5 deletions(-) create mode 100644 crates/ruff_linter/resources/test/fixtures/darglint/DAR401.py create mode 100644 crates/ruff_linter/resources/test/fixtures/darglint/DAR402.py create mode 100644 crates/ruff_linter/src/rules/darglint/mod.rs create mode 100644 crates/ruff_linter/src/rules/darglint/rules/check_docstring.rs create mode 100644 crates/ruff_linter/src/rules/darglint/rules/mod.rs create mode 100644 crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-extraneous-exception_DAR402.py.snap create mode 100644 crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-missing-exception_DAR401.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/darglint/DAR401.py b/crates/ruff_linter/resources/test/fixtures/darglint/DAR401.py new file mode 100644 index 0000000000000..a3dde20c59ecd --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/darglint/DAR401.py @@ -0,0 +1,51 @@ +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 + + +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 + + +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 diff --git a/crates/ruff_linter/resources/test/fixtures/darglint/DAR402.py b/crates/ruff_linter/resources/test/fixtures/darglint/DAR402.py new file mode 100644 index 0000000000000..d6a6254f51a88 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/darglint/DAR402.py @@ -0,0 +1,51 @@ +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 + + +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 + + +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 diff --git a/crates/ruff_linter/src/checkers/ast/analyze/definitions.rs b/crates/ruff_linter/src/checkers/ast/analyze/definitions.rs index 4bff13ac69cb2..81324603122c3 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/definitions.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/definitions.rs @@ -10,7 +10,7 @@ use crate::checkers::ast::Checker; use crate::codes::Rule; use crate::docstrings::Docstring; use crate::fs::relativize_path; -use crate::rules::{flake8_annotations, flake8_pyi, pydocstyle, pylint}; +use crate::rules::{darglint, flake8_annotations, flake8_pyi, pydocstyle, pylint}; use crate::{docstrings, warn_user}; /// Run lint rules over all [`Definition`] nodes in the [`SemanticModel`]. @@ -83,12 +83,17 @@ pub(crate) fn definitions(checker: &mut Checker) { Rule::UndocumentedPublicNestedClass, Rule::UndocumentedPublicPackage, ]); + let enforce_darglint = checker.any_enabled(&[ + Rule::DocstringMissingException, + Rule::DocstringExtraneousException, + ]); if !enforce_annotations && !enforce_docstrings && !enforce_stubs && !enforce_stubs_and_runtime && !enforce_dunder_method + && !enforce_darglint { return; } @@ -169,8 +174,8 @@ pub(crate) fn definitions(checker: &mut Checker) { } } - // pydocstyle - if enforce_docstrings { + // pydocstyle, darglint + if enforce_docstrings || enforce_darglint { if pydocstyle::helpers::should_ignore_definition( definition, &checker.settings.pydocstyle.ignore_decorators, @@ -311,6 +316,18 @@ pub(crate) fn definitions(checker: &mut Checker) { checker.settings.pydocstyle.convention.as_ref(), ); } + + if checker.any_enabled(&[ + Rule::DocstringMissingException, + Rule::DocstringExtraneousException, + ]) { + darglint::rules::check_docstring( + checker, + definition, + &docstring, + checker.settings.pydocstyle.convention.as_ref(), + ); + } } } } diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index 05906db3c906b..9550526a1fa72 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -942,6 +942,10 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Numpy, "003") => (RuleGroup::Stable, rules::numpy::rules::NumpyDeprecatedFunction), (Numpy, "201") => (RuleGroup::Stable, rules::numpy::rules::Numpy2Deprecation), + // darglint + (Darglint, "401") => (RuleGroup::Preview, rules::darglint::rules::DocstringMissingException), + (Darglint, "402") => (RuleGroup::Preview, rules::darglint::rules::DocstringExtraneousException), + // ruff (Ruff, "001") => (RuleGroup::Stable, rules::ruff::rules::AmbiguousUnicodeCharacterString), (Ruff, "002") => (RuleGroup::Stable, rules::ruff::rules::AmbiguousUnicodeCharacterDocstring), diff --git a/crates/ruff_linter/src/docstrings/sections.rs b/crates/ruff_linter/src/docstrings/sections.rs index a6560084ff48b..fb9728cbbdfbf 100644 --- a/crates/ruff_linter/src/docstrings/sections.rs +++ b/crates/ruff_linter/src/docstrings/sections.rs @@ -396,7 +396,7 @@ impl<'a> SectionContext<'a> { 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()] } diff --git a/crates/ruff_linter/src/registry.rs b/crates/ruff_linter/src/registry.rs index fa6bdee4587db..03431281b275c 100644 --- a/crates/ruff_linter/src/registry.rs +++ b/crates/ruff_linter/src/registry.rs @@ -205,6 +205,9 @@ pub enum Linter { /// [refurb](https://pypi.org/project/refurb/) #[prefix = "FURB"] Refurb, + /// [darglint](https://pypi.org/project/darglint/) + #[prefix = "DAR"] + Darglint, /// Ruff-specific rules #[prefix = "RUF"] Ruff, diff --git a/crates/ruff_linter/src/rules/darglint/mod.rs b/crates/ruff_linter/src/rules/darglint/mod.rs new file mode 100644 index 0000000000000..398d3c0a8bda6 --- /dev/null +++ b/crates/ruff_linter/src/rules/darglint/mod.rs @@ -0,0 +1,27 @@ +//! Rules from [darglint](https://pypi.org/project/darglint/). +pub(crate) mod rules; + +#[cfg(test)] +mod tests { + use std::convert::AsRef; + use std::path::Path; + + use anyhow::Result; + use test_case::test_case; + + use crate::registry::Rule; + use crate::test::test_path; + use crate::{assert_messages, settings}; + + #[test_case(Rule::DocstringMissingException, Path::new("DAR401.py"))] + #[test_case(Rule::DocstringExtraneousException, Path::new("DAR402.py"))] + fn rules(rule_code: Rule, path: &Path) -> Result<()> { + let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy()); + let diagnostics = test_path( + Path::new("darglint").join(path).as_path(), + &settings::LinterSettings::for_rule(rule_code), + )?; + assert_messages!(snapshot, diagnostics); + Ok(()) + } +} diff --git a/crates/ruff_linter/src/rules/darglint/rules/check_docstring.rs b/crates/ruff_linter/src/rules/darglint/rules/check_docstring.rs new file mode 100644 index 0000000000000..a1ab3f155a2fc --- /dev/null +++ b/crates/ruff_linter/src/rules/darglint/rules/check_docstring.rs @@ -0,0 +1,268 @@ +use ruff_diagnostics::Diagnostic; +use ruff_diagnostics::Violation; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::visitor::{self, Visitor}; +use ruff_python_ast::{self as ast, Expr, Stmt}; +use ruff_python_semantic::Definition; +use ruff_text_size::TextRange; + +use crate::checkers::ast::Checker; +use crate::docstrings::sections::{SectionContexts, SectionKind}; +use crate::docstrings::styles::SectionStyle; +use crate::docstrings::Docstring; +use crate::registry::Rule; +use crate::rules::pydocstyle::settings::Convention; + +/// ## What it does +/// Checks for function docstrings that do not include documentation for all +/// raised exceptions. +/// +/// ## Why is this bad? +/// This rule helps prevent you from leaving docstrings unfinished or incomplete. +/// Some conventions require all explicit exceptions to be documented. +/// +/// ## 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 left out of 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, +} + +impl Violation for DocstringExtraneousException { + #[derive_message_formats] + fn message(&self) -> String { + let DocstringExtraneousException { ids } = self; + format!("{} not explicitly raised.", ids.join(", ")) + } +} + +// Parse docstring +#[derive(Debug)] +struct DocstringEntries { + raised_exceptions: Vec, + raised_exceptions_range: Option, +} + +impl DocstringEntries { + fn new(sections: &SectionContexts) -> Self { + let mut raised_exceptions: Vec = Vec::new(); + let mut raised_exceptions_range = None; + + for section in sections.iter() { + match section.kind() { + SectionKind::Raises => { + raised_exceptions = parse_entries(section.following_lines_str()); + raised_exceptions_range = Some(section.range()); + } + _ => {} + } + } + + Self { + raised_exceptions, + raised_exceptions_range, + } + } +} + +fn parse_entries(content: &str) -> Vec { + let mut entries: Vec = Vec::new(); + for potential in content.split('\n') { + let Some(colon_idx) = potential.find(':') else { + continue; + }; + let entry = potential[..colon_idx].trim().to_string(); + entries.push(entry); + } + entries +} + +// Parse body +#[derive(Debug)] +struct Entry { + id: String, + range: TextRange, +} + +#[derive(Debug)] +struct BodyEntries { + raised_exceptions: Vec, +} + +impl BodyEntries { + fn new() -> Self { + Self { + raised_exceptions: Vec::new(), + } + } +} + +impl Visitor<'_> for BodyEntries { + fn visit_stmt(&mut self, stmt: &Stmt) { + if let Stmt::Raise(ast::StmtRaise { exc, .. }) = stmt { + if let Some(exc) = exc { + if let Expr::Name(ast::ExprName { id, range, .. }) = exc.as_ref() { + self.raised_exceptions.push(Entry { + id: id.to_string(), + range: *range, + }); + } + } + } + visitor::walk_stmt(self, stmt); + } +} + +/// DAR401, DAR402 +pub(crate) fn check_docstring( + checker: &mut Checker, + definition: &Definition, + docstring: &Docstring, + convention: Option<&Convention>, +) { + let Definition::Member(member) = definition else { + return; + }; + // let sections = match convention { + // Some(Convention::Google) => { + // &SectionContexts::from_docstring(docstring, SectionStyle::Google) + // } + + // Some(Convention::Numpy) => &SectionContexts::from_docstring(docstring, SectionStyle::Numpy), + // _ => { + // return; + // } + // }; + + let sections = SectionContexts::from_docstring(docstring, SectionStyle::Google); + let docstring_entries = DocstringEntries::new(§ions); + + let mut body_entries = BodyEntries::new(); + visitor::walk_body(&mut body_entries, &member.body()); + + // DAR401 + if checker.enabled(Rule::DocstringMissingException) { + for body_raise in body_entries.raised_exceptions.iter() { + if !docstring_entries.raised_exceptions.contains(&body_raise.id) { + let diagnostic = Diagnostic::new( + DocstringMissingException { + id: body_raise.id.clone(), + }, + body_raise.range, + ); + checker.diagnostics.push(diagnostic); + } + } + } + + // DAR402 + if checker.enabled(Rule::DocstringExtraneousException) { + let mut extraneous_exceptions = Vec::new(); + for docstring_raise in docstring_entries.raised_exceptions { + if !body_entries + .raised_exceptions + .iter() + .any(|r| r.id == docstring_raise) + { + extraneous_exceptions.push(docstring_raise); + } + } + if !extraneous_exceptions.is_empty() { + let diagnostic = Diagnostic::new( + DocstringExtraneousException { + ids: extraneous_exceptions, + }, + docstring_entries.raised_exceptions_range.unwrap(), + ); + checker.diagnostics.push(diagnostic); + } + } +} diff --git a/crates/ruff_linter/src/rules/darglint/rules/mod.rs b/crates/ruff_linter/src/rules/darglint/rules/mod.rs new file mode 100644 index 0000000000000..de7b36c2c5c71 --- /dev/null +++ b/crates/ruff_linter/src/rules/darglint/rules/mod.rs @@ -0,0 +1,3 @@ +pub(crate) use check_docstring::*; + +mod check_docstring; diff --git a/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-extraneous-exception_DAR402.py.snap b/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-extraneous-exception_DAR402.py.snap new file mode 100644 index 0000000000000..c6c4c20c82f72 --- /dev/null +++ b/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-extraneous-exception_DAR402.py.snap @@ -0,0 +1,38 @@ +--- +source: crates/ruff_linter/src/rules/darglint/mod.rs +--- +DAR402.py:11:1: DAR402 FasterThanLightError not explicitly raised. + | + 9 | Speed as distance divided by time. +10 | +11 | / Raises: +12 | | FasterThanLightError: If speed is greater than the speed of light. +13 | | """ + | |____^ DAR402 +14 | return distance / time + | + +DAR402.py:27:1: DAR402 FasterThanLightError, DivisionByZero not explicitly raised. + | +25 | Speed as distance divided by time. +26 | +27 | / Raises: +28 | | FasterThanLightError: If speed is greater than the speed of light. +29 | | DivisionByZero: Divide by zero. +30 | | """ + | |____^ DAR402 +31 | return distance / time + | + +DAR402.py:44:1: DAR402 DivisionByZero not explicitly raised. + | +42 | Speed as distance divided by time. +43 | +44 | / Raises: +45 | | FasterThanLightError: If speed is greater than the speed of light. +46 | | DivisionByZero: Divide by zero. +47 | | """ + | |____^ DAR402 +48 | try: +49 | return distance / time + | diff --git a/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-missing-exception_DAR401.py.snap b/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-missing-exception_DAR401.py.snap new file mode 100644 index 0000000000000..8e336979322bf --- /dev/null +++ b/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-missing-exception_DAR401.py.snap @@ -0,0 +1,28 @@ +--- +source: crates/ruff_linter/src/rules/darglint/mod.rs +--- +DAR401.py:33:15: DAR401 Raised exception `FasterThanLightError` missing from docstring + | +31 | return distance / time +32 | except ZeroDivisionError as exc: +33 | raise FasterThanLightError from exc + | ^^^^^^^^^^^^^^^^^^^^ DAR401 + | + +DAR401.py:49:15: DAR401 Raised exception `FasterThanLightError` missing from docstring + | +47 | return distance / time +48 | except ZeroDivisionError as exc: +49 | raise FasterThanLightError from exc + | ^^^^^^^^^^^^^^^^^^^^ DAR401 +50 | except: +51 | raise ValueError + | + +DAR401.py:51:15: DAR401 Raised exception `ValueError` missing from docstring + | +49 | raise FasterThanLightError from exc +50 | except: +51 | raise ValueError + | ^^^^^^^^^^ DAR401 + | diff --git a/crates/ruff_linter/src/rules/mod.rs b/crates/ruff_linter/src/rules/mod.rs index 64b0128cae164..c80fa0789224c 100644 --- a/crates/ruff_linter/src/rules/mod.rs +++ b/crates/ruff_linter/src/rules/mod.rs @@ -1,5 +1,6 @@ #![allow(clippy::useless_format)] pub mod airflow; +pub mod darglint; pub mod eradicate; pub mod flake8_2020; pub mod flake8_annotations; diff --git a/ruff.schema.json b/ruff.schema.json index ec514be5fc8a4..105b6af2d055d 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2825,6 +2825,11 @@ "D417", "D418", "D419", + "DAR", + "DAR4", + "DAR40", + "DAR401", + "DAR402", "DJ", "DJ0", "DJ00", diff --git a/scripts/add_plugin.py b/scripts/add_plugin.py index 0de67c188dfd7..d50bcb9f461cb 100755 --- a/scripts/add_plugin.py +++ b/scripts/add_plugin.py @@ -48,7 +48,7 @@ def main(*, plugin: str, url: str, prefix_code: str) -> None: let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy()); let diagnostics = test_path( Path::new("%s").join(path).as_path(), - &settings::Settings::for_rule(rule_code), + &settings::LinterSettings::for_rule(rule_code), )?; assert_messages!(snapshot, diagnostics); Ok(()) From 099b8b59e9815dacea7995645e99b3a98800d216 Mon Sep 17 00:00:00 2001 From: augustelalande Date: Sun, 19 May 2024 16:32:48 -0400 Subject: [PATCH 02/24] support numpy style --- .../darglint/{DAR401.py => DAR401_google.py} | 0 .../test/fixtures/darglint/DAR401_numpy.py | 71 +++++++++++++++++ .../darglint/{DAR402.py => DAR402_google.py} | 0 .../test/fixtures/darglint/DAR402_numpy.py | 77 +++++++++++++++++++ crates/ruff_linter/src/rules/darglint/mod.rs | 36 ++++++++- .../rules/darglint/rules/check_docstring.rs | 55 +++++++++---- ...xtraneous-exception_DAR402_google.py.snap} | 6 +- ...-extraneous-exception_DAR402_numpy.py.snap | 46 +++++++++++ ...g-missing-exception_DAR401_google.py.snap} | 6 +- ...ing-missing-exception_DAR401_numpy.py.snap | 28 +++++++ 10 files changed, 301 insertions(+), 24 deletions(-) rename crates/ruff_linter/resources/test/fixtures/darglint/{DAR401.py => DAR401_google.py} (100%) create mode 100644 crates/ruff_linter/resources/test/fixtures/darglint/DAR401_numpy.py rename crates/ruff_linter/resources/test/fixtures/darglint/{DAR402.py => DAR402_google.py} (100%) create mode 100644 crates/ruff_linter/resources/test/fixtures/darglint/DAR402_numpy.py rename crates/ruff_linter/src/rules/darglint/snapshots/{ruff_linter__rules__darglint__tests__docstring-extraneous-exception_DAR402.py.snap => ruff_linter__rules__darglint__tests__docstring-extraneous-exception_DAR402_google.py.snap} (79%) create mode 100644 crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-extraneous-exception_DAR402_numpy.py.snap rename crates/ruff_linter/src/rules/darglint/snapshots/{ruff_linter__rules__darglint__tests__docstring-missing-exception_DAR401.py.snap => ruff_linter__rules__darglint__tests__docstring-missing-exception_DAR401_google.py.snap} (69%) create mode 100644 crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-missing-exception_DAR401_numpy.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/darglint/DAR401.py b/crates/ruff_linter/resources/test/fixtures/darglint/DAR401_google.py similarity index 100% rename from crates/ruff_linter/resources/test/fixtures/darglint/DAR401.py rename to crates/ruff_linter/resources/test/fixtures/darglint/DAR401_google.py diff --git a/crates/ruff_linter/resources/test/fixtures/darglint/DAR401_numpy.py b/crates/ruff_linter/resources/test/fixtures/darglint/DAR401_numpy.py new file mode 100644 index 0000000000000..05befb273a94a --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/darglint/DAR401_numpy.py @@ -0,0 +1,71 @@ +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 + + +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 + + +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 diff --git a/crates/ruff_linter/resources/test/fixtures/darglint/DAR402.py b/crates/ruff_linter/resources/test/fixtures/darglint/DAR402_google.py similarity index 100% rename from crates/ruff_linter/resources/test/fixtures/darglint/DAR402.py rename to crates/ruff_linter/resources/test/fixtures/darglint/DAR402_google.py diff --git a/crates/ruff_linter/resources/test/fixtures/darglint/DAR402_numpy.py b/crates/ruff_linter/resources/test/fixtures/darglint/DAR402_numpy.py new file mode 100644 index 0000000000000..28859fbf0cba5 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/darglint/DAR402_numpy.py @@ -0,0 +1,77 @@ +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 + + +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 + + +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 diff --git a/crates/ruff_linter/src/rules/darglint/mod.rs b/crates/ruff_linter/src/rules/darglint/mod.rs index 398d3c0a8bda6..8eb45b3a88f52 100644 --- a/crates/ruff_linter/src/rules/darglint/mod.rs +++ b/crates/ruff_linter/src/rules/darglint/mod.rs @@ -3,6 +3,7 @@ pub(crate) mod rules; #[cfg(test)] mod tests { + use std::collections::BTreeSet; use std::convert::AsRef; use std::path::Path; @@ -10,16 +11,43 @@ mod tests { 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("DAR401.py"))] - #[test_case(Rule::DocstringExtraneousException, Path::new("DAR402.py"))] - fn rules(rule_code: Rule, path: &Path) -> Result<()> { + #[test_case(Rule::DocstringMissingException, Path::new("DAR401_google.py"))] + #[test_case(Rule::DocstringExtraneousException, Path::new("DAR402_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("darglint").join(path).as_path(), - &settings::LinterSettings::for_rule(rule_code), + &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("DAR401_numpy.py"))] + #[test_case(Rule::DocstringExtraneousException, Path::new("DAR402_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("darglint").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(()) diff --git a/crates/ruff_linter/src/rules/darglint/rules/check_docstring.rs b/crates/ruff_linter/src/rules/darglint/rules/check_docstring.rs index a1ab3f155a2fc..a3ff154d29910 100644 --- a/crates/ruff_linter/src/rules/darglint/rules/check_docstring.rs +++ b/crates/ruff_linter/src/rules/darglint/rules/check_docstring.rs @@ -132,14 +132,14 @@ struct DocstringEntries { } impl DocstringEntries { - fn new(sections: &SectionContexts) -> Self { + fn new(sections: &SectionContexts, style: SectionStyle) -> Self { let mut raised_exceptions: Vec = Vec::new(); let mut raised_exceptions_range = None; for section in sections.iter() { match section.kind() { SectionKind::Raises => { - raised_exceptions = parse_entries(section.following_lines_str()); + raised_exceptions = parse_entries(section.following_lines_str(), style); raised_exceptions_range = Some(section.range()); } _ => {} @@ -153,7 +153,14 @@ impl DocstringEntries { } } -fn parse_entries(content: &str) -> Vec { +fn parse_entries(content: &str, style: SectionStyle) -> Vec { + match style { + SectionStyle::Google => parse_entries_google(content), + SectionStyle::Numpy => parse_entries_numpy(content), + } +} + +fn parse_entries_google(content: &str) -> Vec { let mut entries: Vec = Vec::new(); for potential in content.split('\n') { let Some(colon_idx) = potential.find(':') else { @@ -165,6 +172,24 @@ fn parse_entries(content: &str) -> Vec { entries } +fn parse_entries_numpy(content: &str) -> Vec { + let mut entries: Vec = 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[indentation..].chars().next() { + if !first_char.is_whitespace() { + let entry = potential[indentation..].trim().to_string(); + entries.push(entry); + } + } + } + entries +} + // Parse body #[derive(Debug)] struct Entry { @@ -211,19 +236,21 @@ pub(crate) fn check_docstring( let Definition::Member(member) = definition else { return; }; - // let sections = match convention { - // Some(Convention::Google) => { - // &SectionContexts::from_docstring(docstring, SectionStyle::Google) - // } - // Some(Convention::Numpy) => &SectionContexts::from_docstring(docstring, SectionStyle::Numpy), - // _ => { - // return; - // } - // }; + let docstring_entries = match convention { + Some(Convention::Google) => { + let sections = SectionContexts::from_docstring(docstring, SectionStyle::Google); + DocstringEntries::new(§ions, SectionStyle::Google) + } - let sections = SectionContexts::from_docstring(docstring, SectionStyle::Google); - let docstring_entries = DocstringEntries::new(§ions); + Some(Convention::Numpy) => { + let sections = SectionContexts::from_docstring(docstring, SectionStyle::Numpy); + DocstringEntries::new(§ions, SectionStyle::Numpy) + } + _ => { + return; + } + }; let mut body_entries = BodyEntries::new(); visitor::walk_body(&mut body_entries, &member.body()); diff --git a/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-extraneous-exception_DAR402.py.snap b/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-extraneous-exception_DAR402_google.py.snap similarity index 79% rename from crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-extraneous-exception_DAR402.py.snap rename to crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-extraneous-exception_DAR402_google.py.snap index c6c4c20c82f72..3c6fc1c265687 100644 --- a/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-extraneous-exception_DAR402.py.snap +++ b/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-extraneous-exception_DAR402_google.py.snap @@ -1,7 +1,7 @@ --- source: crates/ruff_linter/src/rules/darglint/mod.rs --- -DAR402.py:11:1: DAR402 FasterThanLightError not explicitly raised. +DAR402_google.py:11:1: DAR402 FasterThanLightError not explicitly raised. | 9 | Speed as distance divided by time. 10 | @@ -12,7 +12,7 @@ DAR402.py:11:1: DAR402 FasterThanLightError not explicitly raised. 14 | return distance / time | -DAR402.py:27:1: DAR402 FasterThanLightError, DivisionByZero not explicitly raised. +DAR402_google.py:27:1: DAR402 FasterThanLightError, DivisionByZero not explicitly raised. | 25 | Speed as distance divided by time. 26 | @@ -24,7 +24,7 @@ DAR402.py:27:1: DAR402 FasterThanLightError, DivisionByZero not explicitly raise 31 | return distance / time | -DAR402.py:44:1: DAR402 DivisionByZero not explicitly raised. +DAR402_google.py:44:1: DAR402 DivisionByZero not explicitly raised. | 42 | Speed as distance divided by time. 43 | diff --git a/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-extraneous-exception_DAR402_numpy.py.snap b/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-extraneous-exception_DAR402_numpy.py.snap new file mode 100644 index 0000000000000..e4fbe295878b6 --- /dev/null +++ b/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-extraneous-exception_DAR402_numpy.py.snap @@ -0,0 +1,46 @@ +--- +source: crates/ruff_linter/src/rules/darglint/mod.rs +--- +DAR402_numpy.py:17:1: DAR402 FasterThanLightError not explicitly raised. + | +15 | Speed as distance divided by time. +16 | +17 | / Raises +18 | | ------ +19 | | FasterThanLightError +20 | | If speed is greater than the speed of light. +21 | | """ + | |____^ DAR402 +22 | return distance / time + | + +DAR402_numpy.py:41:1: DAR402 FasterThanLightError, DivisionByZero not explicitly raised. + | +39 | Speed as distance divided by time. +40 | +41 | / Raises +42 | | ------ +43 | | FasterThanLightError +44 | | If speed is greater than the speed of light. +45 | | DivisionByZero +46 | | If attempting to divide by zero. +47 | | """ + | |____^ DAR402 +48 | return distance / time + | + +DAR402_numpy.py:67:1: DAR402 DivisionByZero not explicitly raised. + | +65 | Speed as distance divided by time. +66 | +67 | / Raises +68 | | ------ +69 | | FasterThanLightError +70 | | If speed is greater than the speed of light. +71 | | DivisionByZero +72 | | If attempting to divide by zero. +73 | | """ + | |____^ DAR402 +74 | try: +75 | return distance / time + | diff --git a/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-missing-exception_DAR401.py.snap b/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-missing-exception_DAR401_google.py.snap similarity index 69% rename from crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-missing-exception_DAR401.py.snap rename to crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-missing-exception_DAR401_google.py.snap index 8e336979322bf..b0130fae3873b 100644 --- a/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-missing-exception_DAR401.py.snap +++ b/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-missing-exception_DAR401_google.py.snap @@ -1,7 +1,7 @@ --- source: crates/ruff_linter/src/rules/darglint/mod.rs --- -DAR401.py:33:15: DAR401 Raised exception `FasterThanLightError` missing from docstring +DAR401_google.py:33:15: DAR401 Raised exception `FasterThanLightError` missing from docstring | 31 | return distance / time 32 | except ZeroDivisionError as exc: @@ -9,7 +9,7 @@ DAR401.py:33:15: DAR401 Raised exception `FasterThanLightError` missing from doc | ^^^^^^^^^^^^^^^^^^^^ DAR401 | -DAR401.py:49:15: DAR401 Raised exception `FasterThanLightError` missing from docstring +DAR401_google.py:49:15: DAR401 Raised exception `FasterThanLightError` missing from docstring | 47 | return distance / time 48 | except ZeroDivisionError as exc: @@ -19,7 +19,7 @@ DAR401.py:49:15: DAR401 Raised exception `FasterThanLightError` missing from doc 51 | raise ValueError | -DAR401.py:51:15: DAR401 Raised exception `ValueError` missing from docstring +DAR401_google.py:51:15: DAR401 Raised exception `ValueError` missing from docstring | 49 | raise FasterThanLightError from exc 50 | except: diff --git a/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-missing-exception_DAR401_numpy.py.snap b/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-missing-exception_DAR401_numpy.py.snap new file mode 100644 index 0000000000000..c219ea4836732 --- /dev/null +++ b/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-missing-exception_DAR401_numpy.py.snap @@ -0,0 +1,28 @@ +--- +source: crates/ruff_linter/src/rules/darglint/mod.rs +--- +DAR401_numpy.py:47:15: DAR401 Raised exception `FasterThanLightError` missing from docstring + | +45 | return distance / time +46 | except ZeroDivisionError as exc: +47 | raise FasterThanLightError from exc + | ^^^^^^^^^^^^^^^^^^^^ DAR401 + | + +DAR401_numpy.py:69:15: DAR401 Raised exception `FasterThanLightError` missing from docstring + | +67 | return distance / time +68 | except ZeroDivisionError as exc: +69 | raise FasterThanLightError from exc + | ^^^^^^^^^^^^^^^^^^^^ DAR401 +70 | except: +71 | raise ValueError + | + +DAR401_numpy.py:71:15: DAR401 Raised exception `ValueError` missing from docstring + | +69 | raise FasterThanLightError from exc +70 | except: +71 | raise ValueError + | ^^^^^^^^^^ DAR401 + | From d61c659c225a9d4e5f2c6354c8a7ea03e0c5e590 Mon Sep 17 00:00:00 2001 From: augustelalande Date: Sun, 19 May 2024 16:41:06 -0400 Subject: [PATCH 03/24] guess style if unspecified --- .../rules/darglint/rules/check_docstring.rs | 52 +++++++++++++++++-- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/crates/ruff_linter/src/rules/darglint/rules/check_docstring.rs b/crates/ruff_linter/src/rules/darglint/rules/check_docstring.rs index a3ff154d29910..260f65abcb2f4 100644 --- a/crates/ruff_linter/src/rules/darglint/rules/check_docstring.rs +++ b/crates/ruff_linter/src/rules/darglint/rules/check_docstring.rs @@ -237,18 +237,60 @@ pub(crate) fn check_docstring( return; }; - let docstring_entries = match convention { + let docstring_entries; + match convention { Some(Convention::Google) => { let sections = SectionContexts::from_docstring(docstring, SectionStyle::Google); - DocstringEntries::new(§ions, SectionStyle::Google) + docstring_entries = DocstringEntries::new(§ions, SectionStyle::Google) } Some(Convention::Numpy) => { let sections = SectionContexts::from_docstring(docstring, SectionStyle::Numpy); - DocstringEntries::new(§ions, SectionStyle::Numpy) + docstring_entries = DocstringEntries::new(§ions, SectionStyle::Numpy) } - _ => { - return; + _ => 'unspecified: { + // 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 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 + ) + }) { + docstring_entries = DocstringEntries::new(&google_sections, SectionStyle::Google); + break 'unspecified; + } + + // 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 + ) + }) { + docstring_entries = DocstringEntries::new(&numpy_sections, SectionStyle::Numpy); + break 'unspecified; + } + + // Otherwise, use whichever convention matched more sections. + if google_sections.len() > numpy_sections.len() { + docstring_entries = DocstringEntries::new(&google_sections, SectionStyle::Google); + } else { + docstring_entries = DocstringEntries::new(&numpy_sections, SectionStyle::Numpy); + } } }; From a3c8a97540ab446b187e6daf568e96584aa313bf Mon Sep 17 00:00:00 2001 From: augustelalande Date: Sun, 19 May 2024 16:49:49 -0400 Subject: [PATCH 04/24] clippy --- .../rules/darglint/rules/check_docstring.rs | 31 ++++++++----------- 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/crates/ruff_linter/src/rules/darglint/rules/check_docstring.rs b/crates/ruff_linter/src/rules/darglint/rules/check_docstring.rs index 260f65abcb2f4..c65e16559cb33 100644 --- a/crates/ruff_linter/src/rules/darglint/rules/check_docstring.rs +++ b/crates/ruff_linter/src/rules/darglint/rules/check_docstring.rs @@ -137,12 +137,9 @@ impl DocstringEntries { let mut raised_exceptions_range = None; for section in sections.iter() { - match section.kind() { - SectionKind::Raises => { - raised_exceptions = parse_entries(section.following_lines_str(), style); - raised_exceptions_range = Some(section.range()); - } - _ => {} + if section.kind() == SectionKind::Raises { + raised_exceptions = parse_entries(section.following_lines_str(), style); + raised_exceptions_range = Some(section.range()); } } @@ -212,14 +209,12 @@ impl BodyEntries { impl Visitor<'_> for BodyEntries { fn visit_stmt(&mut self, stmt: &Stmt) { - if let Stmt::Raise(ast::StmtRaise { exc, .. }) = stmt { - if let Some(exc) = exc { - if let Expr::Name(ast::ExprName { id, range, .. }) = exc.as_ref() { - self.raised_exceptions.push(Entry { - id: id.to_string(), - range: *range, - }); - } + if let Stmt::Raise(ast::StmtRaise { exc: Some(exc), .. }) = stmt { + if let Expr::Name(ast::ExprName { id, range, .. }) = exc.as_ref() { + self.raised_exceptions.push(Entry { + id: id.to_string(), + range: *range, + }); } } visitor::walk_stmt(self, stmt); @@ -241,12 +236,12 @@ pub(crate) fn check_docstring( match convention { Some(Convention::Google) => { let sections = SectionContexts::from_docstring(docstring, SectionStyle::Google); - docstring_entries = DocstringEntries::new(§ions, SectionStyle::Google) + docstring_entries = DocstringEntries::new(§ions, SectionStyle::Google); } Some(Convention::Numpy) => { let sections = SectionContexts::from_docstring(docstring, SectionStyle::Numpy); - docstring_entries = DocstringEntries::new(§ions, SectionStyle::Numpy) + docstring_entries = DocstringEntries::new(§ions, SectionStyle::Numpy); } _ => 'unspecified: { // There are some overlapping section names, between the Google and NumPy conventions @@ -295,11 +290,11 @@ pub(crate) fn check_docstring( }; let mut body_entries = BodyEntries::new(); - visitor::walk_body(&mut body_entries, &member.body()); + visitor::walk_body(&mut body_entries, member.body()); // DAR401 if checker.enabled(Rule::DocstringMissingException) { - for body_raise in body_entries.raised_exceptions.iter() { + for body_raise in &body_entries.raised_exceptions { if !docstring_entries.raised_exceptions.contains(&body_raise.id) { let diagnostic = Diagnostic::new( DocstringMissingException { From 7a9ea530b95470d6a81e2568ad699671ca79df0e Mon Sep 17 00:00:00 2001 From: augustelalande Date: Sun, 19 May 2024 16:51:06 -0400 Subject: [PATCH 05/24] fix docs --- .../ruff_linter/src/rules/darglint/rules/check_docstring.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/ruff_linter/src/rules/darglint/rules/check_docstring.rs b/crates/ruff_linter/src/rules/darglint/rules/check_docstring.rs index c65e16559cb33..9bd7ca67aca19 100644 --- a/crates/ruff_linter/src/rules/darglint/rules/check_docstring.rs +++ b/crates/ruff_linter/src/rules/darglint/rules/check_docstring.rs @@ -94,7 +94,7 @@ impl Violation for DocstringMissingException { /// Raises: /// ZeroDivisionError: Divided by zero. /// """ -/// return distance / time +/// return distance / time /// ``` /// /// Use instead: @@ -109,7 +109,7 @@ impl Violation for DocstringMissingException { /// Returns: /// Speed as distance divided by time. /// """ -/// return distance / time +/// return distance / time /// ``` #[violation] pub struct DocstringExtraneousException { From 82015699889e9231fdb7e0a895eb8c9c5f5e0a58 Mon Sep 17 00:00:00 2001 From: augustelalande Date: Sun, 19 May 2024 17:00:43 -0400 Subject: [PATCH 06/24] bug --- crates/ruff_linter/src/rules/darglint/rules/check_docstring.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ruff_linter/src/rules/darglint/rules/check_docstring.rs b/crates/ruff_linter/src/rules/darglint/rules/check_docstring.rs index 9bd7ca67aca19..a3a6ad268a7d4 100644 --- a/crates/ruff_linter/src/rules/darglint/rules/check_docstring.rs +++ b/crates/ruff_linter/src/rules/darglint/rules/check_docstring.rs @@ -177,7 +177,7 @@ fn parse_entries_numpy(content: &str) -> Vec { }; let indentation = dashes.len() - dashes.trim_start().len(); for potential in split { - if let Some(first_char) = potential[indentation..].chars().next() { + if let Some(first_char) = potential.chars().nth(indentation) { if !first_char.is_whitespace() { let entry = potential[indentation..].trim().to_string(); entries.push(entry); From 228298d92268d4dc2ec138c8e68b2b9073385efe Mon Sep 17 00:00:00 2001 From: augustelalande Date: Sun, 19 May 2024 17:48:05 -0400 Subject: [PATCH 07/24] share section_contexts --- .../src/checkers/ast/analyze/definitions.rs | 37 ++++++----- crates/ruff_linter/src/docstrings/sections.rs | 6 ++ .../rules/darglint/rules/check_docstring.rs | 62 ++---------------- .../src/rules/pydocstyle/helpers.rs | 65 +++++++++++++++++++ .../src/rules/pydocstyle/rules/sections.rs | 65 ++----------------- 5 files changed, 105 insertions(+), 130 deletions(-) diff --git a/crates/ruff_linter/src/checkers/ast/analyze/definitions.rs b/crates/ruff_linter/src/checkers/ast/analyze/definitions.rs index 81324603122c3..bf8b81859b355 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/definitions.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/definitions.rs @@ -293,7 +293,8 @@ pub(crate) fn definitions(checker: &mut Checker) { if checker.enabled(Rule::OverloadWithDocstring) { pydocstyle::rules::if_needed(checker, &docstring); } - if checker.any_enabled(&[ + + let enforce_sections = checker.any_enabled(&[ Rule::BlankLineAfterLastSection, Rule::BlankLinesBetweenHeaderAndContent, Rule::CapitalizeSectionName, @@ -309,24 +310,30 @@ pub(crate) fn definitions(checker: &mut Checker) { Rule::SectionUnderlineMatchesSectionLength, Rule::SectionUnderlineNotOverIndented, Rule::UndocumentedParam, - ]) { - pydocstyle::rules::sections( - checker, + ]); + if enforce_sections || enforce_darglint { + let section_contexts = pydocstyle::helpers::get_section_contexts( &docstring, checker.settings.pydocstyle.convention.as_ref(), ); - } - if checker.any_enabled(&[ - Rule::DocstringMissingException, - Rule::DocstringExtraneousException, - ]) { - darglint::rules::check_docstring( - checker, - definition, - &docstring, - checker.settings.pydocstyle.convention.as_ref(), - ); + if enforce_sections { + pydocstyle::rules::sections( + checker, + &docstring, + §ion_contexts, + checker.settings.pydocstyle.convention.as_ref(), + ); + } + + if enforce_darglint { + darglint::rules::check_docstring( + checker, + definition, + §ion_contexts, + checker.settings.pydocstyle.convention.as_ref(), + ); + } } } } diff --git a/crates/ruff_linter/src/docstrings/sections.rs b/crates/ruff_linter/src/docstrings/sections.rs index fb9728cbbdfbf..1068a0db07535 100644 --- a/crates/ruff_linter/src/docstrings/sections.rs +++ b/crates/ruff_linter/src/docstrings/sections.rs @@ -163,6 +163,7 @@ impl SectionKind { pub(crate) struct SectionContexts<'a> { contexts: Vec, docstring: &'a Docstring<'a>, + style: SectionStyle, } impl<'a> SectionContexts<'a> { @@ -221,9 +222,14 @@ impl<'a> SectionContexts<'a> { Self { contexts, docstring, + style, } } + pub(crate) fn style(&self) -> SectionStyle { + self.style + } + pub(crate) fn len(&self) -> usize { self.contexts.len() } diff --git a/crates/ruff_linter/src/rules/darglint/rules/check_docstring.rs b/crates/ruff_linter/src/rules/darglint/rules/check_docstring.rs index a3a6ad268a7d4..d20d68140a3b7 100644 --- a/crates/ruff_linter/src/rules/darglint/rules/check_docstring.rs +++ b/crates/ruff_linter/src/rules/darglint/rules/check_docstring.rs @@ -9,7 +9,6 @@ use ruff_text_size::TextRange; use crate::checkers::ast::Checker; use crate::docstrings::sections::{SectionContexts, SectionKind}; use crate::docstrings::styles::SectionStyle; -use crate::docstrings::Docstring; use crate::registry::Rule; use crate::rules::pydocstyle::settings::Convention; @@ -225,68 +224,17 @@ impl Visitor<'_> for BodyEntries { pub(crate) fn check_docstring( checker: &mut Checker, definition: &Definition, - docstring: &Docstring, + section_contexts: &SectionContexts, convention: Option<&Convention>, ) { let Definition::Member(member) = definition else { return; }; - let docstring_entries; - match convention { - Some(Convention::Google) => { - let sections = SectionContexts::from_docstring(docstring, SectionStyle::Google); - docstring_entries = DocstringEntries::new(§ions, SectionStyle::Google); - } - - Some(Convention::Numpy) => { - let sections = SectionContexts::from_docstring(docstring, SectionStyle::Numpy); - docstring_entries = DocstringEntries::new(§ions, SectionStyle::Numpy); - } - _ => 'unspecified: { - // 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 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 - ) - }) { - docstring_entries = DocstringEntries::new(&google_sections, SectionStyle::Google); - break 'unspecified; - } - - // 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 - ) - }) { - docstring_entries = DocstringEntries::new(&numpy_sections, SectionStyle::Numpy); - break 'unspecified; - } - - // Otherwise, use whichever convention matched more sections. - if google_sections.len() > numpy_sections.len() { - docstring_entries = DocstringEntries::new(&google_sections, SectionStyle::Google); - } else { - docstring_entries = DocstringEntries::new(&numpy_sections, SectionStyle::Numpy); - } - } + let docstring_entries = match convention { + Some(Convention::Google) => DocstringEntries::new(§ion_contexts, SectionStyle::Google), + Some(Convention::Numpy) => DocstringEntries::new(§ion_contexts, SectionStyle::Numpy), + _ => DocstringEntries::new(§ion_contexts, section_contexts.style()), }; let mut body_entries = BodyEntries::new(); diff --git a/crates/ruff_linter/src/rules/pydocstyle/helpers.rs b/crates/ruff_linter/src/rules/pydocstyle/helpers.rs index 9ce0a757ac58b..49e960504b349 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/helpers.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/helpers.rs @@ -5,6 +5,11 @@ use ruff_python_ast::name::QualifiedName; use ruff_python_semantic::{Definition, SemanticModel}; 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. pub(super) fn logical_line(content: &str) -> Option { // Find the first logical line. @@ -61,3 +66,63 @@ pub(crate) fn should_ignore_definition( }) }) } + +pub(crate) fn get_section_contexts<'a>( + docstring: &'a Docstring<'a>, + convention: Option<&'a Convention>, +) -> SectionContexts<'a> { + let section_contexts; + match convention { + Some(Convention::Google) => { + section_contexts = SectionContexts::from_docstring(docstring, SectionStyle::Google); + } + Some(Convention::Numpy) => { + section_contexts = SectionContexts::from_docstring(docstring, SectionStyle::Numpy); + } + Some(Convention::Pep257) | None => 'unspecified: { + // 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 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 + ) + }) { + section_contexts = google_sections; + break 'unspecified; + } + + // 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 + ) + }) { + section_contexts = numpy_sections; + break 'unspecified; + } + + // Otherwise, use whichever convention matched more sections. + if google_sections.len() > numpy_sections.len() { + section_contexts = google_sections; + } else { + section_contexts = numpy_sections; + } + } + } + section_contexts +} diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/sections.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/sections.rs index b381fcca2bbe6..6077b1af61d05 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/sections.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/sections.rs @@ -1324,67 +1324,16 @@ impl AlwaysFixableViolation for BlankLinesBetweenHeaderAndContent { pub(crate) fn sections( checker: &mut Checker, docstring: &Docstring, + section_contexts: &SectionContexts, convention: Option<&Convention>, ) { match convention { - Some(Convention::Google) => { - parse_google_sections( - checker, - docstring, - &SectionContexts::from_docstring(docstring, SectionStyle::Google), - ); - } - 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); - } - } + Some(Convention::Google) => parse_google_sections(checker, docstring, §ion_contexts), + Some(Convention::Numpy) => parse_numpy_sections(checker, docstring, §ion_contexts), + Some(Convention::Pep257) | None => match section_contexts.style() { + SectionStyle::Google => parse_google_sections(checker, docstring, §ion_contexts), + SectionStyle::Numpy => parse_numpy_sections(checker, docstring, §ion_contexts), + }, } } From 52307441eaa03ae2cfa550ebe7bffe633c42eceb Mon Sep 17 00:00:00 2001 From: augustelalande Date: Sun, 19 May 2024 17:52:25 -0400 Subject: [PATCH 08/24] clippy --- .../src/rules/darglint/rules/check_docstring.rs | 6 +++--- crates/ruff_linter/src/rules/pydocstyle/rules/sections.rs | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/ruff_linter/src/rules/darglint/rules/check_docstring.rs b/crates/ruff_linter/src/rules/darglint/rules/check_docstring.rs index d20d68140a3b7..afcb108bdddba 100644 --- a/crates/ruff_linter/src/rules/darglint/rules/check_docstring.rs +++ b/crates/ruff_linter/src/rules/darglint/rules/check_docstring.rs @@ -232,9 +232,9 @@ pub(crate) fn check_docstring( }; let docstring_entries = match convention { - Some(Convention::Google) => DocstringEntries::new(§ion_contexts, SectionStyle::Google), - Some(Convention::Numpy) => DocstringEntries::new(§ion_contexts, SectionStyle::Numpy), - _ => DocstringEntries::new(§ion_contexts, section_contexts.style()), + Some(Convention::Google) => DocstringEntries::new(section_contexts, SectionStyle::Google), + Some(Convention::Numpy) => DocstringEntries::new(section_contexts, SectionStyle::Numpy), + _ => DocstringEntries::new(section_contexts, section_contexts.style()), }; let mut body_entries = BodyEntries::new(); diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/sections.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/sections.rs index 6077b1af61d05..6ec26fca2de84 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/sections.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/sections.rs @@ -1328,11 +1328,11 @@ pub(crate) fn sections( convention: Option<&Convention>, ) { match convention { - Some(Convention::Google) => parse_google_sections(checker, docstring, §ion_contexts), - Some(Convention::Numpy) => parse_numpy_sections(checker, docstring, §ion_contexts), + Some(Convention::Google) => parse_google_sections(checker, docstring, section_contexts), + Some(Convention::Numpy) => parse_numpy_sections(checker, docstring, section_contexts), Some(Convention::Pep257) | None => match section_contexts.style() { - SectionStyle::Google => parse_google_sections(checker, docstring, §ion_contexts), - SectionStyle::Numpy => parse_numpy_sections(checker, docstring, §ion_contexts), + SectionStyle::Google => parse_google_sections(checker, docstring, section_contexts), + SectionStyle::Numpy => parse_numpy_sections(checker, docstring, section_contexts), }, } } From ebd102b9425f7c396a1a03af6de9bc9d04d68fec Mon Sep 17 00:00:00 2001 From: augustelalande Date: Sun, 19 May 2024 18:05:40 -0400 Subject: [PATCH 09/24] exclude classes --- .../test/fixtures/darglint/DAR401_google.py | 16 +++++++++++ .../rules/darglint/rules/check_docstring.rs | 13 ++++++++- ...ng-missing-exception_DAR401_google.py.snap | 28 +++++++++---------- 3 files changed, 42 insertions(+), 15 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/darglint/DAR401_google.py b/crates/ruff_linter/resources/test/fixtures/darglint/DAR401_google.py index a3dde20c59ecd..41e73bd84ef00 100644 --- a/crates/ruff_linter/resources/test/fixtures/darglint/DAR401_google.py +++ b/crates/ruff_linter/resources/test/fixtures/darglint/DAR401_google.py @@ -1,3 +1,4 @@ +# DAR401 def calculate_speed(distance: float, time: float) -> float: """Calculate speed as distance divided by time. @@ -17,6 +18,7 @@ def calculate_speed(distance: float, time: float) -> float: raise FasterThanLightError from exc +# DAR401 def calculate_speed(distance: float, time: float) -> float: """Calculate speed as distance divided by time. @@ -33,6 +35,7 @@ def calculate_speed(distance: float, time: float) -> float: raise FasterThanLightError from exc +# DAR401 def calculate_speed(distance: float, time: float) -> float: """Calculate speed as distance divided by time. @@ -49,3 +52,16 @@ def calculate_speed(distance: float, time: float) -> float: raise FasterThanLightError from exc except: raise ValueError + + +# 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 diff --git a/crates/ruff_linter/src/rules/darglint/rules/check_docstring.rs b/crates/ruff_linter/src/rules/darglint/rules/check_docstring.rs index afcb108bdddba..db3785c96e691 100644 --- a/crates/ruff_linter/src/rules/darglint/rules/check_docstring.rs +++ b/crates/ruff_linter/src/rules/darglint/rules/check_docstring.rs @@ -3,7 +3,7 @@ use ruff_diagnostics::Violation; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::visitor::{self, Visitor}; use ruff_python_ast::{self as ast, Expr, Stmt}; -use ruff_python_semantic::Definition; +use ruff_python_semantic::{Definition, MemberKind}; use ruff_text_size::TextRange; use crate::checkers::ast::Checker; @@ -231,6 +231,13 @@ pub(crate) fn check_docstring( return; }; + if matches!( + member.kind, + MemberKind::Class(_) | MemberKind::NestedClass(_) + ) { + return; + } + let docstring_entries = match convention { Some(Convention::Google) => DocstringEntries::new(section_contexts, SectionStyle::Google), Some(Convention::Numpy) => DocstringEntries::new(section_contexts, SectionStyle::Numpy), @@ -243,6 +250,10 @@ pub(crate) fn check_docstring( // DAR401 if checker.enabled(Rule::DocstringMissingException) { for body_raise in &body_entries.raised_exceptions { + if body_raise.id == "NotImplementedError" { + continue; + } + if !docstring_entries.raised_exceptions.contains(&body_raise.id) { let diagnostic = Diagnostic::new( DocstringMissingException { diff --git a/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-missing-exception_DAR401_google.py.snap b/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-missing-exception_DAR401_google.py.snap index b0130fae3873b..d631667aaa88e 100644 --- a/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-missing-exception_DAR401_google.py.snap +++ b/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-missing-exception_DAR401_google.py.snap @@ -1,28 +1,28 @@ --- source: crates/ruff_linter/src/rules/darglint/mod.rs --- -DAR401_google.py:33:15: DAR401 Raised exception `FasterThanLightError` missing from docstring +DAR401_google.py:35:15: DAR401 Raised exception `FasterThanLightError` missing from docstring | -31 | return distance / time -32 | except ZeroDivisionError as exc: -33 | raise FasterThanLightError from exc +33 | return distance / time +34 | except ZeroDivisionError as exc: +35 | raise FasterThanLightError from exc | ^^^^^^^^^^^^^^^^^^^^ DAR401 | -DAR401_google.py:49:15: DAR401 Raised exception `FasterThanLightError` missing from docstring +DAR401_google.py:52:15: DAR401 Raised exception `FasterThanLightError` missing from docstring | -47 | return distance / time -48 | except ZeroDivisionError as exc: -49 | raise FasterThanLightError from exc +50 | return distance / time +51 | except ZeroDivisionError as exc: +52 | raise FasterThanLightError from exc | ^^^^^^^^^^^^^^^^^^^^ DAR401 -50 | except: -51 | raise ValueError +53 | except: +54 | raise ValueError | -DAR401_google.py:51:15: DAR401 Raised exception `ValueError` missing from docstring +DAR401_google.py:54:15: DAR401 Raised exception `ValueError` missing from docstring | -49 | raise FasterThanLightError from exc -50 | except: -51 | raise ValueError +52 | raise FasterThanLightError from exc +53 | except: +54 | raise ValueError | ^^^^^^^^^^ DAR401 | From cdb1604bb803fd33ee234b029f110e7bbdb0ab40 Mon Sep 17 00:00:00 2001 From: augustelalande Date: Sun, 19 May 2024 18:43:34 -0400 Subject: [PATCH 10/24] order was intentional --- .../test/fixtures/darglint/DAR401_google.py | 36 +++++++++++++++ .../rules/darglint/rules/check_docstring.rs | 16 ++++++- ...issing-exception_DAR401_google.py.snap.new | 45 +++++++++++++++++++ .../src/rules/pydocstyle/helpers.rs | 30 ++++++------- 4 files changed, 111 insertions(+), 16 deletions(-) create mode 100644 crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-missing-exception_DAR401_google.py.snap.new diff --git a/crates/ruff_linter/resources/test/fixtures/darglint/DAR401_google.py b/crates/ruff_linter/resources/test/fixtures/darglint/DAR401_google.py index 41e73bd84ef00..26b2c883f6718 100644 --- a/crates/ruff_linter/resources/test/fixtures/darglint/DAR401_google.py +++ b/crates/ruff_linter/resources/test/fixtures/darglint/DAR401_google.py @@ -54,6 +54,42 @@ def calculate_speed(distance: float, time: float) -> float: raise ValueError +# DAR401 +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 + + +# DAR401 +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 + + # OK def calculate_speed(distance: float, time: float) -> float: try: diff --git a/crates/ruff_linter/src/rules/darglint/rules/check_docstring.rs b/crates/ruff_linter/src/rules/darglint/rules/check_docstring.rs index db3785c96e691..6d36f76497bc1 100644 --- a/crates/ruff_linter/src/rules/darglint/rules/check_docstring.rs +++ b/crates/ruff_linter/src/rules/darglint/rules/check_docstring.rs @@ -1,8 +1,10 @@ +use std::collections::HashMap; + use ruff_diagnostics::Diagnostic; use ruff_diagnostics::Violation; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::visitor::{self, Visitor}; -use ruff_python_ast::{self as ast, Expr, Stmt}; +use ruff_python_ast::{self as ast, ExceptHandler, Expr, Stmt}; use ruff_python_semantic::{Definition, MemberKind}; use ruff_text_size::TextRange; @@ -218,6 +220,18 @@ impl Visitor<'_> for BodyEntries { } visitor::walk_stmt(self, stmt); } + + fn visit_except_handler(&mut self, except_handler: &ExceptHandler) { + if let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { + type_: Some(type_), + name: Some(name), + .. + }) = except_handler + { + println!("{:?} {:?}", type_, name.id); + } + visitor::walk_except_handler(self, except_handler); + } } /// DAR401, DAR402 diff --git a/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-missing-exception_DAR401_google.py.snap.new b/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-missing-exception_DAR401_google.py.snap.new new file mode 100644 index 0000000000000..3d2704fa05f7a --- /dev/null +++ b/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-missing-exception_DAR401_google.py.snap.new @@ -0,0 +1,45 @@ +--- +source: crates/ruff_linter/src/rules/darglint/mod.rs +assertion_line: 33 +--- +DAR401_google.py:35:15: DAR401 Raised exception `FasterThanLightError` missing from docstring + | +33 | return distance / time +34 | except ZeroDivisionError as exc: +35 | raise FasterThanLightError from exc + | ^^^^^^^^^^^^^^^^^^^^ DAR401 + | + +DAR401_google.py:52:15: DAR401 Raised exception `FasterThanLightError` missing from docstring + | +50 | return distance / time +51 | except ZeroDivisionError as exc: +52 | raise FasterThanLightError from exc + | ^^^^^^^^^^^^^^^^^^^^ DAR401 +53 | except: +54 | raise ValueError + | + +DAR401_google.py:54:15: DAR401 Raised exception `ValueError` missing from docstring + | +52 | raise FasterThanLightError from exc +53 | except: +54 | raise ValueError + | ^^^^^^^^^^ DAR401 + | + +DAR401_google.py:72:15: DAR401 Raised exception `exc` missing from docstring + | +70 | except ZeroDivisionError as exc: +71 | print('oops') +72 | raise exc + | ^^^ DAR401 + | + +DAR401_google.py:90:15: DAR401 Raised exception `exc` missing from docstring + | +88 | except (ZeroDivisionError, ValueError) as exc: +89 | print('oops') +90 | raise exc + | ^^^ DAR401 + | diff --git a/crates/ruff_linter/src/rules/pydocstyle/helpers.rs b/crates/ruff_linter/src/rules/pydocstyle/helpers.rs index 49e960504b349..7097f2e97edc2 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/helpers.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/helpers.rs @@ -84,6 +84,21 @@ pub(crate) fn get_section_contexts<'a>( // (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 + ) + }) { + section_contexts = numpy_sections; + break 'unspecified; + } + // 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| { @@ -101,21 +116,6 @@ pub(crate) fn get_section_contexts<'a>( break 'unspecified; } - // 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 - ) - }) { - section_contexts = numpy_sections; - break 'unspecified; - } - // Otherwise, use whichever convention matched more sections. if google_sections.len() > numpy_sections.len() { section_contexts = google_sections; From 7a3c637b9c063cd962ba9175f20a8eec73ac8019 Mon Sep 17 00:00:00 2001 From: augustelalande Date: Sun, 19 May 2024 18:52:05 -0400 Subject: [PATCH 11/24] ignore variable exceptions for now --- .../rules/darglint/rules/check_docstring.rs | 18 +++++--- ...issing-exception_DAR401_google.py.snap.new | 45 ------------------- 2 files changed, 11 insertions(+), 52 deletions(-) delete mode 100644 crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-missing-exception_DAR401_google.py.snap.new diff --git a/crates/ruff_linter/src/rules/darglint/rules/check_docstring.rs b/crates/ruff_linter/src/rules/darglint/rules/check_docstring.rs index 6d36f76497bc1..6ca81358fc8af 100644 --- a/crates/ruff_linter/src/rules/darglint/rules/check_docstring.rs +++ b/crates/ruff_linter/src/rules/darglint/rules/check_docstring.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::collections::HashSet; use ruff_diagnostics::Diagnostic; use ruff_diagnostics::Violation; @@ -198,12 +198,14 @@ struct Entry { #[derive(Debug)] struct BodyEntries { raised_exceptions: Vec, + named_exceptions: HashSet, } impl BodyEntries { fn new() -> Self { Self { raised_exceptions: Vec::new(), + named_exceptions: HashSet::new(), } } } @@ -212,10 +214,13 @@ impl Visitor<'_> for BodyEntries { fn visit_stmt(&mut self, stmt: &Stmt) { if let Stmt::Raise(ast::StmtRaise { exc: Some(exc), .. }) = stmt { if let Expr::Name(ast::ExprName { id, range, .. }) = exc.as_ref() { - self.raised_exceptions.push(Entry { - id: id.to_string(), - range: *range, - }); + // Skip variable exceptions for now + if !self.named_exceptions.contains(id) { + self.raised_exceptions.push(Entry { + id: id.to_string(), + range: *range, + }); + } } } visitor::walk_stmt(self, stmt); @@ -223,12 +228,11 @@ impl Visitor<'_> for BodyEntries { fn visit_except_handler(&mut self, except_handler: &ExceptHandler) { if let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { - type_: Some(type_), name: Some(name), .. }) = except_handler { - println!("{:?} {:?}", type_, name.id); + self.named_exceptions.insert(name.id.to_string()); } visitor::walk_except_handler(self, except_handler); } diff --git a/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-missing-exception_DAR401_google.py.snap.new b/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-missing-exception_DAR401_google.py.snap.new deleted file mode 100644 index 3d2704fa05f7a..0000000000000 --- a/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-missing-exception_DAR401_google.py.snap.new +++ /dev/null @@ -1,45 +0,0 @@ ---- -source: crates/ruff_linter/src/rules/darglint/mod.rs -assertion_line: 33 ---- -DAR401_google.py:35:15: DAR401 Raised exception `FasterThanLightError` missing from docstring - | -33 | return distance / time -34 | except ZeroDivisionError as exc: -35 | raise FasterThanLightError from exc - | ^^^^^^^^^^^^^^^^^^^^ DAR401 - | - -DAR401_google.py:52:15: DAR401 Raised exception `FasterThanLightError` missing from docstring - | -50 | return distance / time -51 | except ZeroDivisionError as exc: -52 | raise FasterThanLightError from exc - | ^^^^^^^^^^^^^^^^^^^^ DAR401 -53 | except: -54 | raise ValueError - | - -DAR401_google.py:54:15: DAR401 Raised exception `ValueError` missing from docstring - | -52 | raise FasterThanLightError from exc -53 | except: -54 | raise ValueError - | ^^^^^^^^^^ DAR401 - | - -DAR401_google.py:72:15: DAR401 Raised exception `exc` missing from docstring - | -70 | except ZeroDivisionError as exc: -71 | print('oops') -72 | raise exc - | ^^^ DAR401 - | - -DAR401_google.py:90:15: DAR401 Raised exception `exc` missing from docstring - | -88 | except (ZeroDivisionError, ValueError) as exc: -89 | print('oops') -90 | raise exc - | ^^^ DAR401 - | From 596bf416905d1721d89ce317a83287428df6c950 Mon Sep 17 00:00:00 2001 From: augustelalande Date: Sun, 19 May 2024 20:32:31 -0400 Subject: [PATCH 12/24] ignore variable exceptions --- .../test/fixtures/darglint/DAR401_google.py | 38 +++++++++++ .../test/fixtures/darglint/DAR401_numpy.py | 4 ++ .../test/fixtures/darglint/DAR402_google.py | 4 ++ .../test/fixtures/darglint/DAR402_numpy.py | 4 ++ .../rules/darglint/rules/check_docstring.rs | 47 +++++++------- ...extraneous-exception_DAR402_google.py.snap | 48 +++++++------- ...-extraneous-exception_DAR402_numpy.py.snap | 64 +++++++++---------- ...ng-missing-exception_DAR401_google.py.snap | 36 +++++++---- ...ing-missing-exception_DAR401_numpy.py.snap | 28 ++++---- 9 files changed, 165 insertions(+), 108 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/darglint/DAR401_google.py b/crates/ruff_linter/resources/test/fixtures/darglint/DAR401_google.py index 26b2c883f6718..cf8bd5698fe60 100644 --- a/crates/ruff_linter/resources/test/fixtures/darglint/DAR401_google.py +++ b/crates/ruff_linter/resources/test/fixtures/darglint/DAR401_google.py @@ -1,3 +1,13 @@ +from somewhere import AnotherError + + +class FasterThanLightError(Exception): + ... + + +_some_error = Exception + + # DAR401 def calculate_speed(distance: float, time: float) -> float: """Calculate speed as distance divided by time. @@ -90,6 +100,34 @@ def calculate_speed(distance: float, time: float) -> float: raise exc +# DAR401 +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 + + +# DAR401, 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: diff --git a/crates/ruff_linter/resources/test/fixtures/darglint/DAR401_numpy.py b/crates/ruff_linter/resources/test/fixtures/darglint/DAR401_numpy.py index 05befb273a94a..81f28211e3585 100644 --- a/crates/ruff_linter/resources/test/fixtures/darglint/DAR401_numpy.py +++ b/crates/ruff_linter/resources/test/fixtures/darglint/DAR401_numpy.py @@ -1,3 +1,7 @@ +class FasterThanLightError(Exception): + ... + + def calculate_speed(distance: float, time: float) -> float: """ Calculate speed as distance divided by time. diff --git a/crates/ruff_linter/resources/test/fixtures/darglint/DAR402_google.py b/crates/ruff_linter/resources/test/fixtures/darglint/DAR402_google.py index d6a6254f51a88..a4af696c6e0e8 100644 --- a/crates/ruff_linter/resources/test/fixtures/darglint/DAR402_google.py +++ b/crates/ruff_linter/resources/test/fixtures/darglint/DAR402_google.py @@ -1,3 +1,7 @@ +class FasterThanLightError(Exception): + ... + + def calculate_speed(distance: float, time: float) -> float: """Calculate speed as distance divided by time. diff --git a/crates/ruff_linter/resources/test/fixtures/darglint/DAR402_numpy.py b/crates/ruff_linter/resources/test/fixtures/darglint/DAR402_numpy.py index 28859fbf0cba5..7cb75a7dd6053 100644 --- a/crates/ruff_linter/resources/test/fixtures/darglint/DAR402_numpy.py +++ b/crates/ruff_linter/resources/test/fixtures/darglint/DAR402_numpy.py @@ -1,3 +1,7 @@ +class FasterThanLightError(Exception): + ... + + def calculate_speed(distance: float, time: float) -> float: """ Calculate speed as distance divided by time. diff --git a/crates/ruff_linter/src/rules/darglint/rules/check_docstring.rs b/crates/ruff_linter/src/rules/darglint/rules/check_docstring.rs index 6ca81358fc8af..8376bf35c5a0c 100644 --- a/crates/ruff_linter/src/rules/darglint/rules/check_docstring.rs +++ b/crates/ruff_linter/src/rules/darglint/rules/check_docstring.rs @@ -1,10 +1,9 @@ -use std::collections::HashSet; - use ruff_diagnostics::Diagnostic; use ruff_diagnostics::Violation; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::visitor::{self, Visitor}; -use ruff_python_ast::{self as ast, ExceptHandler, Expr, Stmt}; +use ruff_python_ast::{self as ast, Expr, Stmt}; +use ruff_python_semantic::SemanticModel; use ruff_python_semantic::{Definition, MemberKind}; use ruff_text_size::TextRange; @@ -195,27 +194,37 @@ struct Entry { range: TextRange, } -#[derive(Debug)] struct BodyEntries { raised_exceptions: Vec, - named_exceptions: HashSet, } -impl BodyEntries { - fn new() -> Self { +struct BodyVisitor<'a> { + raised_exceptions: Vec, + semantic: &'a SemanticModel<'a>, +} + +impl<'a> BodyVisitor<'a> { + fn new(semantic: &'a SemanticModel<'a>) -> Self { Self { raised_exceptions: Vec::new(), - named_exceptions: HashSet::new(), + semantic, + } + } + + fn finish(self) -> BodyEntries { + BodyEntries { + raised_exceptions: self.raised_exceptions, } } } -impl Visitor<'_> for BodyEntries { +impl Visitor<'_> for BodyVisitor<'_> { fn visit_stmt(&mut self, stmt: &Stmt) { if let Stmt::Raise(ast::StmtRaise { exc: Some(exc), .. }) = stmt { if let Expr::Name(ast::ExprName { id, range, .. }) = exc.as_ref() { - // Skip variable exceptions for now - if !self.named_exceptions.contains(id) { + // SemanticModel will resolve qualified_name for local Class definitions, + // or imported definitions, but not variables which we want to ignore. + if self.semantic.resolve_qualified_name(exc.as_ref()).is_some() { self.raised_exceptions.push(Entry { id: id.to_string(), range: *range, @@ -225,17 +234,6 @@ impl Visitor<'_> for BodyEntries { } visitor::walk_stmt(self, stmt); } - - fn visit_except_handler(&mut self, except_handler: &ExceptHandler) { - if let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { - name: Some(name), - .. - }) = except_handler - { - self.named_exceptions.insert(name.id.to_string()); - } - visitor::walk_except_handler(self, except_handler); - } } /// DAR401, DAR402 @@ -262,8 +260,9 @@ pub(crate) fn check_docstring( _ => DocstringEntries::new(section_contexts, section_contexts.style()), }; - let mut body_entries = BodyEntries::new(); - visitor::walk_body(&mut body_entries, member.body()); + let mut visitor = BodyVisitor::new(checker.semantic()); + visitor::walk_body(&mut visitor, member.body()); + let body_entries = visitor.finish(); // DAR401 if checker.enabled(Rule::DocstringMissingException) { diff --git a/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-extraneous-exception_DAR402_google.py.snap b/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-extraneous-exception_DAR402_google.py.snap index 3c6fc1c265687..830b2f9a15c20 100644 --- a/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-extraneous-exception_DAR402_google.py.snap +++ b/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-extraneous-exception_DAR402_google.py.snap @@ -1,38 +1,38 @@ --- source: crates/ruff_linter/src/rules/darglint/mod.rs --- -DAR402_google.py:11:1: DAR402 FasterThanLightError not explicitly raised. +DAR402_google.py:15:1: DAR402 FasterThanLightError not explicitly raised. | - 9 | Speed as distance divided by time. -10 | -11 | / Raises: -12 | | FasterThanLightError: If speed is greater than the speed of light. -13 | | """ +13 | Speed as distance divided by time. +14 | +15 | / Raises: +16 | | FasterThanLightError: If speed is greater than the speed of light. +17 | | """ | |____^ DAR402 -14 | return distance / time +18 | return distance / time | -DAR402_google.py:27:1: DAR402 FasterThanLightError, DivisionByZero not explicitly raised. +DAR402_google.py:31:1: DAR402 FasterThanLightError, DivisionByZero not explicitly raised. | -25 | Speed as distance divided by time. -26 | -27 | / Raises: -28 | | FasterThanLightError: If speed is greater than the speed of light. -29 | | DivisionByZero: Divide by zero. -30 | | """ +29 | Speed as distance divided by time. +30 | +31 | / Raises: +32 | | FasterThanLightError: If speed is greater than the speed of light. +33 | | DivisionByZero: Divide by zero. +34 | | """ | |____^ DAR402 -31 | return distance / time +35 | return distance / time | -DAR402_google.py:44:1: DAR402 DivisionByZero not explicitly raised. +DAR402_google.py:48:1: DAR402 DivisionByZero not explicitly raised. | -42 | Speed as distance divided by time. -43 | -44 | / Raises: -45 | | FasterThanLightError: If speed is greater than the speed of light. -46 | | DivisionByZero: Divide by zero. -47 | | """ +46 | Speed as distance divided by time. +47 | +48 | / Raises: +49 | | FasterThanLightError: If speed is greater than the speed of light. +50 | | DivisionByZero: Divide by zero. +51 | | """ | |____^ DAR402 -48 | try: -49 | return distance / time +52 | try: +53 | return distance / time | diff --git a/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-extraneous-exception_DAR402_numpy.py.snap b/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-extraneous-exception_DAR402_numpy.py.snap index e4fbe295878b6..5cf4f8089f890 100644 --- a/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-extraneous-exception_DAR402_numpy.py.snap +++ b/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-extraneous-exception_DAR402_numpy.py.snap @@ -1,46 +1,46 @@ --- source: crates/ruff_linter/src/rules/darglint/mod.rs --- -DAR402_numpy.py:17:1: DAR402 FasterThanLightError not explicitly raised. +DAR402_numpy.py:21:1: DAR402 FasterThanLightError not explicitly raised. | -15 | Speed as distance divided by time. -16 | -17 | / Raises -18 | | ------ -19 | | FasterThanLightError -20 | | If speed is greater than the speed of light. -21 | | """ +19 | Speed as distance divided by time. +20 | +21 | / Raises +22 | | ------ +23 | | FasterThanLightError +24 | | If speed is greater than the speed of light. +25 | | """ | |____^ DAR402 -22 | return distance / time +26 | return distance / time | -DAR402_numpy.py:41:1: DAR402 FasterThanLightError, DivisionByZero not explicitly raised. +DAR402_numpy.py:45:1: DAR402 FasterThanLightError, DivisionByZero not explicitly raised. | -39 | Speed as distance divided by time. -40 | -41 | / Raises -42 | | ------ -43 | | FasterThanLightError -44 | | If speed is greater than the speed of light. -45 | | DivisionByZero -46 | | If attempting to divide by zero. -47 | | """ +43 | Speed as distance divided by time. +44 | +45 | / Raises +46 | | ------ +47 | | FasterThanLightError +48 | | If speed is greater than the speed of light. +49 | | DivisionByZero +50 | | If attempting to divide by zero. +51 | | """ | |____^ DAR402 -48 | return distance / time +52 | return distance / time | -DAR402_numpy.py:67:1: DAR402 DivisionByZero not explicitly raised. +DAR402_numpy.py:71:1: DAR402 DivisionByZero not explicitly raised. | -65 | Speed as distance divided by time. -66 | -67 | / Raises -68 | | ------ -69 | | FasterThanLightError -70 | | If speed is greater than the speed of light. -71 | | DivisionByZero -72 | | If attempting to divide by zero. -73 | | """ +69 | Speed as distance divided by time. +70 | +71 | / Raises +72 | | ------ +73 | | FasterThanLightError +74 | | If speed is greater than the speed of light. +75 | | DivisionByZero +76 | | If attempting to divide by zero. +77 | | """ | |____^ DAR402 -74 | try: -75 | return distance / time +78 | try: +79 | return distance / time | diff --git a/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-missing-exception_DAR401_google.py.snap b/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-missing-exception_DAR401_google.py.snap index d631667aaa88e..fd3a6be24519d 100644 --- a/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-missing-exception_DAR401_google.py.snap +++ b/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-missing-exception_DAR401_google.py.snap @@ -1,28 +1,36 @@ --- source: crates/ruff_linter/src/rules/darglint/mod.rs --- -DAR401_google.py:35:15: DAR401 Raised exception `FasterThanLightError` missing from docstring +DAR401_google.py:45:15: DAR401 Raised exception `FasterThanLightError` missing from docstring | -33 | return distance / time -34 | except ZeroDivisionError as exc: -35 | raise FasterThanLightError from exc +43 | return distance / time +44 | except ZeroDivisionError as exc: +45 | raise FasterThanLightError from exc | ^^^^^^^^^^^^^^^^^^^^ DAR401 | -DAR401_google.py:52:15: DAR401 Raised exception `FasterThanLightError` missing from docstring +DAR401_google.py:62:15: DAR401 Raised exception `FasterThanLightError` missing from docstring | -50 | return distance / time -51 | except ZeroDivisionError as exc: -52 | raise FasterThanLightError from exc +60 | return distance / time +61 | except ZeroDivisionError as exc: +62 | raise FasterThanLightError from exc | ^^^^^^^^^^^^^^^^^^^^ DAR401 -53 | except: -54 | raise ValueError +63 | except: +64 | raise ValueError | -DAR401_google.py:54:15: DAR401 Raised exception `ValueError` missing from docstring +DAR401_google.py:64:15: DAR401 Raised exception `ValueError` missing from docstring | -52 | raise FasterThanLightError from exc -53 | except: -54 | raise ValueError +62 | raise FasterThanLightError from exc +63 | except: +64 | raise ValueError | ^^^^^^^^^^ DAR401 | + +DAR401_google.py:114:11: DAR401 Raised exception `AnotherError` missing from docstring + | +112 | Speed as distance divided by time. +113 | """ +114 | raise AnotherError + | ^^^^^^^^^^^^ DAR401 + | diff --git a/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-missing-exception_DAR401_numpy.py.snap b/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-missing-exception_DAR401_numpy.py.snap index c219ea4836732..88117860cd4d5 100644 --- a/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-missing-exception_DAR401_numpy.py.snap +++ b/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-missing-exception_DAR401_numpy.py.snap @@ -1,28 +1,28 @@ --- source: crates/ruff_linter/src/rules/darglint/mod.rs --- -DAR401_numpy.py:47:15: DAR401 Raised exception `FasterThanLightError` missing from docstring +DAR401_numpy.py:51:15: DAR401 Raised exception `FasterThanLightError` missing from docstring | -45 | return distance / time -46 | except ZeroDivisionError as exc: -47 | raise FasterThanLightError from exc +49 | return distance / time +50 | except ZeroDivisionError as exc: +51 | raise FasterThanLightError from exc | ^^^^^^^^^^^^^^^^^^^^ DAR401 | -DAR401_numpy.py:69:15: DAR401 Raised exception `FasterThanLightError` missing from docstring +DAR401_numpy.py:73:15: DAR401 Raised exception `FasterThanLightError` missing from docstring | -67 | return distance / time -68 | except ZeroDivisionError as exc: -69 | raise FasterThanLightError from exc +71 | return distance / time +72 | except ZeroDivisionError as exc: +73 | raise FasterThanLightError from exc | ^^^^^^^^^^^^^^^^^^^^ DAR401 -70 | except: -71 | raise ValueError +74 | except: +75 | raise ValueError | -DAR401_numpy.py:71:15: DAR401 Raised exception `ValueError` missing from docstring +DAR401_numpy.py:75:15: DAR401 Raised exception `ValueError` missing from docstring | -69 | raise FasterThanLightError from exc -70 | except: -71 | raise ValueError +73 | raise FasterThanLightError from exc +74 | except: +75 | raise ValueError | ^^^^^^^^^^ DAR401 | From 3413e598ba3606b6895c60d4fc537361c7578eb2 Mon Sep 17 00:00:00 2001 From: augustelalande Date: Sun, 19 May 2024 20:56:32 -0400 Subject: [PATCH 13/24] handle exception calls, i.e., Exception() --- .../test/fixtures/darglint/DAR401_google.py | 14 +++++++ .../rules/darglint/rules/check_docstring.rs | 37 ++++++++++++++----- ...ng-missing-exception_DAR401_google.py.snap | 8 ++++ 3 files changed, 50 insertions(+), 9 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/darglint/DAR401_google.py b/crates/ruff_linter/resources/test/fixtures/darglint/DAR401_google.py index cf8bd5698fe60..68e7fb7c07eff 100644 --- a/crates/ruff_linter/resources/test/fixtures/darglint/DAR401_google.py +++ b/crates/ruff_linter/resources/test/fixtures/darglint/DAR401_google.py @@ -114,6 +114,20 @@ def calculate_speed(distance: float, time: float) -> float: raise AnotherError +# DAR401 +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() + + # DAR401, but can't resolve the error def calculate_speed(distance: float, time: float) -> float: """Calculate speed as distance divided by time. diff --git a/crates/ruff_linter/src/rules/darglint/rules/check_docstring.rs b/crates/ruff_linter/src/rules/darglint/rules/check_docstring.rs index 8376bf35c5a0c..3419e0265a565 100644 --- a/crates/ruff_linter/src/rules/darglint/rules/check_docstring.rs +++ b/crates/ruff_linter/src/rules/darglint/rules/check_docstring.rs @@ -221,16 +221,35 @@ impl<'a> BodyVisitor<'a> { impl Visitor<'_> for BodyVisitor<'_> { fn visit_stmt(&mut self, stmt: &Stmt) { if let Stmt::Raise(ast::StmtRaise { exc: Some(exc), .. }) = stmt { - if let Expr::Name(ast::ExprName { id, range, .. }) = exc.as_ref() { - // SemanticModel will resolve qualified_name for local Class definitions, - // or imported definitions, but not variables which we want to ignore. - if self.semantic.resolve_qualified_name(exc.as_ref()).is_some() { - self.raised_exceptions.push(Entry { - id: id.to_string(), - range: *range, - }); + match exc.as_ref() { + Expr::Name(ast::ExprName { id, range, .. }) => { + // SemanticModel will resolve qualified_name for local Class definitions, + // or imported definitions, but not variables which we want to ignore. + if self.semantic.resolve_qualified_name(exc.as_ref()).is_some() { + self.raised_exceptions.push(Entry { + id: id.to_string(), + range: *range, + }); + } } - } + Expr::Call(ast::ExprCall { func, range, .. }) => { + if let Expr::Name(ast::ExprName { id, .. }) = func.as_ref() { + // SemanticModel will resolve qualified_name for local Class definitions, + // or imported definitions, but not variables which we want to ignore. + if self + .semantic + .resolve_qualified_name(func.as_ref()) + .is_some() + { + self.raised_exceptions.push(Entry { + id: id.to_string(), + range: *range, + }); + } + } + } + _ => {} + }; } visitor::walk_stmt(self, stmt); } diff --git a/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-missing-exception_DAR401_google.py.snap b/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-missing-exception_DAR401_google.py.snap index fd3a6be24519d..0b033680b0903 100644 --- a/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-missing-exception_DAR401_google.py.snap +++ b/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-missing-exception_DAR401_google.py.snap @@ -34,3 +34,11 @@ DAR401_google.py:114:11: DAR401 Raised exception `AnotherError` missing from doc 114 | raise AnotherError | ^^^^^^^^^^^^ DAR401 | + +DAR401_google.py:128:11: DAR401 Raised exception `AnotherError` missing from docstring + | +126 | Speed as distance divided by time. +127 | """ +128 | raise AnotherError() + | ^^^^^^^^^^^^^^ DAR401 + | From 4688d066e2359465726959938075e32a12c6435c Mon Sep 17 00:00:00 2001 From: augustelalande Date: Thu, 18 Jul 2024 11:16:17 -0400 Subject: [PATCH 14/24] recode to pydoclint --- .../DOC501_google.py} | 16 +++---- .../DOC501_numpy.py} | 3 ++ .../DOC502_google.py} | 3 ++ .../DOC502_numpy.py} | 3 ++ .../src/checkers/ast/analyze/definitions.rs | 16 +++---- crates/ruff_linter/src/codes.rs | 6 +-- crates/ruff_linter/src/registry.rs | 6 +-- ...extraneous-exception_DAR402_google.py.snap | 38 --------------- ...-extraneous-exception_DAR402_numpy.py.snap | 46 ------------------- ...ing-missing-exception_DAR401_numpy.py.snap | 28 ----------- crates/ruff_linter/src/rules/mod.rs | 2 +- .../src/rules/{darglint => pydoclint}/mod.rs | 14 +++--- .../rules/check_docstring.rs | 6 +-- .../{darglint => pydoclint}/rules/mod.rs | 0 ...extraneous-exception_DOC502_google.py.snap | 38 +++++++++++++++ ...-extraneous-exception_DOC502_numpy.py.snap | 46 +++++++++++++++++++ ...g-missing-exception_DOC501_google.py.snap} | 22 ++++----- ...ing-missing-exception_DOC501_numpy.py.snap | 28 +++++++++++ ruff.schema.json | 10 ++-- 19 files changed, 170 insertions(+), 161 deletions(-) rename crates/ruff_linter/resources/test/fixtures/{darglint/DAR401_google.py => pydoclint/DOC501_google.py} (97%) rename crates/ruff_linter/resources/test/fixtures/{darglint/DAR401_numpy.py => pydoclint/DOC501_numpy.py} (98%) rename crates/ruff_linter/resources/test/fixtures/{darglint/DAR402_google.py => pydoclint/DOC502_google.py} (98%) rename crates/ruff_linter/resources/test/fixtures/{darglint/DAR402_numpy.py => pydoclint/DOC502_numpy.py} (98%) delete mode 100644 crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-extraneous-exception_DAR402_google.py.snap delete mode 100644 crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-extraneous-exception_DAR402_numpy.py.snap delete mode 100644 crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-missing-exception_DAR401_numpy.py.snap rename crates/ruff_linter/src/rules/{darglint => pydoclint}/mod.rs (83%) rename crates/ruff_linter/src/rules/{darglint => pydoclint}/rules/check_docstring.rs (99%) rename crates/ruff_linter/src/rules/{darglint => pydoclint}/rules/mod.rs (100%) create mode 100644 crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-extraneous-exception_DOC502_google.py.snap create mode 100644 crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-extraneous-exception_DOC502_numpy.py.snap rename crates/ruff_linter/src/rules/{darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-missing-exception_DAR401_google.py.snap => pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-missing-exception_DOC501_google.py.snap} (57%) create mode 100644 crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-missing-exception_DOC501_numpy.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/darglint/DAR401_google.py b/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC501_google.py similarity index 97% rename from crates/ruff_linter/resources/test/fixtures/darglint/DAR401_google.py rename to crates/ruff_linter/resources/test/fixtures/pydoclint/DOC501_google.py index 68e7fb7c07eff..84dc6e4ad26b6 100644 --- a/crates/ruff_linter/resources/test/fixtures/darglint/DAR401_google.py +++ b/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC501_google.py @@ -8,7 +8,7 @@ class FasterThanLightError(Exception): _some_error = Exception -# DAR401 +# DOC501 def calculate_speed(distance: float, time: float) -> float: """Calculate speed as distance divided by time. @@ -28,7 +28,7 @@ def calculate_speed(distance: float, time: float) -> float: raise FasterThanLightError from exc -# DAR401 +# DOC501 def calculate_speed(distance: float, time: float) -> float: """Calculate speed as distance divided by time. @@ -45,7 +45,7 @@ def calculate_speed(distance: float, time: float) -> float: raise FasterThanLightError from exc -# DAR401 +# DOC501 def calculate_speed(distance: float, time: float) -> float: """Calculate speed as distance divided by time. @@ -64,7 +64,7 @@ def calculate_speed(distance: float, time: float) -> float: raise ValueError -# DAR401 +# DOC501 def calculate_speed(distance: float, time: float) -> float: """Calculate speed as distance divided by time. @@ -82,7 +82,7 @@ def calculate_speed(distance: float, time: float) -> float: raise exc -# DAR401 +# DOC501 def calculate_speed(distance: float, time: float) -> float: """Calculate speed as distance divided by time. @@ -100,7 +100,7 @@ def calculate_speed(distance: float, time: float) -> float: raise exc -# DAR401 +# DOC501 def calculate_speed(distance: float, time: float) -> float: """Calculate speed as distance divided by time. @@ -114,7 +114,7 @@ def calculate_speed(distance: float, time: float) -> float: raise AnotherError -# DAR401 +# DOC501 def calculate_speed(distance: float, time: float) -> float: """Calculate speed as distance divided by time. @@ -128,7 +128,7 @@ def calculate_speed(distance: float, time: float) -> float: raise AnotherError() -# DAR401, but can't resolve the error +# DOC501, but can't resolve the error def calculate_speed(distance: float, time: float) -> float: """Calculate speed as distance divided by time. diff --git a/crates/ruff_linter/resources/test/fixtures/darglint/DAR401_numpy.py b/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC501_numpy.py similarity index 98% rename from crates/ruff_linter/resources/test/fixtures/darglint/DAR401_numpy.py rename to crates/ruff_linter/resources/test/fixtures/pydoclint/DOC501_numpy.py index 81f28211e3585..fb949a4672de6 100644 --- a/crates/ruff_linter/resources/test/fixtures/darglint/DAR401_numpy.py +++ b/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC501_numpy.py @@ -2,6 +2,7 @@ class FasterThanLightError(Exception): ... +# DOC501 def calculate_speed(distance: float, time: float) -> float: """ Calculate speed as distance divided by time. @@ -29,6 +30,7 @@ def calculate_speed(distance: float, time: float) -> float: raise FasterThanLightError from exc +# DOC501 def calculate_speed(distance: float, time: float) -> float: """ Calculate speed as distance divided by time. @@ -51,6 +53,7 @@ def calculate_speed(distance: float, time: float) -> float: raise FasterThanLightError from exc +# DOC501 def calculate_speed(distance: float, time: float) -> float: """ Calculate speed as distance divided by time. diff --git a/crates/ruff_linter/resources/test/fixtures/darglint/DAR402_google.py b/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC502_google.py similarity index 98% rename from crates/ruff_linter/resources/test/fixtures/darglint/DAR402_google.py rename to crates/ruff_linter/resources/test/fixtures/pydoclint/DOC502_google.py index a4af696c6e0e8..639a7965134f7 100644 --- a/crates/ruff_linter/resources/test/fixtures/darglint/DAR402_google.py +++ b/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC502_google.py @@ -2,6 +2,7 @@ class FasterThanLightError(Exception): ... +# DOC502 def calculate_speed(distance: float, time: float) -> float: """Calculate speed as distance divided by time. @@ -18,6 +19,7 @@ def calculate_speed(distance: float, time: float) -> float: return distance / time +# DOC502 def calculate_speed(distance: float, time: float) -> float: """Calculate speed as distance divided by time. @@ -35,6 +37,7 @@ def calculate_speed(distance: float, time: float) -> float: return distance / time +# DOC502 def calculate_speed(distance: float, time: float) -> float: """Calculate speed as distance divided by time. diff --git a/crates/ruff_linter/resources/test/fixtures/darglint/DAR402_numpy.py b/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC502_numpy.py similarity index 98% rename from crates/ruff_linter/resources/test/fixtures/darglint/DAR402_numpy.py rename to crates/ruff_linter/resources/test/fixtures/pydoclint/DOC502_numpy.py index 7cb75a7dd6053..95b84e813495c 100644 --- a/crates/ruff_linter/resources/test/fixtures/darglint/DAR402_numpy.py +++ b/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC502_numpy.py @@ -2,6 +2,7 @@ class FasterThanLightError(Exception): ... +# DOC502 def calculate_speed(distance: float, time: float) -> float: """ Calculate speed as distance divided by time. @@ -26,6 +27,7 @@ def calculate_speed(distance: float, time: float) -> float: return distance / time +# DOC502 def calculate_speed(distance: float, time: float) -> float: """ Calculate speed as distance divided by time. @@ -52,6 +54,7 @@ def calculate_speed(distance: float, time: float) -> float: return distance / time +# DOC502 def calculate_speed(distance: float, time: float) -> float: """ Calculate speed as distance divided by time. diff --git a/crates/ruff_linter/src/checkers/ast/analyze/definitions.rs b/crates/ruff_linter/src/checkers/ast/analyze/definitions.rs index bf8b81859b355..f25d091ce7da4 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/definitions.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/definitions.rs @@ -10,7 +10,7 @@ use crate::checkers::ast::Checker; use crate::codes::Rule; use crate::docstrings::Docstring; use crate::fs::relativize_path; -use crate::rules::{darglint, flake8_annotations, flake8_pyi, pydocstyle, pylint}; +use crate::rules::{flake8_annotations, flake8_pyi, pydoclint, pydocstyle, pylint}; use crate::{docstrings, warn_user}; /// Run lint rules over all [`Definition`] nodes in the [`SemanticModel`]. @@ -83,7 +83,7 @@ pub(crate) fn definitions(checker: &mut Checker) { Rule::UndocumentedPublicNestedClass, Rule::UndocumentedPublicPackage, ]); - let enforce_darglint = checker.any_enabled(&[ + let enforce_pydoclint = checker.any_enabled(&[ Rule::DocstringMissingException, Rule::DocstringExtraneousException, ]); @@ -93,7 +93,7 @@ pub(crate) fn definitions(checker: &mut Checker) { && !enforce_stubs && !enforce_stubs_and_runtime && !enforce_dunder_method - && !enforce_darglint + && !enforce_pydoclint { return; } @@ -174,8 +174,8 @@ pub(crate) fn definitions(checker: &mut Checker) { } } - // pydocstyle, darglint - if enforce_docstrings || enforce_darglint { + // pydocstyle, pydoclint + if enforce_docstrings || enforce_pydoclint { if pydocstyle::helpers::should_ignore_definition( definition, &checker.settings.pydocstyle.ignore_decorators, @@ -311,7 +311,7 @@ pub(crate) fn definitions(checker: &mut Checker) { Rule::SectionUnderlineNotOverIndented, Rule::UndocumentedParam, ]); - if enforce_sections || enforce_darglint { + if enforce_sections || enforce_pydoclint { let section_contexts = pydocstyle::helpers::get_section_contexts( &docstring, checker.settings.pydocstyle.convention.as_ref(), @@ -326,8 +326,8 @@ pub(crate) fn definitions(checker: &mut Checker) { ); } - if enforce_darglint { - darglint::rules::check_docstring( + if enforce_pydoclint { + pydoclint::rules::check_docstring( checker, definition, §ion_contexts, diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index 9550526a1fa72..d6c3ca256ba40 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -942,9 +942,9 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Numpy, "003") => (RuleGroup::Stable, rules::numpy::rules::NumpyDeprecatedFunction), (Numpy, "201") => (RuleGroup::Stable, rules::numpy::rules::Numpy2Deprecation), - // darglint - (Darglint, "401") => (RuleGroup::Preview, rules::darglint::rules::DocstringMissingException), - (Darglint, "402") => (RuleGroup::Preview, rules::darglint::rules::DocstringExtraneousException), + // pydoclint + (Pydoclint, "501") => (RuleGroup::Preview, rules::pydoclint::rules::DocstringMissingException), + (Pydoclint, "502") => (RuleGroup::Preview, rules::pydoclint::rules::DocstringExtraneousException), // ruff (Ruff, "001") => (RuleGroup::Stable, rules::ruff::rules::AmbiguousUnicodeCharacterString), diff --git a/crates/ruff_linter/src/registry.rs b/crates/ruff_linter/src/registry.rs index 03431281b275c..fbc797b078598 100644 --- a/crates/ruff_linter/src/registry.rs +++ b/crates/ruff_linter/src/registry.rs @@ -205,9 +205,9 @@ pub enum Linter { /// [refurb](https://pypi.org/project/refurb/) #[prefix = "FURB"] Refurb, - /// [darglint](https://pypi.org/project/darglint/) - #[prefix = "DAR"] - Darglint, + /// [pydoclint](https://pypi.org/project/pydoclint/) + #[prefix = "DOC"] + Pydoclint, /// Ruff-specific rules #[prefix = "RUF"] Ruff, diff --git a/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-extraneous-exception_DAR402_google.py.snap b/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-extraneous-exception_DAR402_google.py.snap deleted file mode 100644 index 830b2f9a15c20..0000000000000 --- a/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-extraneous-exception_DAR402_google.py.snap +++ /dev/null @@ -1,38 +0,0 @@ ---- -source: crates/ruff_linter/src/rules/darglint/mod.rs ---- -DAR402_google.py:15:1: DAR402 FasterThanLightError not explicitly raised. - | -13 | Speed as distance divided by time. -14 | -15 | / Raises: -16 | | FasterThanLightError: If speed is greater than the speed of light. -17 | | """ - | |____^ DAR402 -18 | return distance / time - | - -DAR402_google.py:31:1: DAR402 FasterThanLightError, DivisionByZero not explicitly raised. - | -29 | Speed as distance divided by time. -30 | -31 | / Raises: -32 | | FasterThanLightError: If speed is greater than the speed of light. -33 | | DivisionByZero: Divide by zero. -34 | | """ - | |____^ DAR402 -35 | return distance / time - | - -DAR402_google.py:48:1: DAR402 DivisionByZero not explicitly raised. - | -46 | Speed as distance divided by time. -47 | -48 | / Raises: -49 | | FasterThanLightError: If speed is greater than the speed of light. -50 | | DivisionByZero: Divide by zero. -51 | | """ - | |____^ DAR402 -52 | try: -53 | return distance / time - | diff --git a/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-extraneous-exception_DAR402_numpy.py.snap b/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-extraneous-exception_DAR402_numpy.py.snap deleted file mode 100644 index 5cf4f8089f890..0000000000000 --- a/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-extraneous-exception_DAR402_numpy.py.snap +++ /dev/null @@ -1,46 +0,0 @@ ---- -source: crates/ruff_linter/src/rules/darglint/mod.rs ---- -DAR402_numpy.py:21:1: DAR402 FasterThanLightError not explicitly raised. - | -19 | Speed as distance divided by time. -20 | -21 | / Raises -22 | | ------ -23 | | FasterThanLightError -24 | | If speed is greater than the speed of light. -25 | | """ - | |____^ DAR402 -26 | return distance / time - | - -DAR402_numpy.py:45:1: DAR402 FasterThanLightError, DivisionByZero not explicitly raised. - | -43 | Speed as distance divided by time. -44 | -45 | / Raises -46 | | ------ -47 | | FasterThanLightError -48 | | If speed is greater than the speed of light. -49 | | DivisionByZero -50 | | If attempting to divide by zero. -51 | | """ - | |____^ DAR402 -52 | return distance / time - | - -DAR402_numpy.py:71:1: DAR402 DivisionByZero not explicitly raised. - | -69 | Speed as distance divided by time. -70 | -71 | / Raises -72 | | ------ -73 | | FasterThanLightError -74 | | If speed is greater than the speed of light. -75 | | DivisionByZero -76 | | If attempting to divide by zero. -77 | | """ - | |____^ DAR402 -78 | try: -79 | return distance / time - | diff --git a/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-missing-exception_DAR401_numpy.py.snap b/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-missing-exception_DAR401_numpy.py.snap deleted file mode 100644 index 88117860cd4d5..0000000000000 --- a/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-missing-exception_DAR401_numpy.py.snap +++ /dev/null @@ -1,28 +0,0 @@ ---- -source: crates/ruff_linter/src/rules/darglint/mod.rs ---- -DAR401_numpy.py:51:15: DAR401 Raised exception `FasterThanLightError` missing from docstring - | -49 | return distance / time -50 | except ZeroDivisionError as exc: -51 | raise FasterThanLightError from exc - | ^^^^^^^^^^^^^^^^^^^^ DAR401 - | - -DAR401_numpy.py:73:15: DAR401 Raised exception `FasterThanLightError` missing from docstring - | -71 | return distance / time -72 | except ZeroDivisionError as exc: -73 | raise FasterThanLightError from exc - | ^^^^^^^^^^^^^^^^^^^^ DAR401 -74 | except: -75 | raise ValueError - | - -DAR401_numpy.py:75:15: DAR401 Raised exception `ValueError` missing from docstring - | -73 | raise FasterThanLightError from exc -74 | except: -75 | raise ValueError - | ^^^^^^^^^^ DAR401 - | diff --git a/crates/ruff_linter/src/rules/mod.rs b/crates/ruff_linter/src/rules/mod.rs index c80fa0789224c..b263fcb4aa604 100644 --- a/crates/ruff_linter/src/rules/mod.rs +++ b/crates/ruff_linter/src/rules/mod.rs @@ -1,6 +1,5 @@ #![allow(clippy::useless_format)] pub mod airflow; -pub mod darglint; pub mod eradicate; pub mod flake8_2020; pub mod flake8_annotations; @@ -50,6 +49,7 @@ pub mod pandas_vet; pub mod pep8_naming; pub mod perflint; pub mod pycodestyle; +pub mod pydoclint; pub mod pydocstyle; pub mod pyflakes; pub mod pygrep_hooks; diff --git a/crates/ruff_linter/src/rules/darglint/mod.rs b/crates/ruff_linter/src/rules/pydoclint/mod.rs similarity index 83% rename from crates/ruff_linter/src/rules/darglint/mod.rs rename to crates/ruff_linter/src/rules/pydoclint/mod.rs index 8eb45b3a88f52..539f310b91e51 100644 --- a/crates/ruff_linter/src/rules/darglint/mod.rs +++ b/crates/ruff_linter/src/rules/pydoclint/mod.rs @@ -1,4 +1,4 @@ -//! Rules from [darglint](https://pypi.org/project/darglint/). +//! Rules from [pydoclint](https://pypi.org/project/pydoclint/). pub(crate) mod rules; #[cfg(test)] @@ -15,12 +15,12 @@ mod tests { use crate::test::test_path; use crate::{assert_messages, settings}; - #[test_case(Rule::DocstringMissingException, Path::new("DAR401_google.py"))] - #[test_case(Rule::DocstringExtraneousException, Path::new("DAR402_google.py"))] + #[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("darglint").join(path).as_path(), + Path::new("pydoclint").join(path).as_path(), &settings::LinterSettings { pydocstyle: Settings { convention: Some(Convention::Google), @@ -34,12 +34,12 @@ mod tests { Ok(()) } - #[test_case(Rule::DocstringMissingException, Path::new("DAR401_numpy.py"))] - #[test_case(Rule::DocstringExtraneousException, Path::new("DAR402_numpy.py"))] + #[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("darglint").join(path).as_path(), + Path::new("pydoclint").join(path).as_path(), &settings::LinterSettings { pydocstyle: Settings { convention: Some(Convention::Numpy), diff --git a/crates/ruff_linter/src/rules/darglint/rules/check_docstring.rs b/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs similarity index 99% rename from crates/ruff_linter/src/rules/darglint/rules/check_docstring.rs rename to crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs index 3419e0265a565..3c44cc94418f9 100644 --- a/crates/ruff_linter/src/rules/darglint/rules/check_docstring.rs +++ b/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs @@ -255,7 +255,7 @@ impl Visitor<'_> for BodyVisitor<'_> { } } -/// DAR401, DAR402 +/// DOC501, DOC502 pub(crate) fn check_docstring( checker: &mut Checker, definition: &Definition, @@ -283,7 +283,7 @@ pub(crate) fn check_docstring( visitor::walk_body(&mut visitor, member.body()); let body_entries = visitor.finish(); - // DAR401 + // DOC501 if checker.enabled(Rule::DocstringMissingException) { for body_raise in &body_entries.raised_exceptions { if body_raise.id == "NotImplementedError" { @@ -302,7 +302,7 @@ pub(crate) fn check_docstring( } } - // DAR402 + // DOC502 if checker.enabled(Rule::DocstringExtraneousException) { let mut extraneous_exceptions = Vec::new(); for docstring_raise in docstring_entries.raised_exceptions { diff --git a/crates/ruff_linter/src/rules/darglint/rules/mod.rs b/crates/ruff_linter/src/rules/pydoclint/rules/mod.rs similarity index 100% rename from crates/ruff_linter/src/rules/darglint/rules/mod.rs rename to crates/ruff_linter/src/rules/pydoclint/rules/mod.rs diff --git a/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-extraneous-exception_DOC502_google.py.snap b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-extraneous-exception_DOC502_google.py.snap new file mode 100644 index 0000000000000..bf38b07b34339 --- /dev/null +++ b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-extraneous-exception_DOC502_google.py.snap @@ -0,0 +1,38 @@ +--- +source: crates/ruff_linter/src/rules/pydoclint/mod.rs +--- +DOC502_google.py:16:1: DOC502 FasterThanLightError not explicitly raised. + | +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 FasterThanLightError, DivisionByZero not explicitly raised. + | +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 DivisionByZero not explicitly raised. + | +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 + | diff --git a/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-extraneous-exception_DOC502_numpy.py.snap b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-extraneous-exception_DOC502_numpy.py.snap new file mode 100644 index 0000000000000..a8e7f98cc67b7 --- /dev/null +++ b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-extraneous-exception_DOC502_numpy.py.snap @@ -0,0 +1,46 @@ +--- +source: crates/ruff_linter/src/rules/pydoclint/mod.rs +--- +DOC502_numpy.py:22:1: DOC502 FasterThanLightError not explicitly raised. + | +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 FasterThanLightError, DivisionByZero not explicitly raised. + | +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 DivisionByZero not explicitly raised. + | +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 + | diff --git a/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-missing-exception_DAR401_google.py.snap b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-missing-exception_DOC501_google.py.snap similarity index 57% rename from crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-missing-exception_DAR401_google.py.snap rename to crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-missing-exception_DOC501_google.py.snap index 0b033680b0903..c7a2fe47cbb4c 100644 --- a/crates/ruff_linter/src/rules/darglint/snapshots/ruff_linter__rules__darglint__tests__docstring-missing-exception_DAR401_google.py.snap +++ b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-missing-exception_DOC501_google.py.snap @@ -1,44 +1,44 @@ --- -source: crates/ruff_linter/src/rules/darglint/mod.rs +source: crates/ruff_linter/src/rules/pydoclint/mod.rs --- -DAR401_google.py:45:15: DAR401 Raised exception `FasterThanLightError` missing from docstring +DOC501_google.py:45:15: DOC501 Raised exception `FasterThanLightError` missing from docstring | 43 | return distance / time 44 | except ZeroDivisionError as exc: 45 | raise FasterThanLightError from exc - | ^^^^^^^^^^^^^^^^^^^^ DAR401 + | ^^^^^^^^^^^^^^^^^^^^ DOC501 | -DAR401_google.py:62:15: DAR401 Raised exception `FasterThanLightError` missing from docstring +DOC501_google.py:62:15: DOC501 Raised exception `FasterThanLightError` missing from docstring | 60 | return distance / time 61 | except ZeroDivisionError as exc: 62 | raise FasterThanLightError from exc - | ^^^^^^^^^^^^^^^^^^^^ DAR401 + | ^^^^^^^^^^^^^^^^^^^^ DOC501 63 | except: 64 | raise ValueError | -DAR401_google.py:64:15: DAR401 Raised exception `ValueError` missing from docstring +DOC501_google.py:64:15: DOC501 Raised exception `ValueError` missing from docstring | 62 | raise FasterThanLightError from exc 63 | except: 64 | raise ValueError - | ^^^^^^^^^^ DAR401 + | ^^^^^^^^^^ DOC501 | -DAR401_google.py:114:11: DAR401 Raised exception `AnotherError` missing from docstring +DOC501_google.py:114:11: DOC501 Raised exception `AnotherError` missing from docstring | 112 | Speed as distance divided by time. 113 | """ 114 | raise AnotherError - | ^^^^^^^^^^^^ DAR401 + | ^^^^^^^^^^^^ DOC501 | -DAR401_google.py:128:11: DAR401 Raised exception `AnotherError` missing from docstring +DOC501_google.py:128:11: DOC501 Raised exception `AnotherError` missing from docstring | 126 | Speed as distance divided by time. 127 | """ 128 | raise AnotherError() - | ^^^^^^^^^^^^^^ DAR401 + | ^^^^^^^^^^^^^^ DOC501 | diff --git a/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-missing-exception_DOC501_numpy.py.snap b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-missing-exception_DOC501_numpy.py.snap new file mode 100644 index 0000000000000..f91ec86eb3b1b --- /dev/null +++ b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-missing-exception_DOC501_numpy.py.snap @@ -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 + | diff --git a/ruff.schema.json b/ruff.schema.json index 105b6af2d055d..a63f6413622d7 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2825,11 +2825,6 @@ "D417", "D418", "D419", - "DAR", - "DAR4", - "DAR40", - "DAR401", - "DAR402", "DJ", "DJ0", "DJ00", @@ -2841,6 +2836,11 @@ "DJ01", "DJ012", "DJ013", + "DOC", + "DOC5", + "DOC50", + "DOC501", + "DOC502", "DTZ", "DTZ0", "DTZ00", From 269d3b6800d1c1dd2218582dc8c4716abf52b60c Mon Sep 17 00:00:00 2001 From: augustelalande Date: Thu, 18 Jul 2024 11:23:34 -0400 Subject: [PATCH 15/24] add license --- LICENSE | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/LICENSE b/LICENSE index 04ed9285de85a..f5c3b02beccb0 100644 --- a/LICENSE +++ b/LICENSE @@ -1371,3 +1371,28 @@ are: OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 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. + """ From 1f62e07a440618b8dce4b77c19a012ec1ba104e1 Mon Sep 17 00:00:00 2001 From: augustelalande Date: Thu, 18 Jul 2024 22:21:44 -0400 Subject: [PATCH 16/24] return section_context directly --- .../src/rules/pydocstyle/helpers.rs | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/crates/ruff_linter/src/rules/pydocstyle/helpers.rs b/crates/ruff_linter/src/rules/pydocstyle/helpers.rs index 7097f2e97edc2..6754fdd3948b6 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/helpers.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/helpers.rs @@ -71,15 +71,14 @@ pub(crate) fn get_section_contexts<'a>( docstring: &'a Docstring<'a>, convention: Option<&'a Convention>, ) -> SectionContexts<'a> { - let section_contexts; match convention { Some(Convention::Google) => { - section_contexts = SectionContexts::from_docstring(docstring, SectionStyle::Google); + return SectionContexts::from_docstring(docstring, SectionStyle::Google); } Some(Convention::Numpy) => { - section_contexts = SectionContexts::from_docstring(docstring, SectionStyle::Numpy); + return SectionContexts::from_docstring(docstring, SectionStyle::Numpy); } - Some(Convention::Pep257) | None => 'unspecified: { + 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. @@ -95,8 +94,7 @@ pub(crate) fn get_section_contexts<'a>( | SectionKind::OtherParameters ) }) { - section_contexts = numpy_sections; - break 'unspecified; + return numpy_sections; } // If the docstring contains any argument specifier, use the Google convention. @@ -112,17 +110,15 @@ pub(crate) fn get_section_contexts<'a>( | SectionKind::OtherArguments ) }) { - section_contexts = google_sections; - break 'unspecified; + return google_sections; } // Otherwise, use whichever convention matched more sections. if google_sections.len() > numpy_sections.len() { - section_contexts = google_sections; + return google_sections; } else { - section_contexts = numpy_sections; + return numpy_sections; } } } - section_contexts } From c9bff03663bc499b89b3e4d446bcc238b57822df Mon Sep 17 00:00:00 2001 From: augustelalande Date: Thu, 18 Jul 2024 22:31:00 -0400 Subject: [PATCH 17/24] document section styles --- .../src/rules/pydoclint/rules/check_docstring.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs b/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs index 3c44cc94418f9..8f9d1103a009a 100644 --- a/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs +++ b/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs @@ -150,6 +150,7 @@ impl DocstringEntries { } } +// Parses docstring sections of supported styles. fn parse_entries(content: &str, style: SectionStyle) -> Vec { match style { SectionStyle::Google => parse_entries_google(content), @@ -157,6 +158,12 @@ fn parse_entries(content: &str, style: SectionStyle) -> Vec { } } +// Parses google style docstring sections of the form: +// +// 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 { let mut entries: Vec = Vec::new(); for potential in content.split('\n') { @@ -169,6 +176,15 @@ fn parse_entries_google(content: &str) -> Vec { entries } +// Parses numpy style docstring sections of the form: +// +// 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 { let mut entries: Vec = Vec::new(); let mut split = content.split('\n'); From b9da62dc2372a3d819268496665875709f8646d5 Mon Sep 17 00:00:00 2001 From: augustelalande Date: Thu, 18 Jul 2024 22:53:21 -0400 Subject: [PATCH 18/24] clippy --- crates/ruff_linter/src/rules/pydocstyle/helpers.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/ruff_linter/src/rules/pydocstyle/helpers.rs b/crates/ruff_linter/src/rules/pydocstyle/helpers.rs index 6754fdd3948b6..3b78c003c9b8d 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/helpers.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/helpers.rs @@ -115,9 +115,9 @@ pub(crate) fn get_section_contexts<'a>( // Otherwise, use whichever convention matched more sections. if google_sections.len() > numpy_sections.len() { - return google_sections; + google_sections } else { - return numpy_sections; + numpy_sections } } } From 1705b53e9257f49e08c92ead772614863c555417 Mon Sep 17 00:00:00 2001 From: augustelalande Date: Thu, 18 Jul 2024 23:16:36 -0400 Subject: [PATCH 19/24] document --- crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs b/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs index 8f9d1103a009a..470984ac507ac 100644 --- a/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs +++ b/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs @@ -282,6 +282,7 @@ pub(crate) fn check_docstring( return; }; + // Only check function docstrings. if matches!( member.kind, MemberKind::Class(_) | MemberKind::NestedClass(_) @@ -289,6 +290,7 @@ pub(crate) fn check_docstring( return; } + // Prioritize the specified convention over the determined style. let docstring_entries = match convention { Some(Convention::Google) => DocstringEntries::new(section_contexts, SectionStyle::Google), Some(Convention::Numpy) => DocstringEntries::new(section_contexts, SectionStyle::Numpy), From cb5e037383db5819eb68becb667ef871f3620e87 Mon Sep 17 00:00:00 2001 From: augustelalande Date: Fri, 19 Jul 2024 01:46:39 -0400 Subject: [PATCH 20/24] allow different variations of the qualified name in the docstring --- .../rules/pydoclint/rules/check_docstring.rs | 136 +++++++++--------- 1 file changed, 69 insertions(+), 67 deletions(-) diff --git a/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs b/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs index 470984ac507ac..81d300fd70838 100644 --- a/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs +++ b/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs @@ -1,11 +1,11 @@ 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::SemanticModel; -use ruff_python_semantic::{Definition, MemberKind}; -use ruff_text_size::TextRange; +use ruff_python_semantic::{Definition, MemberKind, SemanticModel}; +use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; use crate::docstrings::sections::{SectionContexts, SectionKind}; @@ -126,14 +126,14 @@ impl Violation for DocstringExtraneousException { // Parse docstring #[derive(Debug)] -struct DocstringEntries { - raised_exceptions: Vec, +struct DocstringEntries<'a> { + raised_exceptions: Vec>, raised_exceptions_range: Option, } -impl DocstringEntries { - fn new(sections: &SectionContexts, style: SectionStyle) -> Self { - let mut raised_exceptions: Vec = Vec::new(); +impl<'a> DocstringEntries<'a> { + fn new(sections: &'a SectionContexts, style: SectionStyle) -> Self { + let mut raised_exceptions: Vec = Vec::new(); let mut raised_exceptions_range = None; for section in sections.iter() { @@ -151,7 +151,7 @@ impl DocstringEntries { } // Parses docstring sections of supported styles. -fn parse_entries(content: &str, style: SectionStyle) -> Vec { +fn parse_entries<'a>(content: &'a str, style: SectionStyle) -> Vec> { match style { SectionStyle::Google => parse_entries_google(content), SectionStyle::Numpy => parse_entries_numpy(content), @@ -164,14 +164,14 @@ fn parse_entries(content: &str, style: SectionStyle) -> Vec { // FasterThanLightError: If speed is greater than the speed of light. // DivisionByZero: If attempting to divide by zero. // -fn parse_entries_google(content: &str) -> Vec { - let mut entries: Vec = Vec::new(); +fn parse_entries_google(content: &str) -> Vec { + let mut entries: Vec = Vec::new(); for potential in content.split('\n') { let Some(colon_idx) = potential.find(':') else { continue; }; - let entry = potential[..colon_idx].trim().to_string(); - entries.push(entry); + let entry = potential[..colon_idx].trim(); + entries.push(QualifiedName::user_defined(entry)); } entries } @@ -185,8 +185,8 @@ fn parse_entries_google(content: &str) -> Vec { // DivisionByZero // If attempting to divide by zero. // -fn parse_entries_numpy(content: &str) -> Vec { - let mut entries: Vec = Vec::new(); +fn parse_entries_numpy(content: &str) -> Vec { + let mut entries: Vec = Vec::new(); let mut split = content.split('\n'); let Some(dashes) = split.next() else { return entries; @@ -195,8 +195,8 @@ fn parse_entries_numpy(content: &str) -> Vec { for potential in split { if let Some(first_char) = potential.chars().nth(indentation) { if !first_char.is_whitespace() { - let entry = potential[indentation..].trim().to_string(); - entries.push(entry); + let entry = potential[indentation..].trim(); + entries.push(QualifiedName::user_defined(entry)); } } } @@ -205,72 +205,62 @@ fn parse_entries_numpy(content: &str) -> Vec { // Parse body #[derive(Debug)] -struct Entry { - id: String, +struct Entry<'a> { + qualified_name: QualifiedName<'a>, range: TextRange, } -struct BodyEntries { - raised_exceptions: Vec, +struct BodyEntries<'a> { + raised_exceptions: Vec>, } struct BodyVisitor<'a> { - raised_exceptions: Vec, + raised_exceptions: Vec>, semantic: &'a SemanticModel<'a>, } impl<'a> BodyVisitor<'a> { - fn new(semantic: &'a SemanticModel<'a>) -> Self { + fn new(semantic: &'a SemanticModel) -> Self { Self { raised_exceptions: Vec::new(), semantic, } } - fn finish(self) -> BodyEntries { + fn finish(self) -> BodyEntries<'a> { BodyEntries { raised_exceptions: self.raised_exceptions, } } } -impl Visitor<'_> for BodyVisitor<'_> { - fn visit_stmt(&mut self, stmt: &Stmt) { +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 { - match exc.as_ref() { - Expr::Name(ast::ExprName { id, range, .. }) => { - // SemanticModel will resolve qualified_name for local Class definitions, - // or imported definitions, but not variables which we want to ignore. - if self.semantic.resolve_qualified_name(exc.as_ref()).is_some() { - self.raised_exceptions.push(Entry { - id: id.to_string(), - range: *range, - }); - } - } - Expr::Call(ast::ExprCall { func, range, .. }) => { - if let Expr::Name(ast::ExprName { id, .. }) = func.as_ref() { - // SemanticModel will resolve qualified_name for local Class definitions, - // or imported definitions, but not variables which we want to ignore. - if self - .semantic - .resolve_qualified_name(func.as_ref()) - .is_some() - { - self.raised_exceptions.push(Entry { - id: id.to_string(), - range: *range, - }); - } - } - } - _ => {} - }; + 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> { + 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, @@ -278,6 +268,7 @@ pub(crate) fn check_docstring( section_contexts: &SectionContexts, convention: Option<&Convention>, ) { + let mut diagnostics = Vec::new(); let Definition::Member(member) = definition else { return; }; @@ -304,18 +295,27 @@ pub(crate) fn check_docstring( // DOC501 if checker.enabled(Rule::DocstringMissingException) { for body_raise in &body_entries.raised_exceptions { - if body_raise.id == "NotImplementedError" { + if *body_raise.qualified_name.segments().last().unwrap() == "NotImplementedError" { continue; } - if !docstring_entries.raised_exceptions.contains(&body_raise.id) { + if !docstring_entries + .raised_exceptions + .iter() + .any(|r| body_raise.qualified_name.segments().ends_with(r.segments())) + { let diagnostic = Diagnostic::new( DocstringMissingException { - id: body_raise.id.clone(), + id: body_raise + .qualified_name + .segments() + .last() + .unwrap() + .to_string(), }, body_raise.range, ); - checker.diagnostics.push(diagnostic); + diagnostics.push(diagnostic); } } } @@ -323,13 +323,13 @@ pub(crate) fn check_docstring( // DOC502 if checker.enabled(Rule::DocstringExtraneousException) { let mut extraneous_exceptions = Vec::new(); - for docstring_raise in docstring_entries.raised_exceptions { - if !body_entries - .raised_exceptions - .iter() - .any(|r| r.id == docstring_raise) - { - extraneous_exceptions.push(docstring_raise); + for docstring_raise in &docstring_entries.raised_exceptions { + if !body_entries.raised_exceptions.iter().any(|r| { + r.qualified_name + .segments() + .ends_with(docstring_raise.segments()) + }) { + extraneous_exceptions.push(docstring_raise.to_string()); } } if !extraneous_exceptions.is_empty() { @@ -339,7 +339,9 @@ pub(crate) fn check_docstring( }, docstring_entries.raised_exceptions_range.unwrap(), ); - checker.diagnostics.push(diagnostic); + diagnostics.push(diagnostic); } } + + checker.diagnostics.extend(diagnostics); } From aae1027d67d7555360a95fe4674b67f5523a6ad3 Mon Sep 17 00:00:00 2001 From: augustelalande Date: Fri, 19 Jul 2024 01:59:39 -0400 Subject: [PATCH 21/24] add more tests for attribute exceptions --- .../test/fixtures/pydoclint/DOC501_google.py | 39 +++++++++++++- .../test/fixtures/pydoclint/DOC501_numpy.py | 2 +- ...ng-missing-exception_DOC501_google.py.snap | 52 +++++++++++-------- 3 files changed, 69 insertions(+), 24 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC501_google.py b/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC501_google.py index 84dc6e4ad26b6..c5dc038b22497 100644 --- a/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC501_google.py +++ b/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC501_google.py @@ -1,3 +1,4 @@ +import something from somewhere import AnotherError @@ -8,7 +9,7 @@ class FasterThanLightError(Exception): _some_error = Exception -# DOC501 +# OK def calculate_speed(distance: float, time: float) -> float: """Calculate speed as distance divided by time. @@ -128,6 +129,16 @@ def calculate_speed(distance: float, time: float) -> float: 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. @@ -153,3 +164,29 @@ def calculate_speed(distance: float, time: float) -> float: # 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 diff --git a/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC501_numpy.py b/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC501_numpy.py index fb949a4672de6..f78beaec3f701 100644 --- a/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC501_numpy.py +++ b/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC501_numpy.py @@ -2,7 +2,7 @@ class FasterThanLightError(Exception): ... -# DOC501 +# OK def calculate_speed(distance: float, time: float) -> float: """ Calculate speed as distance divided by time. diff --git a/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-missing-exception_DOC501_google.py.snap b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-missing-exception_DOC501_google.py.snap index c7a2fe47cbb4c..8ea9749d5246d 100644 --- a/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-missing-exception_DOC501_google.py.snap +++ b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-missing-exception_DOC501_google.py.snap @@ -1,44 +1,52 @@ --- source: crates/ruff_linter/src/rules/pydoclint/mod.rs --- -DOC501_google.py:45:15: DOC501 Raised exception `FasterThanLightError` missing from docstring +DOC501_google.py:46:15: DOC501 Raised exception `FasterThanLightError` missing from docstring | -43 | return distance / time -44 | except ZeroDivisionError as exc: -45 | raise FasterThanLightError from exc +44 | return distance / time +45 | except ZeroDivisionError as exc: +46 | raise FasterThanLightError from exc | ^^^^^^^^^^^^^^^^^^^^ DOC501 | -DOC501_google.py:62:15: DOC501 Raised exception `FasterThanLightError` missing from docstring +DOC501_google.py:63:15: DOC501 Raised exception `FasterThanLightError` missing from docstring | -60 | return distance / time -61 | except ZeroDivisionError as exc: -62 | raise FasterThanLightError from exc +61 | return distance / time +62 | except ZeroDivisionError as exc: +63 | raise FasterThanLightError from exc | ^^^^^^^^^^^^^^^^^^^^ DOC501 -63 | except: -64 | raise ValueError +64 | except: +65 | raise ValueError | -DOC501_google.py:64:15: DOC501 Raised exception `ValueError` missing from docstring +DOC501_google.py:65:15: DOC501 Raised exception `ValueError` missing from docstring | -62 | raise FasterThanLightError from exc -63 | except: -64 | raise ValueError +63 | raise FasterThanLightError from exc +64 | except: +65 | raise ValueError | ^^^^^^^^^^ DOC501 | -DOC501_google.py:114:11: DOC501 Raised exception `AnotherError` missing from docstring +DOC501_google.py:115:11: DOC501 Raised exception `AnotherError` missing from docstring | -112 | Speed as distance divided by time. -113 | """ -114 | raise AnotherError +113 | Speed as distance divided by time. +114 | """ +115 | raise AnotherError | ^^^^^^^^^^^^ DOC501 | -DOC501_google.py:128:11: DOC501 Raised exception `AnotherError` missing from docstring +DOC501_google.py:129:11: DOC501 Raised exception `AnotherError` missing from docstring | -126 | Speed as distance divided by time. -127 | """ -128 | raise AnotherError() +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 + | From 585db73f9906e3c636a06f856ac375bc444c0f2b Mon Sep 17 00:00:00 2001 From: augustelalande Date: Fri, 19 Jul 2024 02:02:34 -0400 Subject: [PATCH 22/24] clippy --- .../src/rules/pydoclint/rules/check_docstring.rs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs b/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs index 81d300fd70838..55b24bfc2aee3 100644 --- a/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs +++ b/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs @@ -151,7 +151,7 @@ impl<'a> DocstringEntries<'a> { } // Parses docstring sections of supported styles. -fn parse_entries<'a>(content: &'a str, style: SectionStyle) -> Vec> { +fn parse_entries(content: &str, style: SectionStyle) -> Vec { match style { SectionStyle::Google => parse_entries_google(content), SectionStyle::Numpy => parse_entries_numpy(content), @@ -306,12 +306,7 @@ pub(crate) fn check_docstring( { let diagnostic = Diagnostic::new( DocstringMissingException { - id: body_raise - .qualified_name - .segments() - .last() - .unwrap() - .to_string(), + id: (*body_raise.qualified_name.segments().last().unwrap()).to_string(), }, body_raise.range, ); From 0434606d7e2a8cd582049e89bb5ef7ba721823dc Mon Sep 17 00:00:00 2001 From: Auguste Lalande Date: Fri, 19 Jul 2024 10:18:32 -0400 Subject: [PATCH 23/24] Update crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs Co-authored-by: T-256 <132141463+T-256@users.noreply.github.com> --- crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs b/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs index 55b24bfc2aee3..d367a4a9b5cba 100644 --- a/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs +++ b/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs @@ -72,7 +72,7 @@ impl Violation for DocstringMissingException { } } -/// ## What it does' +/// ## What it does /// Checks for function docstrings that include exceptions which are not /// explicitly raised. /// From 9a6edd4a6f728af983b0d910b45e08b2bbf3fbc5 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 20 Jul 2024 15:36:04 -0400 Subject: [PATCH 24/24] Add some docs; tweak diagnostic message; return None for no-Raises --- .../rules/pydoclint/rules/check_docstring.rs | 170 +++++++++++------- ...extraneous-exception_DOC502_google.py.snap | 6 +- ...-extraneous-exception_DOC502_numpy.py.snap | 6 +- 3 files changed, 111 insertions(+), 71 deletions(-) diff --git a/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs b/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs index d367a4a9b5cba..e85d91fd1cca2 100644 --- a/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs +++ b/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs @@ -1,3 +1,4 @@ +use itertools::Itertools; use ruff_diagnostics::Diagnostic; use ruff_diagnostics::Violation; use ruff_macros::{derive_message_formats, violation}; @@ -15,11 +16,12 @@ use crate::rules::pydocstyle::settings::Convention; /// ## What it does /// Checks for function docstrings that do not include documentation for all -/// raised exceptions. +/// explicitly-raised exceptions. /// /// ## Why is this bad? -/// This rule helps prevent you from leaving docstrings unfinished or incomplete. -/// Some conventions require all explicit exceptions to be documented. +/// 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 @@ -77,7 +79,8 @@ impl Violation for DocstringMissingException { /// explicitly raised. /// /// ## Why is this bad? -/// Some conventions prefer non-explicit exceptions be left out of the docstring. +/// Some conventions prefer non-explicit exceptions be omitted from the +/// docstring. /// /// ## Example /// ```python @@ -120,37 +123,47 @@ impl Violation for DocstringExtraneousException { #[derive_message_formats] fn message(&self) -> String { let DocstringExtraneousException { ids } = self; - format!("{} not explicitly raised.", ids.join(", ")) + + 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(", ") + ) + } } } -// Parse docstring #[derive(Debug)] struct DocstringEntries<'a> { raised_exceptions: Vec>, - raised_exceptions_range: Option, + raised_exceptions_range: TextRange, } impl<'a> DocstringEntries<'a> { - fn new(sections: &'a SectionContexts, style: SectionStyle) -> Self { - let mut raised_exceptions: Vec = Vec::new(); - let mut raised_exceptions_range = None; - + /// 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 { for section in sections.iter() { if section.kind() == SectionKind::Raises { - raised_exceptions = parse_entries(section.following_lines_str(), style); - raised_exceptions_range = Some(section.range()); + return Some(Self { + raised_exceptions: parse_entries(section.following_lines_str(), style), + raised_exceptions_range: section.range(), + }); } } + None + } +} - Self { - raised_exceptions, - raised_exceptions_range, - } +impl Ranged for DocstringEntries<'_> { + fn range(&self) -> TextRange { + self.raised_exceptions_range } } -// Parses docstring sections of supported styles. +/// Parse the entries in a `Raises` section of a docstring. fn parse_entries(content: &str, style: SectionStyle) -> Vec { match style { SectionStyle::Google => parse_entries_google(content), @@ -158,12 +171,13 @@ fn parse_entries(content: &str, style: SectionStyle) -> Vec { } } -// Parses google style docstring sections of the form: -// -// Raises: -// FasterThanLightError: If speed is greater than the speed of light. -// DivisionByZero: If attempting to divide by zero. -// +/// 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 { let mut entries: Vec = Vec::new(); for potential in content.split('\n') { @@ -176,15 +190,16 @@ fn parse_entries_google(content: &str) -> Vec { entries } -// Parses numpy style docstring sections of the form: -// -// Raises -// ------ -// FasterThanLightError -// If speed is greater than the speed of light. -// DivisionByZero -// If attempting to divide by zero. -// +/// 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 { let mut entries: Vec = Vec::new(); let mut split = content.split('\n'); @@ -203,17 +218,26 @@ fn parse_entries_numpy(content: &str) -> Vec { entries } -// Parse body +/// 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>, } +/// An AST visitor to extract the raised exceptions from a function body. struct BodyVisitor<'a> { raised_exceptions: Vec>, semantic: &'a SemanticModel<'a>, @@ -283,32 +307,45 @@ pub(crate) fn check_docstring( // Prioritize the specified convention over the determined style. let docstring_entries = match convention { - Some(Convention::Google) => DocstringEntries::new(section_contexts, SectionStyle::Google), - Some(Convention::Numpy) => DocstringEntries::new(section_contexts, SectionStyle::Numpy), - _ => DocstringEntries::new(section_contexts, section_contexts.style()), + 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 mut visitor = BodyVisitor::new(checker.semantic()); - visitor::walk_body(&mut visitor, member.body()); - let body_entries = visitor.finish(); + 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 { - if *body_raise.qualified_name.segments().last().unwrap() == "NotImplementedError" { + let Some(name) = body_raise.qualified_name.segments().last() else { + continue; + }; + + if *name == "NotImplementedError" { continue; } - if !docstring_entries - .raised_exceptions - .iter() - .any(|r| body_raise.qualified_name.segments().ends_with(r.segments())) - { + 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: (*body_raise.qualified_name.segments().last().unwrap()).to_string(), + id: (*name).to_string(), }, - body_raise.range, + body_raise.range(), ); diagnostics.push(diagnostic); } @@ -317,24 +354,27 @@ pub(crate) fn check_docstring( // DOC502 if checker.enabled(Rule::DocstringExtraneousException) { - let mut extraneous_exceptions = Vec::new(); - for docstring_raise in &docstring_entries.raised_exceptions { - if !body_entries.raised_exceptions.iter().any(|r| { - r.qualified_name - .segments() - .ends_with(docstring_raise.segments()) - }) { - extraneous_exceptions.push(docstring_raise.to_string()); + 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); } - } - if !extraneous_exceptions.is_empty() { - let diagnostic = Diagnostic::new( - DocstringExtraneousException { - ids: extraneous_exceptions, - }, - docstring_entries.raised_exceptions_range.unwrap(), - ); - diagnostics.push(diagnostic); } } diff --git a/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-extraneous-exception_DOC502_google.py.snap b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-extraneous-exception_DOC502_google.py.snap index bf38b07b34339..8ef9ed882159f 100644 --- a/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-extraneous-exception_DOC502_google.py.snap +++ b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-extraneous-exception_DOC502_google.py.snap @@ -1,7 +1,7 @@ --- source: crates/ruff_linter/src/rules/pydoclint/mod.rs --- -DOC502_google.py:16:1: DOC502 FasterThanLightError not explicitly raised. +DOC502_google.py:16:1: DOC502 Raised exception is not explicitly raised: `FasterThanLightError` | 14 | Speed as distance divided by time. 15 | @@ -12,7 +12,7 @@ DOC502_google.py:16:1: DOC502 FasterThanLightError not explicitly raised. 19 | return distance / time | -DOC502_google.py:33:1: DOC502 FasterThanLightError, DivisionByZero not explicitly raised. +DOC502_google.py:33:1: DOC502 Raised exceptions are not explicitly raised: `FasterThanLightError`, `DivisionByZero` | 31 | Speed as distance divided by time. 32 | @@ -24,7 +24,7 @@ DOC502_google.py:33:1: DOC502 FasterThanLightError, DivisionByZero not explicitl 37 | return distance / time | -DOC502_google.py:51:1: DOC502 DivisionByZero not explicitly raised. +DOC502_google.py:51:1: DOC502 Raised exception is not explicitly raised: `DivisionByZero` | 49 | Speed as distance divided by time. 50 | diff --git a/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-extraneous-exception_DOC502_numpy.py.snap b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-extraneous-exception_DOC502_numpy.py.snap index a8e7f98cc67b7..41498f2f6e03b 100644 --- a/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-extraneous-exception_DOC502_numpy.py.snap +++ b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-extraneous-exception_DOC502_numpy.py.snap @@ -1,7 +1,7 @@ --- source: crates/ruff_linter/src/rules/pydoclint/mod.rs --- -DOC502_numpy.py:22:1: DOC502 FasterThanLightError not explicitly raised. +DOC502_numpy.py:22:1: DOC502 Raised exception is not explicitly raised: `FasterThanLightError` | 20 | Speed as distance divided by time. 21 | @@ -14,7 +14,7 @@ DOC502_numpy.py:22:1: DOC502 FasterThanLightError not explicitly raised. 27 | return distance / time | -DOC502_numpy.py:47:1: DOC502 FasterThanLightError, DivisionByZero not explicitly raised. +DOC502_numpy.py:47:1: DOC502 Raised exceptions are not explicitly raised: `FasterThanLightError`, `DivisionByZero` | 45 | Speed as distance divided by time. 46 | @@ -29,7 +29,7 @@ DOC502_numpy.py:47:1: DOC502 FasterThanLightError, DivisionByZero not explicitly 54 | return distance / time | -DOC502_numpy.py:74:1: DOC502 DivisionByZero not explicitly raised. +DOC502_numpy.py:74:1: DOC502 Raised exception is not explicitly raised: `DivisionByZero` | 72 | Speed as distance divided by time. 73 |