From bcc518cd6926a396627fba4730eb4dc8722fa315 Mon Sep 17 00:00:00 2001 From: augustelalande Date: Thu, 30 Oct 2025 19:46:51 -0400 Subject: [PATCH 1/6] refactor --- .../src/checkers/ast/analyze/definitions.rs | 1 + crates/ruff_linter/src/rules/pydoclint/mod.rs | 104 ++++++++++++++ .../rules/pydoclint/rules/check_docstring.rs | 135 +++++++++++++++--- ...ts__D417_canonical_google_examples.py.snap | 4 + ...sts__D417_canonical_numpy_examples.py.snap | 4 + ...s__pydoclint__tests__D417_sections.py.snap | 112 +++++++++++++++ ..._rules__pydoclint__tests__d417_google.snap | 113 +++++++++++++++ ...ts__d417_google_ignore_var_parameters.snap | 95 ++++++++++++ ...__rules__pydoclint__tests__d417_numpy.snap | 4 + ...s__pydoclint__tests__d417_unspecified.snap | 113 +++++++++++++++ ...417_unspecified_ignore_var_parameters.snap | 113 +++++++++++++++ .../src/rules/pydocstyle/rules/sections.rs | 26 ++-- 12 files changed, 793 insertions(+), 31 deletions(-) create mode 100644 crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__D417_canonical_google_examples.py.snap create mode 100644 crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__D417_canonical_numpy_examples.py.snap create mode 100644 crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__D417_sections.py.snap create mode 100644 crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__d417_google.snap create mode 100644 crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__d417_google_ignore_var_parameters.snap create mode 100644 crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__d417_numpy.snap create mode 100644 crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__d417_unspecified.snap create mode 100644 crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__d417_unspecified_ignore_var_parameters.snap diff --git a/crates/ruff_linter/src/checkers/ast/analyze/definitions.rs b/crates/ruff_linter/src/checkers/ast/analyze/definitions.rs index 4a3fe560be..841021fabe 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/definitions.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/definitions.rs @@ -81,6 +81,7 @@ pub(crate) fn definitions(checker: &mut Checker) { Rule::UndocumentedPublicPackage, ]); let enforce_pydoclint = checker.any_rule_enabled(&[ + Rule::UndocumentedParam, Rule::DocstringExtraneousParameter, Rule::DocstringMissingReturns, Rule::DocstringExtraneousReturns, diff --git a/crates/ruff_linter/src/rules/pydoclint/mod.rs b/crates/ruff_linter/src/rules/pydoclint/mod.rs index 69326b0c51..75c3f24981 100644 --- a/crates/ruff_linter/src/rules/pydoclint/mod.rs +++ b/crates/ruff_linter/src/rules/pydoclint/mod.rs @@ -12,6 +12,7 @@ mod tests { use crate::registry::Rule; use crate::rules::pydocstyle; use crate::rules::pydocstyle::settings::Convention; + use crate::settings::types::PreviewMode; use crate::test::test_path; use crate::{assert_diagnostics, settings}; @@ -99,4 +100,107 @@ mod tests { assert_diagnostics!(snapshot, diagnostics); Ok(()) } + + #[test_case(Rule::UndocumentedParam, Path::new("canonical_google_examples.py"))] + #[test_case(Rule::UndocumentedParam, Path::new("canonical_numpy_examples.py"))] + #[test_case(Rule::UndocumentedParam, Path::new("sections.py"))] + fn undocumented_param(rule_code: Rule, path: &Path) -> Result<()> { + let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); + let diagnostics = test_path( + Path::new("pydocstyle").join(path).as_path(), + &settings::LinterSettings { + preview: PreviewMode::Enabled, + pydocstyle: pydocstyle::settings::Settings { + ..pydocstyle::settings::Settings::default() + }, + ..settings::LinterSettings::for_rule(rule_code) + }, + )?; + assert_diagnostics!(snapshot, diagnostics); + Ok(()) + } + + #[test] + fn d417_unspecified() -> Result<()> { + let diagnostics = test_path( + Path::new("pydocstyle/D417.py"), + &settings::LinterSettings { + preview: PreviewMode::Enabled, + // When inferring the convention, we'll see a few false negatives. + // See: https://github.com/PyCQA/pydocstyle/issues/459. + pydocstyle: pydocstyle::settings::Settings::default(), + ..settings::LinterSettings::for_rule(Rule::UndocumentedParam) + }, + )?; + assert_diagnostics!(diagnostics); + Ok(()) + } + + #[test] + fn d417_unspecified_ignore_var_parameters() -> Result<()> { + let diagnostics = test_path( + Path::new("pydocstyle/D417.py"), + &settings::LinterSettings { + preview: PreviewMode::Enabled, + pydocstyle: pydocstyle::settings::Settings::default(), + ..settings::LinterSettings::for_rule(Rule::UndocumentedParam) + }, + )?; + assert_diagnostics!(diagnostics); + Ok(()) + } + + #[test] + fn d417_google() -> Result<()> { + let diagnostics = test_path( + Path::new("pydocstyle/D417.py"), + &settings::LinterSettings { + preview: PreviewMode::Enabled, + // With explicit Google convention, we should flag every function. + pydocstyle: pydocstyle::settings::Settings { + convention: Some(Convention::Google), + ..pydocstyle::settings::Settings::default() + }, + ..settings::LinterSettings::for_rule(Rule::UndocumentedParam) + }, + )?; + assert_diagnostics!(diagnostics); + Ok(()) + } + + #[test] + fn d417_google_ignore_var_parameters() -> Result<()> { + let diagnostics = test_path( + Path::new("pydocstyle/D417.py"), + &settings::LinterSettings { + preview: PreviewMode::Enabled, + pydocstyle: pydocstyle::settings::Settings { + convention: Some(Convention::Google), + ignore_var_parameters: true, + ..pydocstyle::settings::Settings::default() + }, + ..settings::LinterSettings::for_rule(Rule::UndocumentedParam) + }, + )?; + assert_diagnostics!(diagnostics); + Ok(()) + } + + #[test] + fn d417_numpy() -> Result<()> { + let diagnostics = test_path( + Path::new("pydocstyle/D417.py"), + &settings::LinterSettings { + preview: PreviewMode::Enabled, + // With explicit numpy convention, we shouldn't flag anything. + pydocstyle: pydocstyle::settings::Settings { + convention: Some(Convention::Numpy), + ..pydocstyle::settings::Settings::default() + }, + ..settings::LinterSettings::for_rule(Rule::UndocumentedParam) + }, + )?; + assert_diagnostics!(diagnostics); + Ok(()) + } } 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 dd88250952..370f0b40dc 100644 --- a/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs +++ b/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs @@ -1,9 +1,11 @@ use itertools::Itertools; use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::{map_callable, map_subscript}; +use ruff_python_ast::identifier::Identifier; use ruff_python_ast::name::QualifiedName; use ruff_python_ast::visitor::Visitor; use ruff_python_ast::{self as ast, Expr, Stmt, visitor}; +use ruff_python_semantic::analyze::visibility::is_staticmethod; use ruff_python_semantic::analyze::{function_type, visibility}; use ruff_python_semantic::{Definition, SemanticModel}; use ruff_python_stdlib::identifiers::is_identifier; @@ -17,6 +19,7 @@ use crate::docstrings::Docstring; use crate::docstrings::sections::{SectionContext, SectionContexts, SectionKind}; use crate::docstrings::styles::SectionStyle; use crate::registry::Rule; +use crate::rules::pydocstyle::rules::UndocumentedParam; use crate::rules::pydocstyle::settings::Convention; /// ## What it does @@ -464,6 +467,7 @@ impl GenericSection { #[derive(Debug, Clone)] struct ParameterEntry<'a> { name: &'a str, + has_definition: bool, range: TextRange, } @@ -523,6 +527,16 @@ impl<'a> ParametersSection<'a> { range: section.section_name_range(), } } + + fn extends_from_section(&mut self, section: &SectionContext<'a>, style: Option) { + let mut new_entries = parse_parameters( + section.following_lines_str(), + section.following_range().start(), + style, + ); + + self.parameters.append(&mut new_entries); + } } #[derive(Debug, Default)] @@ -538,9 +552,21 @@ impl<'a> DocstringSections<'a> { let mut docstring_sections = Self::default(); for section in sections { match section.kind() { - SectionKind::Args | SectionKind::Arguments | SectionKind::Parameters => { - docstring_sections.parameters = - Some(ParametersSection::from_section(§ion, style)); + SectionKind::Args + | SectionKind::Arguments + | SectionKind::Parameters + | SectionKind::KeywordArgs + | SectionKind::KeywordArguments + | SectionKind::OtherArgs + | SectionKind::OtherArguments + | SectionKind::OtherParams + | SectionKind::OtherParameters => { + if let Some(ref mut parameters_section) = docstring_sections.parameters { + parameters_section.extends_from_section(§ion, style); + } else { + docstring_sections.parameters = + Some(ParametersSection::from_section(§ion, style)); + } } SectionKind::Raises => { docstring_sections.raises = Some(RaisesSection::from_section(§ion, style)); @@ -569,11 +595,12 @@ fn parse_parameters( ) -> Vec> { match style { Some(SectionStyle::Google) => parse_parameters_google(content, content_start), + Some(SectionStyle::Numpy) => parse_parameters_numpy(content, content_start), None => { - let entries = parse_parameters_google(content, content_start); + let entries = parse_parameters_numpy(content, content_start); if entries.is_empty() { - parse_parameters_numpy(content, content_start) + parse_parameters_google(content, content_start) } else { entries } @@ -607,7 +634,7 @@ fn parse_parameters_google(content: &str, content_start: TextSize) -> Vec Vec Vec Vec Vec> { let mut entries: Vec = Vec::new(); let mut lines = content.lines(); - let Some(dashes) = lines.next() else { + let Some(dashes_line) = lines.next() else { return entries; }; - let indentation = &dashes[..dashes.len() - dashes.trim_start().len()]; + let dashes = dashes_line.trim_start(); + if dashes.is_empty() || !dashes.chars().all(|c| c == '-') { + return entries; + } + let indentation = &dashes_line[..dashes_line.len() - dashes.len()]; let mut current_pos = content.full_line_end(dashes.text_len()); for potential in lines { @@ -669,6 +709,7 @@ fn parse_parameters_numpy(content: &str, content_start: TextSize) -> Vec) -> Vec parse_raises_google(content), Some(SectionStyle::Numpy) => parse_raises_numpy(content), None => { - let entries = parse_raises_google(content); + let entries = parse_raises_numpy(content); if entries.is_empty() { - parse_raises_numpy(content) + parse_raises_google(content) } else { entries } @@ -733,10 +774,14 @@ fn parse_raises_google(content: &str) -> Vec> { fn parse_raises_numpy(content: &str) -> Vec> { let mut entries: Vec = Vec::new(); let mut lines = content.lines(); - let Some(dashes) = lines.next() else { + let Some(dashes_line) = lines.next() else { return entries; }; - let indentation = &dashes[..dashes.len() - dashes.trim_start().len()]; + let dashes = dashes_line.trim_start(); + if dashes.is_empty() || !dashes.chars().all(|c| c == '-') { + return entries; + } + let indentation = &dashes_line[..dashes_line.len() - dashes.len()]; for potential in lines { if let Some(entry) = potential.strip_prefix(indentation) { if let Some(first_char) = entry.chars().next() { @@ -1130,14 +1175,24 @@ fn is_generator_function_annotated_as_returning_none( .is_some_and(GeneratorOrIteratorArguments::indicates_none_returned) } -fn parameters_from_signature<'a>(docstring: &'a Docstring) -> Vec<&'a str> { +#[derive(Debug)] +struct SignatureParameter<'a> { + name: &'a str, + is_variadic: bool, +} + +fn parameters_from_signature<'a>(docstring: &'a Docstring) -> Vec> { let mut parameters = Vec::new(); let Some(function) = docstring.definition.as_function_def() else { return parameters; }; for param in &function.parameters { - parameters.push(param.name()); + parameters.push(SignatureParameter { + name: param.name(), + is_variadic: param.is_variadic(), + }); } + parameters } @@ -1173,10 +1228,6 @@ pub(crate) fn check_docstring( let semantic = checker.semantic(); - if function_type::is_stub(function_def, semantic) { - return; - } - // Prioritize the specified convention over the determined style. let docstring_sections = match convention { Some(Convention::Google) => { @@ -1196,6 +1247,49 @@ pub(crate) fn check_docstring( let signature_parameters = parameters_from_signature(docstring); + // DOC101 + if checker.settings().preview.is_enabled() { + if checker.is_rule_enabled(Rule::UndocumentedParam) { + if let Some(parameters_section) = docstring_sections.parameters.as_ref() { + let mut missing_parameters = Vec::new(); + + // Here we check if the function is a method (and not a staticmethod) + // in which case we skip the first argument which should be `self` or + // `cls`, and does not need to be documented. + for signature_param in signature_parameters.iter().skip(usize::from( + docstring.definition.is_method() + && !is_staticmethod(&function_def.decorator_list, semantic), + )) { + if !(checker.settings().pydocstyle.ignore_var_parameters() + && signature_param.is_variadic) + && !signature_param.name.starts_with('_') + && !parameters_section.parameters.iter().any(|param| { + ¶m.name == &signature_param.name && param.has_definition + }) + { + missing_parameters.push(signature_param.name.to_string()); + } + } + + if !missing_parameters.is_empty() { + if let Some(definition) = docstring.definition.name() { + checker.report_diagnostic( + UndocumentedParam { + definition: definition.to_string(), + names: missing_parameters, + }, + function_def.identifier(), + ); + } + } + } + } + } + + if function_type::is_stub(function_def, semantic) { + return; + } + // DOC201 if checker.is_rule_enabled(Rule::DocstringMissingReturns) { if should_document_returns(function_def) @@ -1290,7 +1384,10 @@ pub(crate) fn check_docstring( if function_def.parameters.vararg.is_none() && function_def.parameters.kwarg.is_none() { if let Some(docstring_params) = docstring_sections.parameters { for docstring_param in &docstring_params.parameters { - if !signature_parameters.contains(&docstring_param.name) { + if !signature_parameters + .iter() + .any(|param| ¶m.name == &docstring_param.name) + { checker.report_diagnostic( DocstringExtraneousParameter { id: docstring_param.name.to_string(), diff --git a/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__D417_canonical_google_examples.py.snap b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__D417_canonical_google_examples.py.snap new file mode 100644 index 0000000000..d3c56b22a3 --- /dev/null +++ b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__D417_canonical_google_examples.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/rules/pydoclint/mod.rs +--- + diff --git a/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__D417_canonical_numpy_examples.py.snap b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__D417_canonical_numpy_examples.py.snap new file mode 100644 index 0000000000..d3c56b22a3 --- /dev/null +++ b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__D417_canonical_numpy_examples.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/rules/pydoclint/mod.rs +--- + diff --git a/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__D417_sections.py.snap b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__D417_sections.py.snap new file mode 100644 index 0000000000..6af655148e --- /dev/null +++ b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__D417_sections.py.snap @@ -0,0 +1,112 @@ +--- +source: crates/ruff_linter/src/rules/pydoclint/mod.rs +--- +D417 Missing argument description in the docstring for `bar`: `y` + --> sections.py:292:9 + | +290 | x = 1 +291 | +292 | def bar(y=2): # noqa: D207, D213, D406, D407 + | ^^^ +293 | """Nested function test for docstrings. + | + +D417 Missing argument description in the docstring for `test_missing_google_args`: `y` + --> sections.py:309:5 + | +307 | "(argument(s) y are missing descriptions in " +308 | "'test_missing_google_args' docstring)") +309 | def test_missing_google_args(x=1, y=2, _private=3): # noqa: D406, D407 + | ^^^^^^^^^^^^^^^^^^^^^^^^ +310 | """Toggle the gizmo. + | + +D417 Missing argument descriptions in the docstring for `test_missing_args`: `test`, `y`, `z` + --> sections.py:333:9 + | +331 | "(argument(s) test, y, z are missing descriptions in " +332 | "'test_missing_args' docstring)", arg_count=5) +333 | def test_missing_args(self, test, x, y, z=3, _private_arg=3): # noqa: D213, D407 + | ^^^^^^^^^^^^^^^^^ +334 | """Test a valid args section. + | + +D417 Missing argument descriptions in the docstring for `test_missing_args_class_method`: `test`, `y`, `z` + --> sections.py:345:9 + | +343 | "(argument(s) test, y, z are missing descriptions in " +344 | "'test_missing_args_class_method' docstring)", arg_count=5) +345 | def test_missing_args_class_method(cls, test, x, y, _, z=3): # noqa: D213, D407 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +346 | """Test a valid args section. + | + +D417 Missing argument descriptions in the docstring for `test_missing_args_static_method`: `a`, `y`, `z` + --> sections.py:358:9 + | +356 | "(argument(s) a, y, z are missing descriptions in " +357 | "'test_missing_args_static_method' docstring)", arg_count=4) +358 | def test_missing_args_static_method(a, x, y, _test, z=3): # noqa: D213, D407 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +359 | """Test a valid args section. + | + +D417 Missing argument descriptions in the docstring for `test_missing_docstring`: `a`, `b` + --> sections.py:370:9 + | +368 | "(argument(s) a, b are missing descriptions in " +369 | "'test_missing_docstring' docstring)", arg_count=2) +370 | def test_missing_docstring(a, b): # noqa: D213, D407 + | ^^^^^^^^^^^^^^^^^^^^^^ +371 | """Test a valid args section. + | + +D417 Missing argument description in the docstring for `test_missing_numpy_args`: `y` + --> sections.py:398:5 + | +396 | "(argument(s) y are missing descriptions in " +397 | "'test_missing_numpy_args' docstring)") +398 | def test_missing_numpy_args(_private_arg=0, x=1, y=2): # noqa: D406, D407 + | ^^^^^^^^^^^^^^^^^^^^^^^ +399 | """Toggle the gizmo. + | + +D417 Missing argument descriptions in the docstring for `test_method`: `test`, `another_test`, `x`, `y` + --> sections.py:413:9 + | +411 | """Test class.""" +412 | +413 | def test_method(self, test, another_test, z, _, x=1, y=2, _private_arg=1): # noqa: D213, D407 + | ^^^^^^^^^^^ +414 | """Test a valid args section. + | + +D417 Missing argument descriptions in the docstring for `test_missing_args`: `test`, `x`, `y`, `z`, `t` + --> sections.py:434:9 + | +432 | "(argument(s) test, y, z are missing descriptions in " +433 | "'test_missing_args' docstring)", arg_count=5) +434 | def test_missing_args(self, test, x, y, z=3, t=1, _private=0): # noqa: D213, D407 + | ^^^^^^^^^^^^^^^^^ +435 | """Test a valid args section. + | + +D417 Missing argument descriptions in the docstring for `test_missing_args_static_method`: `a`, `x`, `y`, `z` + --> sections.py:468:9 + | +466 | "(argument(s) a, z are missing descriptions in " +467 | "'test_missing_args_static_method' docstring)", arg_count=3) +468 | def test_missing_args_static_method(a, x, y, z=3, t=1): # noqa: D213, D407 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +469 | """Test a valid args section. + | + +D417 Missing argument description in the docstring for `test_incorrect_indent`: `y` + --> sections.py:498:9 + | +496 | "(argument(s) y are missing descriptions in " +497 | "'test_incorrect_indent' docstring)", arg_count=3) +498 | def test_incorrect_indent(self, x=1, y=2): # noqa: D207, D213, D407 + | ^^^^^^^^^^^^^^^^^^^^^ +499 | """Reproducing issue #437. + | diff --git a/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__d417_google.snap b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__d417_google.snap new file mode 100644 index 0000000000..3463b99b86 --- /dev/null +++ b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__d417_google.snap @@ -0,0 +1,113 @@ +--- +source: crates/ruff_linter/src/rules/pydoclint/mod.rs +--- +D417 Missing argument descriptions in the docstring for `f`: `y`, `z` + --> D417.py:1:5 + | +1 | def f(x, y, z): + | ^ +2 | """Do something. + | + +D417 Missing argument descriptions in the docstring for `f`: `y`, `z` + --> D417.py:14:5 + | +14 | def f(x, y, z): + | ^ +15 | """Do something. + | + +D417 Missing argument descriptions in the docstring for `f`: `y`, `z` + --> D417.py:27:5 + | +27 | def f(x, y, z): + | ^ +28 | """Do something. + | + +D417 Missing argument descriptions in the docstring for `f`: `y`, `z` + --> D417.py:39:5 + | +39 | def f(x, y, z): + | ^ +40 | """Do something. + | + +D417 Missing argument description in the docstring for `f`: `y` + --> D417.py:52:5 + | +52 | def f(x, y, z): + | ^ +53 | """Do something. + | + +D417 Missing argument description in the docstring for `f`: `y` + --> D417.py:65:5 + | +65 | def f(x, y, z): + | ^ +66 | """Do something. + | + +D417 Missing argument description in the docstring for `f`: `y` + --> D417.py:77:5 + | +77 | def f(x, y, z): + | ^ +78 | """Do something. + | + +D417 Missing argument description in the docstring for `f`: `x` + --> D417.py:98:5 + | +98 | def f(x, *args, **kwargs): + | ^ +99 | """Do something. + | + +D417 Missing argument description in the docstring for `f`: `args` + --> D417.py:108:5 + | +108 | def f(x, *args, **kwargs): + | ^ +109 | """Do something. + | + +D417 Missing argument description in the docstring for `f`: `x` + --> D417.py:131:5 + | +129 | return x, y, z +130 | +131 | def f(x): + | ^ +132 | """Do something with valid description. + | + +D417 Missing argument description in the docstring for `select_data`: `auto_save` + --> D417.py:155:5 + | +155 | def select_data( + | ^^^^^^^^^^^ +156 | query: str, +157 | args: tuple, + | + +D417 Missing argument description in the docstring for `f`: `kwargs` + --> D417.py:172:5 + | +170 | """ +171 | +172 | def f(x, *args, **kwargs): + | ^ +173 | """Do something. + | + +D417 Missing argument description in the docstring for `should_fail`: `Args` + --> D417.py:199:5 + | +198 | # undocumented argument with the same name as a section +199 | def should_fail(payload, Args): + | ^^^^^^^^^^^ +200 | """ +201 | Send a message. + | diff --git a/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__d417_google_ignore_var_parameters.snap b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__d417_google_ignore_var_parameters.snap new file mode 100644 index 0000000000..16159b47ac --- /dev/null +++ b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__d417_google_ignore_var_parameters.snap @@ -0,0 +1,95 @@ +--- +source: crates/ruff_linter/src/rules/pydoclint/mod.rs +--- +D417 Missing argument descriptions in the docstring for `f`: `y`, `z` + --> D417.py:1:5 + | +1 | def f(x, y, z): + | ^ +2 | """Do something. + | + +D417 Missing argument descriptions in the docstring for `f`: `y`, `z` + --> D417.py:14:5 + | +14 | def f(x, y, z): + | ^ +15 | """Do something. + | + +D417 Missing argument descriptions in the docstring for `f`: `y`, `z` + --> D417.py:27:5 + | +27 | def f(x, y, z): + | ^ +28 | """Do something. + | + +D417 Missing argument descriptions in the docstring for `f`: `y`, `z` + --> D417.py:39:5 + | +39 | def f(x, y, z): + | ^ +40 | """Do something. + | + +D417 Missing argument description in the docstring for `f`: `y` + --> D417.py:52:5 + | +52 | def f(x, y, z): + | ^ +53 | """Do something. + | + +D417 Missing argument description in the docstring for `f`: `y` + --> D417.py:65:5 + | +65 | def f(x, y, z): + | ^ +66 | """Do something. + | + +D417 Missing argument description in the docstring for `f`: `y` + --> D417.py:77:5 + | +77 | def f(x, y, z): + | ^ +78 | """Do something. + | + +D417 Missing argument description in the docstring for `f`: `x` + --> D417.py:98:5 + | +98 | def f(x, *args, **kwargs): + | ^ +99 | """Do something. + | + +D417 Missing argument description in the docstring for `f`: `x` + --> D417.py:131:5 + | +129 | return x, y, z +130 | +131 | def f(x): + | ^ +132 | """Do something with valid description. + | + +D417 Missing argument description in the docstring for `select_data`: `auto_save` + --> D417.py:155:5 + | +155 | def select_data( + | ^^^^^^^^^^^ +156 | query: str, +157 | args: tuple, + | + +D417 Missing argument description in the docstring for `should_fail`: `Args` + --> D417.py:199:5 + | +198 | # undocumented argument with the same name as a section +199 | def should_fail(payload, Args): + | ^^^^^^^^^^^ +200 | """ +201 | Send a message. + | diff --git a/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__d417_numpy.snap b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__d417_numpy.snap new file mode 100644 index 0000000000..d3c56b22a3 --- /dev/null +++ b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__d417_numpy.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/rules/pydoclint/mod.rs +--- + diff --git a/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__d417_unspecified.snap b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__d417_unspecified.snap new file mode 100644 index 0000000000..3463b99b86 --- /dev/null +++ b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__d417_unspecified.snap @@ -0,0 +1,113 @@ +--- +source: crates/ruff_linter/src/rules/pydoclint/mod.rs +--- +D417 Missing argument descriptions in the docstring for `f`: `y`, `z` + --> D417.py:1:5 + | +1 | def f(x, y, z): + | ^ +2 | """Do something. + | + +D417 Missing argument descriptions in the docstring for `f`: `y`, `z` + --> D417.py:14:5 + | +14 | def f(x, y, z): + | ^ +15 | """Do something. + | + +D417 Missing argument descriptions in the docstring for `f`: `y`, `z` + --> D417.py:27:5 + | +27 | def f(x, y, z): + | ^ +28 | """Do something. + | + +D417 Missing argument descriptions in the docstring for `f`: `y`, `z` + --> D417.py:39:5 + | +39 | def f(x, y, z): + | ^ +40 | """Do something. + | + +D417 Missing argument description in the docstring for `f`: `y` + --> D417.py:52:5 + | +52 | def f(x, y, z): + | ^ +53 | """Do something. + | + +D417 Missing argument description in the docstring for `f`: `y` + --> D417.py:65:5 + | +65 | def f(x, y, z): + | ^ +66 | """Do something. + | + +D417 Missing argument description in the docstring for `f`: `y` + --> D417.py:77:5 + | +77 | def f(x, y, z): + | ^ +78 | """Do something. + | + +D417 Missing argument description in the docstring for `f`: `x` + --> D417.py:98:5 + | +98 | def f(x, *args, **kwargs): + | ^ +99 | """Do something. + | + +D417 Missing argument description in the docstring for `f`: `args` + --> D417.py:108:5 + | +108 | def f(x, *args, **kwargs): + | ^ +109 | """Do something. + | + +D417 Missing argument description in the docstring for `f`: `x` + --> D417.py:131:5 + | +129 | return x, y, z +130 | +131 | def f(x): + | ^ +132 | """Do something with valid description. + | + +D417 Missing argument description in the docstring for `select_data`: `auto_save` + --> D417.py:155:5 + | +155 | def select_data( + | ^^^^^^^^^^^ +156 | query: str, +157 | args: tuple, + | + +D417 Missing argument description in the docstring for `f`: `kwargs` + --> D417.py:172:5 + | +170 | """ +171 | +172 | def f(x, *args, **kwargs): + | ^ +173 | """Do something. + | + +D417 Missing argument description in the docstring for `should_fail`: `Args` + --> D417.py:199:5 + | +198 | # undocumented argument with the same name as a section +199 | def should_fail(payload, Args): + | ^^^^^^^^^^^ +200 | """ +201 | Send a message. + | diff --git a/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__d417_unspecified_ignore_var_parameters.snap b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__d417_unspecified_ignore_var_parameters.snap new file mode 100644 index 0000000000..3463b99b86 --- /dev/null +++ b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__d417_unspecified_ignore_var_parameters.snap @@ -0,0 +1,113 @@ +--- +source: crates/ruff_linter/src/rules/pydoclint/mod.rs +--- +D417 Missing argument descriptions in the docstring for `f`: `y`, `z` + --> D417.py:1:5 + | +1 | def f(x, y, z): + | ^ +2 | """Do something. + | + +D417 Missing argument descriptions in the docstring for `f`: `y`, `z` + --> D417.py:14:5 + | +14 | def f(x, y, z): + | ^ +15 | """Do something. + | + +D417 Missing argument descriptions in the docstring for `f`: `y`, `z` + --> D417.py:27:5 + | +27 | def f(x, y, z): + | ^ +28 | """Do something. + | + +D417 Missing argument descriptions in the docstring for `f`: `y`, `z` + --> D417.py:39:5 + | +39 | def f(x, y, z): + | ^ +40 | """Do something. + | + +D417 Missing argument description in the docstring for `f`: `y` + --> D417.py:52:5 + | +52 | def f(x, y, z): + | ^ +53 | """Do something. + | + +D417 Missing argument description in the docstring for `f`: `y` + --> D417.py:65:5 + | +65 | def f(x, y, z): + | ^ +66 | """Do something. + | + +D417 Missing argument description in the docstring for `f`: `y` + --> D417.py:77:5 + | +77 | def f(x, y, z): + | ^ +78 | """Do something. + | + +D417 Missing argument description in the docstring for `f`: `x` + --> D417.py:98:5 + | +98 | def f(x, *args, **kwargs): + | ^ +99 | """Do something. + | + +D417 Missing argument description in the docstring for `f`: `args` + --> D417.py:108:5 + | +108 | def f(x, *args, **kwargs): + | ^ +109 | """Do something. + | + +D417 Missing argument description in the docstring for `f`: `x` + --> D417.py:131:5 + | +129 | return x, y, z +130 | +131 | def f(x): + | ^ +132 | """Do something with valid description. + | + +D417 Missing argument description in the docstring for `select_data`: `auto_save` + --> D417.py:155:5 + | +155 | def select_data( + | ^^^^^^^^^^^ +156 | query: str, +157 | args: tuple, + | + +D417 Missing argument description in the docstring for `f`: `kwargs` + --> D417.py:172:5 + | +170 | """ +171 | +172 | def f(x, *args, **kwargs): + | ^ +173 | """Do something. + | + +D417 Missing argument description in the docstring for `should_fail`: `Args` + --> D417.py:199:5 + | +198 | # undocumented argument with the same name as a section +199 | def should_fail(payload, Args): + | ^^^^^^^^^^^ +200 | """ +201 | Send a message. + | diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/sections.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/sections.rs index 7b9fc80ba8..05e48a87d7 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/sections.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/sections.rs @@ -1237,9 +1237,9 @@ impl AlwaysFixableViolation for MissingSectionNameColon { #[violation_metadata(stable_since = "v0.0.73")] pub(crate) struct UndocumentedParam { /// The name of the function being documented. - definition: String, + pub(crate) definition: String, /// The names of the undocumented parameters. - names: Vec, + pub(crate) names: Vec, } impl Violation for UndocumentedParam { @@ -1820,16 +1820,18 @@ fn missing_args(checker: &Checker, docstring: &Docstring, docstrings_args: &FxHa } } - if !missing_arg_names.is_empty() { - if let Some(definition) = docstring.definition.name() { - let names = missing_arg_names.into_iter().sorted().collect(); - checker.report_diagnostic( - UndocumentedParam { - definition: definition.to_string(), - names, - }, - function.identifier(), - ); + if checker.settings().preview.is_disabled() { + if !missing_arg_names.is_empty() { + if let Some(definition) = docstring.definition.name() { + let names = missing_arg_names.into_iter().sorted().collect(); + checker.report_diagnostic( + UndocumentedParam { + definition: definition.to_string(), + names, + }, + function.identifier(), + ); + } } } } From 7b44b2521942417631973b16af72a63d71cc8b9d Mon Sep 17 00:00:00 2001 From: augustelalande Date: Thu, 30 Oct 2025 20:04:20 -0400 Subject: [PATCH 2/6] clippy --- .../src/rules/pydoclint/rules/check_docstring.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 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 370f0b40dc..80931570fb 100644 --- a/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs +++ b/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs @@ -1261,11 +1261,11 @@ pub(crate) fn check_docstring( && !is_staticmethod(&function_def.decorator_list, semantic), )) { if !(checker.settings().pydocstyle.ignore_var_parameters() - && signature_param.is_variadic) - && !signature_param.name.starts_with('_') - && !parameters_section.parameters.iter().any(|param| { - ¶m.name == &signature_param.name && param.has_definition - }) + && signature_param.is_variadic + || signature_param.name.starts_with('_') + || parameters_section.parameters.iter().any(|param| { + param.name == signature_param.name && param.has_definition + })) { missing_parameters.push(signature_param.name.to_string()); } @@ -1386,7 +1386,7 @@ pub(crate) fn check_docstring( for docstring_param in &docstring_params.parameters { if !signature_parameters .iter() - .any(|param| ¶m.name == &docstring_param.name) + .any(|param| param.name == docstring_param.name) { checker.report_diagnostic( DocstringExtraneousParameter { From 951c805e0575716764bb33550e848c42fbebb726 Mon Sep 17 00:00:00 2001 From: augustelalande Date: Fri, 31 Oct 2025 17:55:41 -0400 Subject: [PATCH 3/6] sort args alphabetically to match previous behaviour --- .../ruff_linter/src/rules/pydoclint/rules/check_docstring.rs | 3 ++- ...uff_linter__rules__pydoclint__tests__D417_sections.py.snap | 4 ++-- 2 files changed, 4 insertions(+), 3 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 80931570fb..3acd719cd0 100644 --- a/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs +++ b/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs @@ -1273,10 +1273,11 @@ pub(crate) fn check_docstring( if !missing_parameters.is_empty() { if let Some(definition) = docstring.definition.name() { + let names = missing_parameters.into_iter().sorted().collect(); checker.report_diagnostic( UndocumentedParam { definition: definition.to_string(), - names: missing_parameters, + names, }, function_def.identifier(), ); diff --git a/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__D417_sections.py.snap b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__D417_sections.py.snap index 6af655148e..c6205ba71f 100644 --- a/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__D417_sections.py.snap +++ b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__D417_sections.py.snap @@ -71,7 +71,7 @@ D417 Missing argument description in the docstring for `test_missing_numpy_args` 399 | """Toggle the gizmo. | -D417 Missing argument descriptions in the docstring for `test_method`: `test`, `another_test`, `x`, `y` +D417 Missing argument descriptions in the docstring for `test_method`: `another_test`, `test`, `x`, `y` --> sections.py:413:9 | 411 | """Test class.""" @@ -81,7 +81,7 @@ D417 Missing argument descriptions in the docstring for `test_method`: `test`, ` 414 | """Test a valid args section. | -D417 Missing argument descriptions in the docstring for `test_missing_args`: `test`, `x`, `y`, `z`, `t` +D417 Missing argument descriptions in the docstring for `test_missing_args`: `t`, `test`, `x`, `y`, `z` --> sections.py:434:9 | 432 | "(argument(s) test, y, z are missing descriptions in " From cf4924f4aa0f004daca6b09d5017803b7646cbd3 Mon Sep 17 00:00:00 2001 From: augustelalande Date: Fri, 31 Oct 2025 18:17:25 -0400 Subject: [PATCH 4/6] update formatting to match previous implementation --- .../rules/pydoclint/rules/check_docstring.rs | 34 ++++++++++++++++--- ..._rules__pydoclint__tests__d417_google.snap | 4 +-- ...s__pydoclint__tests__d417_unspecified.snap | 4 +-- ...417_unspecified_ignore_var_parameters.snap | 4 +-- 4 files changed, 35 insertions(+), 11 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 3acd719cd0..8d7f0130cd 100644 --- a/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs +++ b/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs @@ -1178,7 +1178,8 @@ fn is_generator_function_annotated_as_returning_none( #[derive(Debug)] struct SignatureParameter<'a> { name: &'a str, - is_variadic: bool, + is_vararg: bool, + is_kwarg: bool, } fn parameters_from_signature<'a>(docstring: &'a Docstring) -> Vec> { @@ -1186,10 +1187,26 @@ fn parameters_from_signature<'a>(docstring: &'a Docstring) -> Vec D417.py:108:5 | 108 | def f(x, *args, **kwargs): @@ -92,7 +92,7 @@ D417 Missing argument description in the docstring for `select_data`: `auto_save 157 | args: tuple, | -D417 Missing argument description in the docstring for `f`: `kwargs` +D417 Missing argument description in the docstring for `f`: `**kwargs` --> D417.py:172:5 | 170 | """ diff --git a/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__d417_unspecified.snap b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__d417_unspecified.snap index 3463b99b86..906d69d95b 100644 --- a/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__d417_unspecified.snap +++ b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__d417_unspecified.snap @@ -65,7 +65,7 @@ D417 Missing argument description in the docstring for `f`: `x` 99 | """Do something. | -D417 Missing argument description in the docstring for `f`: `args` +D417 Missing argument description in the docstring for `f`: `*args` --> D417.py:108:5 | 108 | def f(x, *args, **kwargs): @@ -92,7 +92,7 @@ D417 Missing argument description in the docstring for `select_data`: `auto_save 157 | args: tuple, | -D417 Missing argument description in the docstring for `f`: `kwargs` +D417 Missing argument description in the docstring for `f`: `**kwargs` --> D417.py:172:5 | 170 | """ diff --git a/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__d417_unspecified_ignore_var_parameters.snap b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__d417_unspecified_ignore_var_parameters.snap index 3463b99b86..906d69d95b 100644 --- a/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__d417_unspecified_ignore_var_parameters.snap +++ b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__d417_unspecified_ignore_var_parameters.snap @@ -65,7 +65,7 @@ D417 Missing argument description in the docstring for `f`: `x` 99 | """Do something. | -D417 Missing argument description in the docstring for `f`: `args` +D417 Missing argument description in the docstring for `f`: `*args` --> D417.py:108:5 | 108 | def f(x, *args, **kwargs): @@ -92,7 +92,7 @@ D417 Missing argument description in the docstring for `select_data`: `auto_save 157 | args: tuple, | -D417 Missing argument description in the docstring for `f`: `kwargs` +D417 Missing argument description in the docstring for `f`: `**kwargs` --> D417.py:172:5 | 170 | """ From 9bcf6361b669005416bcaf45516f3781aef488d3 Mon Sep 17 00:00:00 2001 From: augustelalande Date: Sat, 1 Nov 2025 01:57:50 -0400 Subject: [PATCH 5/6] allow empty lines at start of google-style section --- .../src/rules/pydoclint/rules/check_docstring.rs | 6 +++++- 1 file changed, 5 insertions(+), 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 8d7f0130cd..fda9ffda35 100644 --- a/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs +++ b/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs @@ -618,7 +618,11 @@ fn parse_parameters( fn parse_parameters_google(content: &str, content_start: TextSize) -> Vec> { let mut entries: Vec = Vec::new(); // Find first entry to determine indentation - let Some(first_arg) = content.lines().next() else { + let Some((_, first_arg)) = content + .lines() + .enumerate() + .find(|(_, line)| !line.trim().is_empty()) + else { return entries; }; let indentation = &first_arg[..first_arg.len() - first_arg.trim_start().len()]; From dfd3c9a78dfdbbdfd1841171055e72b6ea40323a Mon Sep 17 00:00:00 2001 From: augustelalande Date: Wed, 12 Nov 2025 18:00:52 -0500 Subject: [PATCH 6/6] typo --- .../ruff_linter/src/rules/pydoclint/rules/check_docstring.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 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 5064deb941..f879478800 100644 --- a/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs +++ b/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs @@ -528,7 +528,7 @@ impl<'a> ParametersSection<'a> { } } - fn extends_from_section(&mut self, section: &SectionContext<'a>, style: Option) { + fn extend_from_section(&mut self, section: &SectionContext<'a>, style: Option) { let mut new_entries = parse_parameters( section.following_lines_str(), section.following_range().start(), @@ -562,7 +562,7 @@ impl<'a> DocstringSections<'a> { | SectionKind::OtherParams | SectionKind::OtherParameters => { if let Some(ref mut parameters_section) = docstring_sections.parameters { - parameters_section.extends_from_section(§ion, style); + parameters_section.extend_from_section(§ion, style); } else { docstring_sections.parameters = Some(ParametersSection::from_section(§ion, style));