diff --git a/Cargo.lock b/Cargo.lock index 2cdec5a11c..760a9551ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -981,6 +981,9 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] [[package]] name = "heck" @@ -1999,6 +2002,7 @@ dependencies = [ "strum", "strum_macros", "test-case", + "textwrap", "titlecase", "toml", "update-informer", @@ -2261,6 +2265,12 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" +[[package]] +name = "smawk" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043" + [[package]] name = "socket2" version = "0.4.7" @@ -2435,6 +2445,17 @@ dependencies = [ "syn", ] +[[package]] +name = "textwrap" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "949517c0cf1bf4ee812e2e07e08ab448e3ae0d23472aee8a06c985f0c8815b16" +dependencies = [ + "smawk", + "unicode-linebreak", + "unicode-width", +] + [[package]] name = "thiserror" version = "1.0.37" @@ -2602,6 +2623,16 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd" +[[package]] +name = "unicode-linebreak" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5faade31a542b8b35855fff6e8def199853b2da8da256da52f52f1316ee3137" +dependencies = [ + "hashbrown", + "regex", +] + [[package]] name = "unicode-normalization" version = "0.1.22" @@ -2611,6 +2642,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + [[package]] name = "unicode-xid" version = "0.2.4" diff --git a/Cargo.toml b/Cargo.toml index d016b0907b..5f3347b3cb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ serde = { version = "1.0.143", features = ["derive"] } serde_json = { version = "1.0.83" } strum = { version = "0.24.1", features = ["strum_macros"] } strum_macros = { version = "0.24.3" } +textwrap = { version = "0.15.1" } titlecase = { version = "2.2.1" } toml = { version = "0.5.9" } update-informer = { version = "0.5.0", default_features = false, features = ["pypi"], optional = true } diff --git a/README.md b/README.md index 096047f985..e23409f4fb 100644 --- a/README.md +++ b/README.md @@ -221,7 +221,7 @@ ruff also implements some of the most popular Flake8 plugins natively, including - [`flake8-print`](https://pypi.org/project/flake8-print/) - [`flake8-comprehensions`](https://pypi.org/project/flake8-comprehensions/) (15/16) - [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (3/32) -- [`flake8-docstrings`](https://pypi.org/project/flake8-docstrings/) (37/48) +- [`flake8-docstrings`](https://pypi.org/project/flake8-docstrings/) (41/48) - [`pyupgrade`](https://pypi.org/project/pyupgrade/) (8/34) Beyond rule-set parity, ruff suffers from the following limitations vis-à-vis Flake8: @@ -349,6 +349,7 @@ The 🛠 emoji indicates that a rule is automatically fixable by the `--fix` com | D413 | BlankLineAfterLastSection | Missing blank line after last section ("Returns") | | | | D414 | NonEmptySection | Section has no content ("Returns") | | | | D415 | EndsInPunctuation | First line should end with a period, question mark, or exclamation point | | | +| D416 | SectionNameEndsInColon | Section name should end with a colon ("Returns") | | | | D417 | DocumentAllArguments | Missing argument descriptions in the docstring: `x`, `y` | | | | D418 | SkipDocstring | Function decorated with @overload shouldn't contain a docstring | | | | D419 | NonEmpty | Docstring is empty | | | diff --git a/resources/test/fixtures/canonical_google_examples.py b/resources/test/fixtures/canonical_google_examples.py new file mode 100644 index 0000000000..301f83c46f --- /dev/null +++ b/resources/test/fixtures/canonical_google_examples.py @@ -0,0 +1,108 @@ +"""A one line summary of the module or program, terminated by a period. + +Leave one blank line. The rest of this docstring should contain an +overall description of the module or program. Optionally, it may also +contain a brief description of exported classes and functions and/or usage +examples. + + Typical usage example: + + foo = ClassFoo() + bar = foo.FunctionBar() +""" +# above: "2.8.2 Modules" section example +# https://google.github.io/styleguide/pyguide.html#382-modules + +# Examples from the official "Google Python Style Guide" documentation: +# * As HTML: https://google.github.io/styleguide/pyguide.html +# * Source Markdown: +# https://github.com/google/styleguide/blob/gh-pages/pyguide.md + +import os +from .expected import Expectation + +expectation = Expectation() +expect = expectation.expect + +# module docstring expected violations: +expectation.expected.add(( + os.path.normcase(__file__), + "D213: Multi-line docstring summary should start at the second line")) + + +# "3.8.3 Functions and Methods" section example +# https://google.github.io/styleguide/pyguide.html#383-functions-and-methods +@expect("D213: Multi-line docstring summary should start at the second line", + arg_count=3) +@expect("D401: First line should be in imperative mood " + "(perhaps 'Fetch', not 'Fetches')", arg_count=3) +@expect("D406: Section name should end with a newline " + "('Raises', not 'Raises:')", arg_count=3) +@expect("D406: Section name should end with a newline " + "('Returns', not 'Returns:')", arg_count=3) +@expect("D407: Missing dashed underline after section ('Raises')", arg_count=3) +@expect("D407: Missing dashed underline after section ('Returns')", + arg_count=3) +@expect("D413: Missing blank line after last section ('Raises')", arg_count=3) +def fetch_bigtable_rows(big_table, keys, other_silly_variable=None): + """Fetches rows from a Bigtable. + + Retrieves rows pertaining to the given keys from the Table instance + represented by big_table. Silly things may happen if + other_silly_variable is not None. + + Args: + big_table: An open Bigtable Table instance. + keys: A sequence of strings representing the key of each table row + to fetch. + other_silly_variable: Another optional variable, that has a much + longer name than the other args, and which does nothing. + + Returns: + A dict mapping keys to the corresponding table row data + fetched. Each row is represented as a tuple of strings. For + example: + + {'Serak': ('Rigel VII', 'Preparer'), + 'Zim': ('Irk', 'Invader'), + 'Lrrr': ('Omicron Persei 8', 'Emperor')} + + If a key from the keys argument is missing from the dictionary, + then that row was not found in the table. + + Raises: + IOError: An error occurred accessing the bigtable.Table object. + """ + + +# "3.8.4 Classes" section example +# https://google.github.io/styleguide/pyguide.html#384-classes +@expect("D203: 1 blank line required before class docstring (found 0)") +@expect("D213: Multi-line docstring summary should start at the second line") +@expect("D406: Section name should end with a newline " + "('Attributes', not 'Attributes:')") +@expect("D407: Missing dashed underline after section ('Attributes')") +@expect("D413: Missing blank line after last section ('Attributes')") +class SampleClass: + """Summary of class here. + + Longer class information.... + Longer class information.... + + Attributes: + likes_spam: A boolean indicating if we like SPAM or not. + eggs: An integer count of the eggs we have laid. + """ + + @expect("D401: First line should be in imperative mood " + "(perhaps 'Init', not 'Inits')", arg_count=2) + def __init__(self, likes_spam=False): + """Inits SampleClass with blah.""" + if self: # added to avoid NameError when run via @expect decorator + self.likes_spam = likes_spam + self.eggs = 0 + + @expect("D401: First line should be in imperative mood " + "(perhaps 'Perform', not 'Performs')", arg_count=1) + def public_method(self): + """Performs operation blah.""" diff --git a/src/checks.rs b/src/checks.rs index 2ca6edf3d2..aea68a778c 100644 --- a/src/checks.rs +++ b/src/checks.rs @@ -192,6 +192,7 @@ pub enum CheckCode { D413, D414, D415, + D416, D417, D418, D419, @@ -330,6 +331,7 @@ pub enum CheckKind { PublicModule, PublicNestedClass, PublicPackage, + SectionNameEndsInColon(String), SectionNotOverIndented(String), SectionUnderlineAfterName(String), SectionUnderlineMatchesSectionLength(String), @@ -473,6 +475,8 @@ impl CheckCode { CheckCode::D211 => CheckKind::NoBlankLineBeforeClass(1), CheckCode::D212 => CheckKind::MultiLineSummaryFirstLine, CheckCode::D213 => CheckKind::MultiLineSummarySecondLine, + CheckCode::D214 => CheckKind::SectionNotOverIndented("Returns".to_string()), + CheckCode::D215 => CheckKind::SectionUnderlineNotOverIndented("Returns".to_string()), CheckCode::D300 => CheckKind::UsesTripleQuotes, CheckCode::D400 => CheckKind::EndsInPeriod, CheckCode::D402 => CheckKind::NoSignature, @@ -493,13 +497,12 @@ impl CheckCode { CheckCode::D413 => CheckKind::BlankLineAfterLastSection("Returns".to_string()), CheckCode::D414 => CheckKind::NonEmptySection("Returns".to_string()), CheckCode::D415 => CheckKind::EndsInPunctuation, - CheckCode::D418 => CheckKind::SkipDocstring, - CheckCode::D419 => CheckKind::NonEmpty, - CheckCode::D214 => CheckKind::SectionNotOverIndented("Returns".to_string()), - CheckCode::D215 => CheckKind::SectionUnderlineNotOverIndented("Returns".to_string()), + CheckCode::D416 => CheckKind::SectionNameEndsInColon("Returns".to_string()), CheckCode::D417 => { CheckKind::DocumentAllArguments(vec!["x".to_string(), "y".to_string()]) } + CheckCode::D418 => CheckKind::SkipDocstring, + CheckCode::D419 => CheckKind::NonEmpty, // Meta CheckCode::M001 => CheckKind::UnusedNOQA(None), } @@ -626,6 +629,7 @@ impl CheckKind { CheckKind::PublicModule => &CheckCode::D100, CheckKind::PublicNestedClass => &CheckCode::D106, CheckKind::PublicPackage => &CheckCode::D104, + CheckKind::SectionNameEndsInColon(_) => &CheckCode::D416, CheckKind::SectionNotOverIndented(_) => &CheckCode::D214, CheckKind::SectionUnderlineAfterName(_) => &CheckCode::D408, CheckKind::SectionUnderlineMatchesSectionLength(_) => &CheckCode::D409, @@ -982,6 +986,9 @@ impl CheckKind { CheckKind::SectionUnderlineNotOverIndented(name) => { format!("Section underline is over-indented (\"{name}\")") } + CheckKind::SectionNameEndsInColon(name) => { + format!("Section name should end with a colon (\"{name}\")") + } CheckKind::DocumentAllArguments(names) => { if names.len() == 1 { let name = &names[0]; diff --git a/src/docstrings/docstring_checks.rs b/src/docstrings/docstring_checks.rs index 4d2c940533..da57c49926 100644 --- a/src/docstrings/docstring_checks.rs +++ b/src/docstrings/docstring_checks.rs @@ -7,7 +7,9 @@ use rustpython_ast::{Constant, Expr, ExprKind, Location, Stmt, StmtKind}; use crate::ast::types::Range; use crate::check_ast::Checker; use crate::checks::{Check, CheckCode, CheckKind}; -use crate::docstrings::sections::{check_numpy_section, section_contexts}; +use crate::docstrings::sections::{ + check_google_section, check_numpy_section, section_contexts, SectionStyle, +}; use crate::docstrings::types::{Definition, DefinitionKind, Documentable}; use crate::visibility::{is_init, is_magic, is_overload, Modifier, Visibility, VisibleScope}; @@ -668,9 +670,20 @@ pub fn check_sections(checker: &mut Checker, definition: &Definition) { if lines.len() < 2 { return; } - for context in §ion_contexts(&lines) { + + // First, try to interpret as NumPy-style sections. + let mut found_numpy_section = false; + for context in §ion_contexts(&lines, &SectionStyle::NumPy) { + found_numpy_section = true; check_numpy_section(checker, definition, context); } + + // If no such sections were identified, interpret as Google-style sections. + if !found_numpy_section { + for context in §ion_contexts(&lines, &SectionStyle::Google) { + check_google_section(checker, definition, context); + } + } } } } diff --git a/src/docstrings/sections.rs b/src/docstrings/sections.rs index d8241631e3..6b3327ac52 100644 --- a/src/docstrings/sections.rs +++ b/src/docstrings/sections.rs @@ -2,6 +2,7 @@ use itertools::Itertools; use std::collections::BTreeSet; use once_cell::sync::Lazy; +use regex::Regex; use rustpython_ast::{Arg, Expr, Location, StmtKind}; use titlecase::titlecase; @@ -30,7 +31,7 @@ static NUMPY_SECTION_NAMES: Lazy> = Lazy::new(|| { ]) }); -static NUMPY_SECTION_NAMES_LOWERCASE: Lazy> = Lazy::new(|| { +static LOWERCASE_NUMPY_SECTION_NAMES: Lazy> = Lazy::new(|| { BTreeSet::from([ "short summary", "extended summary", @@ -48,39 +49,92 @@ static NUMPY_SECTION_NAMES_LOWERCASE: Lazy> = Lazy::new(| ]) }); -// TODO(charlie): Include Google section names. -// static GOOGLE_SECTION_NAMES: Lazy> = Lazy::new(|| { -// BTreeSet::from([ -// "Args", -// "Arguments", -// "Attention", -// "Attributes", -// "Caution", -// "Danger", -// "Error", -// "Example", -// "Examples", -// "Hint", -// "Important", -// "Keyword Args", -// "Keyword Arguments", -// "Methods", -// "Note", -// "Notes", -// "Return", -// "Returns", -// "Raises", -// "References", -// "See Also", -// "Tip", -// "Todo", -// "Warning", -// "Warnings", -// "Warns", -// "Yield", -// "Yields", -// ]) -// }); +static GOOGLE_SECTION_NAMES: Lazy> = Lazy::new(|| { + BTreeSet::from([ + "Args", + "Arguments", + "Attention", + "Attributes", + "Caution", + "Danger", + "Error", + "Example", + "Examples", + "Hint", + "Important", + "Keyword Args", + "Keyword Arguments", + "Methods", + "Note", + "Notes", + "Return", + "Returns", + "Raises", + "References", + "See Also", + "Tip", + "Todo", + "Warning", + "Warnings", + "Warns", + "Yield", + "Yields", + ]) +}); + +static LOWERCASE_GOOGLE_SECTION_NAMES: Lazy> = Lazy::new(|| { + BTreeSet::from([ + "args", + "arguments", + "attention", + "attributes", + "caution", + "danger", + "error", + "example", + "examples", + "hint", + "important", + "keyword args", + "keyword arguments", + "methods", + "note", + "notes", + "return", + "returns", + "raises", + "references", + "see also", + "tip", + "todo", + "warning", + "warnings", + "warns", + "yield", + "yields", + ]) +}); + +pub enum SectionStyle { + NumPy, + Google, +} + +impl SectionStyle { + fn section_names(&self) -> &Lazy> { + match self { + SectionStyle::NumPy => &NUMPY_SECTION_NAMES, + SectionStyle::Google => &GOOGLE_SECTION_NAMES, + } + } + + fn lowercase_section_names(&self) -> &Lazy> { + match self { + SectionStyle::NumPy => &LOWERCASE_NUMPY_SECTION_NAMES, + SectionStyle::Google => &LOWERCASE_GOOGLE_SECTION_NAMES, + } + } +} fn indentation<'a>(checker: &'a mut Checker, docstring: &Expr) -> &'a str { let range = range_for(docstring); @@ -103,8 +157,10 @@ fn leading_words(line: &str) -> String { .collect() } -fn suspected_as_section(line: &str) -> bool { - NUMPY_SECTION_NAMES_LOWERCASE.contains(&leading_words(line).to_lowercase().as_str()) +fn suspected_as_section(line: &str, style: &SectionStyle) -> bool { + style + .lowercase_section_names() + .contains(&leading_words(line).to_lowercase().as_str()) } #[derive(Debug)] @@ -145,12 +201,12 @@ fn is_docstring_section(context: &SectionContext) -> bool { } /// Extract all `SectionContext` values from a docstring. -pub fn section_contexts<'a>(lines: &'a [&'a str]) -> Vec> { +pub fn section_contexts<'a>(lines: &'a [&'a str], style: &SectionStyle) -> Vec> { let suspected_section_indices: Vec = lines .iter() .enumerate() .filter_map(|(lineno, line)| { - if lineno > 0 && suspected_as_section(line) { + if lineno > 0 && suspected_as_section(line, style) { Some(lineno) } else { None @@ -322,14 +378,23 @@ fn check_blanks_and_section_underline( } } -fn check_common_section(checker: &mut Checker, definition: &Definition, context: &SectionContext) { +fn check_common_section( + checker: &mut Checker, + definition: &Definition, + context: &SectionContext, + style: &SectionStyle, +) { let docstring = definition .docstring .expect("Sections are only available for docstrings."); if checker.settings.enabled.contains(&CheckCode::D405) { - if !NUMPY_SECTION_NAMES.contains(&context.section_name.as_str()) - && NUMPY_SECTION_NAMES.contains(titlecase(&context.section_name).as_str()) + if !style + .section_names() + .contains(&context.section_name.as_str()) + && style + .section_names() + .contains(titlecase(&context.section_name).as_str()) { checker.add_check(Check::new( CheckKind::CapitalizeSectionName(context.section_name.to_string()), @@ -383,7 +448,7 @@ fn check_common_section(checker: &mut Checker, definition: &Definition, context: fn check_missing_args( checker: &mut Checker, definition: &Definition, - docstrings_args: BTreeSet<&str>, + docstrings_args: &BTreeSet<&str>, ) { if let DefinitionKind::Function(parent) | DefinitionKind::NestedFunction(parent) @@ -476,7 +541,7 @@ fn check_parameters_section( } } // Validate that all arguments were documented. - check_missing_args(checker, definition, docstring_args); + check_missing_args(checker, definition, &docstring_args); } pub fn check_numpy_section( @@ -484,7 +549,7 @@ pub fn check_numpy_section( definition: &Definition, context: &SectionContext, ) { - check_common_section(checker, definition, context); + check_common_section(checker, definition, context, &SectionStyle::NumPy); check_blanks_and_section_underline(checker, definition, context); if checker.settings.enabled.contains(&CheckCode::D406) { @@ -510,3 +575,74 @@ pub fn check_numpy_section( } } } + +// See: `GOOGLE_ARGS_REGEX` in `pydocstyle/checker.py`. +static GOOGLE_ARGS_REGEX: Lazy = + Lazy::new(|| Regex::new(r"^\s*(\w+)\s*(\(.*?\))?\s*:\n?\s*.+").expect("Invalid regex")); + +fn check_args_section(checker: &mut Checker, definition: &Definition, context: &SectionContext) { + let mut args_sections: Vec = vec![]; + for line in textwrap::dedent(&context.following_lines.join("\n")).lines() { + if line + .chars() + .next() + .map(|char| char.is_whitespace()) + .unwrap_or(true) + { + // This is a continuation of documentation for the last + // parameter because it does start with whitespace. + if let Some(current) = args_sections.last_mut() { + current.push_str(line); + } + } else { + // This line is the start of documentation for the next + // parameter because it doesn't start with any whitespace. + args_sections.push(line.to_string()); + } + } + + check_missing_args( + checker, + definition, + // Collect the list of arguments documented in the docstring. + &BTreeSet::from_iter(args_sections.iter().filter_map(|section| { + match GOOGLE_ARGS_REGEX.captures(section.as_str()) { + Some(caps) => caps.get(1).map(|arg_name| arg_name.as_str()), + None => None, + } + })), + ) +} + +pub fn check_google_section( + checker: &mut Checker, + definition: &Definition, + context: &SectionContext, +) { + check_common_section(checker, definition, context, &SectionStyle::Google); + check_blanks_and_section_underline(checker, definition, context); + + if checker.settings.enabled.contains(&CheckCode::D416) { + let suffix = context + .line + .trim() + .strip_prefix(&context.section_name) + .unwrap(); + if suffix != ":" { + let docstring = definition + .docstring + .expect("Sections are only available for docstrings."); + checker.add_check(Check::new( + CheckKind::SectionNameEndsInColon(context.section_name.to_string()), + range_for(docstring), + )) + } + } + + if checker.settings.enabled.contains(&CheckCode::D417) { + let capitalized_section_name = titlecase(&context.section_name); + if capitalized_section_name == "Args" || capitalized_section_name == "Arguments" { + check_args_section(checker, definition, context); + } + } +} diff --git a/src/linter.rs b/src/linter.rs index d94bb2c207..8a0bba6173 100644 --- a/src/linter.rs +++ b/src/linter.rs @@ -295,8 +295,10 @@ mod tests { #[test_case(CheckCode::D413, Path::new("sections.py"); "D413")] #[test_case(CheckCode::D414, Path::new("sections.py"); "D414")] #[test_case(CheckCode::D415, Path::new("D.py"); "D415")] + #[test_case(CheckCode::D416, Path::new("D.py"); "D416")] #[test_case(CheckCode::D417, Path::new("sections.py"); "D417_0")] #[test_case(CheckCode::D417, Path::new("canonical_numpy_examples.py"); "D417_1")] + #[test_case(CheckCode::D417, Path::new("canonical_google_examples.py"); "D417_2")] #[test_case(CheckCode::D418, Path::new("D.py"); "D418")] #[test_case(CheckCode::D419, Path::new("D.py"); "D419")] #[test_case(CheckCode::E402, Path::new("E402.py"); "E402")] diff --git a/src/snapshots/ruff__linter__tests__D407_sections.py.snap b/src/snapshots/ruff__linter__tests__D407_sections.py.snap index 15200d240c..f10bbbd0f8 100644 --- a/src/snapshots/ruff__linter__tests__D407_sections.py.snap +++ b/src/snapshots/ruff__linter__tests__D407_sections.py.snap @@ -47,4 +47,94 @@ expression: checks row: 262 column: 8 fix: ~ +- kind: + DashedUnderlineAfterSection: Args + location: + row: 269 + column: 5 + end_location: + row: 274 + column: 8 + fix: ~ +- kind: + DashedUnderlineAfterSection: Args + location: + row: 284 + column: 9 + end_location: + row: 292 + column: 12 + fix: ~ +- kind: + DashedUnderlineAfterSection: Args + location: + row: 301 + column: 5 + end_location: + row: 306 + column: 8 + fix: ~ +- kind: + DashedUnderlineAfterSection: Args + location: + row: 313 + column: 9 + end_location: + row: 319 + column: 12 + fix: ~ +- kind: + DashedUnderlineAfterSection: Args + location: + row: 325 + column: 9 + end_location: + row: 330 + column: 12 + fix: ~ +- kind: + DashedUnderlineAfterSection: Args + location: + row: 337 + column: 9 + end_location: + row: 343 + column: 12 + fix: ~ +- kind: + DashedUnderlineAfterSection: Args + location: + row: 350 + column: 9 + end_location: + row: 355 + column: 12 + fix: ~ +- kind: + DashedUnderlineAfterSection: Args + location: + row: 362 + column: 9 + end_location: + row: 367 + column: 12 + fix: ~ +- kind: + DashedUnderlineAfterSection: Args + location: + row: 371 + column: 9 + end_location: + row: 382 + column: 12 + fix: ~ +- kind: + DashedUnderlineAfterSection: Args + location: + row: 490 + column: 9 + end_location: + row: 497 + column: 12 + fix: ~ diff --git a/src/snapshots/ruff__linter__tests__D416_D.py.snap b/src/snapshots/ruff__linter__tests__D416_D.py.snap new file mode 100644 index 0000000000..60c615f917 --- /dev/null +++ b/src/snapshots/ruff__linter__tests__D416_D.py.snap @@ -0,0 +1,6 @@ +--- +source: src/linter.rs +expression: checks +--- +[] + diff --git a/src/snapshots/ruff__linter__tests__D417_canonical_google_examples.py.snap b/src/snapshots/ruff__linter__tests__D417_canonical_google_examples.py.snap new file mode 100644 index 0000000000..60c615f917 --- /dev/null +++ b/src/snapshots/ruff__linter__tests__D417_canonical_google_examples.py.snap @@ -0,0 +1,6 @@ +--- +source: src/linter.rs +expression: checks +--- +[] + diff --git a/src/snapshots/ruff__linter__tests__D417_sections.py.snap b/src/snapshots/ruff__linter__tests__D417_sections.py.snap index 0635c6e9bc..f8177d8f02 100644 --- a/src/snapshots/ruff__linter__tests__D417_sections.py.snap +++ b/src/snapshots/ruff__linter__tests__D417_sections.py.snap @@ -2,6 +2,73 @@ source: src/linter.rs expression: checks --- +- kind: + DocumentAllArguments: + - y + location: + row: 283 + column: 5 + end_location: + row: 296 + column: 1 + fix: ~ +- kind: + DocumentAllArguments: + - y + location: + row: 300 + column: 1 + end_location: + row: 309 + column: 1 + fix: ~ +- kind: + DocumentAllArguments: + - test + - y + - z + location: + row: 324 + column: 5 + end_location: + row: 332 + column: 5 + fix: ~ +- kind: + DocumentAllArguments: + - test + - y + - z + location: + row: 336 + column: 5 + end_location: + row: 345 + column: 5 + fix: ~ +- kind: + DocumentAllArguments: + - a + - y + - z + location: + row: 349 + column: 5 + end_location: + row: 357 + column: 5 + fix: ~ +- kind: + DocumentAllArguments: + - a + - b + location: + row: 361 + column: 5 + end_location: + row: 369 + column: 5 + fix: ~ - kind: DocumentAllArguments: - y @@ -47,4 +114,14 @@ expression: checks row: 471 column: 5 fix: ~ +- kind: + DocumentAllArguments: + - y + location: + row: 489 + column: 5 + end_location: + row: 498 + column: 1 + fix: ~