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 2f84e8501d..f879478800 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 extend_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.extend_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 } @@ -591,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()]; @@ -607,7 +638,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 { @@ -662,7 +706,6 @@ fn parse_parameters_numpy(content: &str, content_start: TextSize) -> Vec 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 } @@ -763,10 +807,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) { // Check for Sphinx directives (lines starting with ..) - these indicate the end of the @@ -1166,14 +1214,41 @@ 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_vararg: bool, + is_kwarg: 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()); + for param in function.parameters.iter_non_variadic_params() { + parameters.push(SignatureParameter { + name: param.name(), + is_vararg: false, + is_kwarg: false, + }); } + + if let Some(param) = function.parameters.vararg.as_ref() { + parameters.push(SignatureParameter { + name: param.name(), + is_vararg: true, + is_kwarg: false, + }); + } + if let Some(param) = function.parameters.kwarg.as_ref() { + parameters.push(SignatureParameter { + name: param.name(), + is_vararg: false, + is_kwarg: true, + }); + } + parameters } @@ -1209,10 +1284,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) => { @@ -1232,6 +1303,57 @@ 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_vararg || signature_param.is_kwarg) + || signature_param.name.starts_with('_') + || parameters_section.parameters.iter().any(|param| { + param.name == signature_param.name && param.has_definition + })) + { + let name = signature_param.name; + if signature_param.is_vararg { + missing_parameters.push(format!("*{name}")); + } else if signature_param.is_kwarg { + missing_parameters.push(format!("**{name}")); + } else { + missing_parameters.push(name.to_string()); + } + } + } + + 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, + }, + 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) @@ -1326,7 +1448,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| param.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..977f4e141a --- /dev/null +++ b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__D417_sections.py.snap @@ -0,0 +1,102 @@ +--- +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_missing_args`: `test`, `y`, `z` + --> 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`, `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..906d69d95b --- /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..906d69d95b --- /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..906d69d95b --- /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 bf84ea85fc..7263aece28 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/sections.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/sections.rs @@ -1243,9 +1243,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 { @@ -1828,16 +1828,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(), + ); + } } } }