diff --git a/README.md b/README.md index 4c1dc82254..185b28b2f3 100644 --- a/README.md +++ b/README.md @@ -321,34 +321,37 @@ The 🛠 emoji indicates that a rule is automatically fixable by the `--fix` com | D106 | PublicNestedClass | Missing docstring in public nested class | | | | D107 | PublicInit | Missing docstring in __init__ | | | | D200 | FitsOnOneLine | One-line docstring should fit on one line | | | +| D201 | NoBlankLineBeforeFunction | No blank lines allowed before function docstring (found 1) | | | +| D202 | NoBlankLineAfterFunction | No blank lines allowed after function docstring (found 1) | | | +| D203 | OneBlankLineBeforeClass | 1 blank line required before class docstring | | | +| D204 | OneBlankLineAfterClass | 1 blank line required after class docstring | | | | D205 | NoBlankLineAfterSummary | 1 blank line required between summary line and description | | | | D209 | NewLineAfterLastParagraph | Multi-line docstring closing quotes should be on a separate line | | | | D210 | NoSurroundingWhitespace | No whitespaces allowed surrounding docstring text | | | +| D211 | NoBlankLineBeforeClass | No blank lines allowed before class docstring | | | | D212 | MultiLineSummaryFirstLine | Multi-line docstring summary should start at the first line | | | | D213 | MultiLineSummarySecondLine | Multi-line docstring summary should start at the second line | | | +| D214 | SectionNotOverIndented | Section is over-indented ("Returns") | | | +| D215 | SectionUnderlineNotOverIndented | Section underline is over-indented ("Returns") | | | | D300 | UsesTripleQuotes | Use """triple double quotes""" | | | | D400 | EndsInPeriod | First line should end with a period | | | | D402 | NoSignature | First line should not be the function's 'signature' | | | | D403 | FirstLineCapitalized | First word of the first line should be properly capitalized | | | | D404 | NoThisPrefix | First word of the docstring should not be `This` | | | -| D415 | EndsInPunctuation | First line should end with a period, question mark, or exclamation point | | | -| D418 | SkipDocstring | Function decorated with @overload shouldn't contain a docstring | | | -| D419 | NonEmpty | Docstring is empty | | | -| D201 | NoBlankLineBeforeFunction | No blank lines allowed before function docstring (found 1) | | | -| D202 | NoBlankLineAfterFunction | No blank lines allowed after function docstring (found 1) | | | -| D211 | NoBlankLineBeforeClass | No blank lines allowed before class docstring | | | -| D203 | OneBlankLineBeforeClass | 1 blank line required before class docstring | | | -| D204 | OneBlankLineAfterClass | 1 blank line required after class docstring | | | | D405 | CapitalizeSectionName | Section name should be properly capitalized ("returns") | | | -| D413 | BlankLineAfterLastSection | Missing blank line after last section ("Returns") | | | -| D410 | BlankLineAfterSection | Missing blank line after section ("Returns") | | | -| D411 | BlankLineBeforeSection | Missing blank line before section ("Returns") | | | | D406 | NewLineAfterSectionName | Section name should end with a newline ("Returns") | | | | D407 | DashedUnderlineAfterSection | Missing dashed underline after section ("Returns") | | | | D408 | SectionUnderlineAfterName | Section underline should be in the line following the section's name ("Returns") | | | | D409 | SectionUnderlineMatchesSectionLength | Section underline should match the length of its name ("Returns") | | | +| D410 | BlankLineAfterSection | Missing blank line after section ("Returns") | | | +| D411 | BlankLineBeforeSection | Missing blank line before section ("Returns") | | | | D412 | NoBlankLinesBetweenHeaderAndContent | No blank lines allowed between a section header and its content ("Returns") | | | +| 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 | | | +| 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 | | | | M001 | UnusedNOQA | Unused `noqa` directive | | 🛠 | ## Integrations diff --git a/resources/test/fixtures/canonical_numpy_examples.py b/resources/test/fixtures/canonical_numpy_examples.py new file mode 100644 index 0000000000..f0732b3790 --- /dev/null +++ b/resources/test/fixtures/canonical_numpy_examples.py @@ -0,0 +1,163 @@ +"""This is the docstring for the example.py module. Modules names should +have short, all-lowercase names. The module name may have underscores if +this improves readability. + +Every module should have a docstring at the very top of the file. The +module's docstring may extend over multiple lines. If your docstring does +extend over multiple lines, the closing three quotation marks must be on +a line by itself, preferably preceded by a blank line. + +""" + +# Example source file from the official "numpydoc docstring guide" +# documentation (with the modification of commenting out all the original +# ``import`` lines, plus adding this note and ``Expectation`` code): +# * As HTML: https://numpydoc.readthedocs.io/en/latest/example.html +# * Source Python: +# https://github.com/numpy/numpydoc/blob/master/doc/example.py + +# from __future__ import division, absolute_import, print_function +# +# import os # standard library imports first +# +# Do NOT import using *, e.g. from numpy import * +# +# Import the module using +# +# import numpy +# +# instead or import individual functions as needed, e.g +# +# from numpy import array, zeros +# +# If you prefer the use of abbreviated module names, we suggest the +# convention used by NumPy itself:: +# +# import numpy as np +# import matplotlib as mpl +# import matplotlib.pyplot as plt +# +# These abbreviated names are not to be used in docstrings; users must +# be able to paste and execute docstrings after importing only the +# numpy module itself, unabbreviated. + +import os +from .expected import Expectation + +expectation = Expectation() +expect = expectation.expect + +# module docstring expected violations: +expectation.expected.add(( + os.path.normcase(__file__), + "D205: 1 blank line required between summary line and description " + "(found 0)")) +expectation.expected.add(( + os.path.normcase(__file__), + "D213: Multi-line docstring summary should start at the second line")) +expectation.expected.add(( + os.path.normcase(__file__), + "D400: First line should end with a period (not 'd')")) +expectation.expected.add(( + os.path.normcase(__file__), + "D404: First word of the docstring should not be `This`")) +expectation.expected.add(( + os.path.normcase(__file__), + "D415: First line should end with a period, question mark, or exclamation " + "point (not 'd')")) + + +@expect("D213: Multi-line docstring summary should start at the second line", + arg_count=3) +@expect("D401: First line should be in imperative mood; try rephrasing " + "(found 'A')", arg_count=3) +@expect("D413: Missing blank line after last section ('Examples')", + arg_count=3) +def foo(var1, var2, long_var_name='hi'): + r"""A one-line summary that does not use variable names. + + Several sentences providing an extended description. Refer to + variables using back-ticks, e.g. `var`. + + Parameters + ---------- + var1 : array_like + Array_like means all those objects -- lists, nested lists, etc. -- + that can be converted to an array. We can also refer to + variables like `var1`. + var2 : int + The type above can either refer to an actual Python type + (e.g. ``int``), or describe the type of the variable in more + detail, e.g. ``(N,) ndarray`` or ``array_like``. + long_var_name : {'hi', 'ho'}, optional + Choices in brackets, default first when optional. + + Returns + ------- + type + Explanation of anonymous return value of type ``type``. + describe : type + Explanation of return value named `describe`. + out : type + Explanation of `out`. + type_without_description + + Other Parameters + ---------------- + only_seldom_used_keywords : type + Explanation + common_parameters_listed_above : type + Explanation + + Raises + ------ + BadException + Because you shouldn't have done that. + + See Also + -------- + numpy.array : Relationship (optional). + numpy.ndarray : Relationship (optional), which could be fairly long, in + which case the line wraps here. + numpy.dot, numpy.linalg.norm, numpy.eye + + Notes + ----- + Notes about the implementation algorithm (if needed). + + This can have multiple paragraphs. + + You may include some math: + + .. math:: X(e^{j\omega } ) = x(n)e^{ - j\omega n} + + And even use a Greek symbol like :math:`\omega` inline. + + References + ---------- + Cite the relevant literature, e.g. [1]_. You may also cite these + references in the notes section above. + + .. [1] O. McNoleg, "The integration of GIS, remote sensing, + expert systems and adaptive co-kriging for environmental habitat + modelling of the Highland Haggis using object-oriented, fuzzy-logic + and neural-network techniques," Computers & Geosciences, vol. 22, + pp. 585-588, 1996. + + Examples + -------- + These are written in doctest format, and should illustrate how to + use the function. + + >>> a = [1, 2, 3] + >>> print([x + 3 for x in a]) + [4, 5, 6] + >>> print("a\nb") + a + b + """ + # After closing class docstring, there should be one blank line to + # separate following codes (according to PEP257). + # But for function, method and module, there should be no blank lines + # after closing the docstring. + pass diff --git a/src/check_ast.rs b/src/check_ast.rs index 7b1522b80e..5b58db0bd8 100644 --- a/src/check_ast.rs +++ b/src/check_ast.rs @@ -1657,7 +1657,6 @@ impl<'a> Checker<'a> { fn handle_node_delete(&mut self, expr: &Expr) { if let ExprKind::Name { id, .. } = &expr.node { - // Check if we're on a conditional branch. if operations::on_conditional_branch(&self.parent_stack, &self.parents) { return; } @@ -1996,20 +1995,23 @@ impl<'a> Checker<'a> { if self.settings.enabled.contains(&CheckCode::D418) { docstring_checks::if_needed(self, &docstring); } - if self.settings.enabled.contains(&CheckCode::D407) - || self.settings.enabled.contains(&CheckCode::D414) + if self.settings.enabled.contains(&CheckCode::D212) + || self.settings.enabled.contains(&CheckCode::D214) + || self.settings.enabled.contains(&CheckCode::D215) + || self.settings.enabled.contains(&CheckCode::D405) + || self.settings.enabled.contains(&CheckCode::D406) + || self.settings.enabled.contains(&CheckCode::D407) || self.settings.enabled.contains(&CheckCode::D407) - || self.settings.enabled.contains(&CheckCode::D212) || self.settings.enabled.contains(&CheckCode::D408) || self.settings.enabled.contains(&CheckCode::D409) - || self.settings.enabled.contains(&CheckCode::D414) - || self.settings.enabled.contains(&CheckCode::D412) - || self.settings.enabled.contains(&CheckCode::D414) - || self.settings.enabled.contains(&CheckCode::D405) - || self.settings.enabled.contains(&CheckCode::D413) || self.settings.enabled.contains(&CheckCode::D410) || self.settings.enabled.contains(&CheckCode::D411) - || self.settings.enabled.contains(&CheckCode::D406) + || self.settings.enabled.contains(&CheckCode::D412) + || self.settings.enabled.contains(&CheckCode::D413) + || self.settings.enabled.contains(&CheckCode::D414) + || self.settings.enabled.contains(&CheckCode::D414) + || self.settings.enabled.contains(&CheckCode::D414) + || self.settings.enabled.contains(&CheckCode::D417) { docstring_checks::check_sections(self, &docstring); } diff --git a/src/checks.rs b/src/checks.rs index 19d4718655..2ca6edf3d2 100644 --- a/src/checks.rs +++ b/src/checks.rs @@ -174,6 +174,8 @@ pub enum CheckCode { D211, D212, D213, + D214, + D215, D300, D400, D402, @@ -190,6 +192,7 @@ pub enum CheckCode { D413, D414, D415, + D417, D418, D419, // Meta @@ -298,6 +301,7 @@ pub enum CheckKind { BlankLineBeforeSection(String), CapitalizeSectionName(String), DashedUnderlineAfterSection(String), + DocumentAllArguments(Vec), EndsInPeriod, EndsInPunctuation, FirstLineCapitalized, @@ -326,8 +330,10 @@ pub enum CheckKind { PublicModule, PublicNestedClass, PublicPackage, + SectionNotOverIndented(String), SectionUnderlineAfterName(String), SectionUnderlineMatchesSectionLength(String), + SectionUnderlineNotOverIndented(String), SkipDocstring, UsesTripleQuotes, // Meta @@ -489,6 +495,11 @@ impl CheckCode { 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::D417 => { + CheckKind::DocumentAllArguments(vec!["x".to_string(), "y".to_string()]) + } // Meta CheckCode::M001 => CheckKind::UnusedNOQA(None), } @@ -586,6 +597,7 @@ impl CheckKind { CheckKind::BlankLineBeforeSection(_) => &CheckCode::D411, CheckKind::CapitalizeSectionName(_) => &CheckCode::D405, CheckKind::DashedUnderlineAfterSection(_) => &CheckCode::D407, + CheckKind::DocumentAllArguments(_) => &CheckCode::D417, CheckKind::EndsInPeriod => &CheckCode::D400, CheckKind::EndsInPunctuation => &CheckCode::D415, CheckKind::FirstLineCapitalized => &CheckCode::D403, @@ -614,8 +626,10 @@ impl CheckKind { CheckKind::PublicModule => &CheckCode::D100, CheckKind::PublicNestedClass => &CheckCode::D106, CheckKind::PublicPackage => &CheckCode::D104, + CheckKind::SectionNotOverIndented(_) => &CheckCode::D214, CheckKind::SectionUnderlineAfterName(_) => &CheckCode::D408, CheckKind::SectionUnderlineMatchesSectionLength(_) => &CheckCode::D409, + CheckKind::SectionUnderlineNotOverIndented(_) => &CheckCode::D215, CheckKind::SkipDocstring => &CheckCode::D418, CheckKind::UsesTripleQuotes => &CheckCode::D300, // Meta @@ -962,6 +976,21 @@ impl CheckKind { ) } CheckKind::NonEmptySection(name) => format!("Section has no content (\"{name}\")"), + CheckKind::SectionNotOverIndented(name) => { + format!("Section is over-indented (\"{name}\")") + } + CheckKind::SectionUnderlineNotOverIndented(name) => { + format!("Section underline is over-indented (\"{name}\")") + } + CheckKind::DocumentAllArguments(names) => { + if names.len() == 1 { + let name = &names[0]; + format!("Missing argument description in the docstring: `{name}`") + } else { + let names = names.iter().map(|name| format!("`{name}`")).join(", "); + format!("Missing argument descriptions in the docstring: {names}") + } + } // Meta CheckKind::UnusedNOQA(codes) => match codes { None => "Unused `noqa` directive".to_string(), diff --git a/src/docstrings/sections.rs b/src/docstrings/sections.rs index 3634c8cca9..d8241631e3 100644 --- a/src/docstrings/sections.rs +++ b/src/docstrings/sections.rs @@ -1,12 +1,16 @@ +use itertools::Itertools; use std::collections::BTreeSet; use once_cell::sync::Lazy; +use rustpython_ast::{Arg, Expr, Location, StmtKind}; use titlecase::titlecase; +use crate::ast::types::Range; use crate::check_ast::Checker; use crate::checks::{Check, CheckCode, CheckKind}; use crate::docstrings::docstring_checks::range_for; -use crate::docstrings::types::Definition; +use crate::docstrings::types::{Definition, DefinitionKind}; +use crate::visibility::is_static; static NUMPY_SECTION_NAMES: Lazy> = Lazy::new(|| { BTreeSet::from([ @@ -78,7 +82,21 @@ static NUMPY_SECTION_NAMES_LOWERCASE: Lazy> = Lazy::new(| // ]) // }); -fn get_leading_words(line: &str) -> String { +fn indentation<'a>(checker: &'a mut Checker, docstring: &Expr) -> &'a str { + let range = range_for(docstring); + checker.locator.slice_source_code_range(&Range { + location: Location::new(range.location.row(), 1), + end_location: Location::new(range.location.row(), range.location.column()), + }) +} + +fn leading_space(line: &str) -> String { + line.chars() + .take_while(|char| char.is_whitespace()) + .collect() +} + +fn leading_words(line: &str) -> String { line.trim() .chars() .take_while(|char| char.is_alphanumeric() || char.is_whitespace()) @@ -86,7 +104,7 @@ fn get_leading_words(line: &str) -> String { } fn suspected_as_section(line: &str) -> bool { - NUMPY_SECTION_NAMES_LOWERCASE.contains(&get_leading_words(line).to_lowercase().as_str()) + NUMPY_SECTION_NAMES_LOWERCASE.contains(&leading_words(line).to_lowercase().as_str()) } #[derive(Debug)] @@ -143,7 +161,7 @@ pub fn section_contexts<'a>(lines: &'a [&'a str]) -> Vec> { let mut contexts = vec![]; for lineno in suspected_section_indices { let context = SectionContext { - section_name: get_leading_words(lines[lineno]), + section_name: leading_words(lines[lineno]), previous_line: lines[lineno - 1], line: lines[lineno], following_lines: &lines[lineno + 1..], @@ -196,14 +214,12 @@ fn check_blanks_and_section_underline( // Nothing but blank lines after the section header. if blank_lines_after_header == context.following_lines.len() { - // D407 if checker.settings.enabled.contains(&CheckCode::D407) { checker.add_check(Check::new( CheckKind::DashedUnderlineAfterSection(context.section_name.to_string()), range_for(docstring), )); } - // D414 if checker.settings.enabled.contains(&CheckCode::D414) { checker.add_check(Check::new( CheckKind::NonEmptySection(context.section_name.to_string()), @@ -219,7 +235,6 @@ fn check_blanks_and_section_underline( .all(|char| char.is_whitespace() || char == '-'); if !dash_line_found { - // D407 if checker.settings.enabled.contains(&CheckCode::D407) { checker.add_check(Check::new( CheckKind::DashedUnderlineAfterSection(context.section_name.to_string()), @@ -227,7 +242,6 @@ fn check_blanks_and_section_underline( )); } if blank_lines_after_header > 0 { - // D212 if checker.settings.enabled.contains(&CheckCode::D212) { checker.add_check(Check::new( CheckKind::NoBlankLinesBetweenHeaderAndContent( @@ -239,7 +253,6 @@ fn check_blanks_and_section_underline( } } else { if blank_lines_after_header > 0 { - // D408 if checker.settings.enabled.contains(&CheckCode::D408) { checker.add_check(Check::new( CheckKind::SectionUnderlineAfterName(context.section_name.to_string()), @@ -255,7 +268,6 @@ fn check_blanks_and_section_underline( .count() != context.section_name.len() { - // D409 if checker.settings.enabled.contains(&CheckCode::D409) { checker.add_check(Check::new( CheckKind::SectionUnderlineMatchesSectionLength( @@ -266,16 +278,22 @@ fn check_blanks_and_section_underline( } } - // TODO(charlie): Implement D215, which requires indentation and leading space tracking. + if checker.settings.enabled.contains(&CheckCode::D215) { + if leading_space(non_empty_line).len() > indentation(checker, docstring).len() { + checker.add_check(Check::new( + CheckKind::SectionUnderlineNotOverIndented(context.section_name.to_string()), + range_for(docstring), + )); + } + } + let line_after_dashes_index = blank_lines_after_header + 1; if line_after_dashes_index < context.following_lines.len() { let line_after_dashes = context.following_lines[line_after_dashes_index]; - if line_after_dashes.trim().is_empty() { let rest_of_lines = &context.following_lines[line_after_dashes_index..]; if rest_of_lines.iter().all(|line| line.trim().is_empty()) { - // D414 if checker.settings.enabled.contains(&CheckCode::D414) { checker.add_check(Check::new( CheckKind::NonEmptySection(context.section_name.to_string()), @@ -283,7 +301,6 @@ fn check_blanks_and_section_underline( )); } } else { - // 412 if checker.settings.enabled.contains(&CheckCode::D412) { checker.add_check(Check::new( CheckKind::NoBlankLinesBetweenHeaderAndContent( @@ -295,7 +312,6 @@ fn check_blanks_and_section_underline( } } } else { - // D414 if checker.settings.enabled.contains(&CheckCode::D414) { checker.add_check(Check::new( CheckKind::NonEmptySection(context.section_name.to_string()), @@ -307,7 +323,6 @@ fn check_blanks_and_section_underline( } fn check_common_section(checker: &mut Checker, definition: &Definition, context: &SectionContext) { - // TODO(charlie): Implement D214, which requires indentation and leading space tracking. let docstring = definition .docstring .expect("Sections are only available for docstrings."); @@ -323,6 +338,15 @@ fn check_common_section(checker: &mut Checker, definition: &Definition, context: } } + if checker.settings.enabled.contains(&CheckCode::D214) { + if leading_space(context.line).len() > indentation(checker, docstring).len() { + checker.add_check(Check::new( + CheckKind::SectionNotOverIndented(context.section_name.to_string()), + range_for(docstring), + )) + } + } + if context .following_lines .last() @@ -356,12 +380,110 @@ fn check_common_section(checker: &mut Checker, definition: &Definition, context: } } +fn check_missing_args( + checker: &mut Checker, + definition: &Definition, + docstrings_args: BTreeSet<&str>, +) { + if let DefinitionKind::Function(parent) + | DefinitionKind::NestedFunction(parent) + | DefinitionKind::Method(parent) = definition.kind + { + if let StmtKind::FunctionDef { + args: arguments, .. + } + | StmtKind::AsyncFunctionDef { + args: arguments, .. + } = &parent.node + { + // Collect all the arguments into a single vector. + let mut all_arguments: Vec<&Arg> = arguments + .args + .iter() + .chain(arguments.posonlyargs.iter()) + .chain(arguments.kwonlyargs.iter()) + .skip( + // If this is a non-static method, skip `cls` or `self`. + if matches!(definition.kind, DefinitionKind::Method(_)) && !is_static(parent) { + 1 + } else { + 0 + }, + ) + .collect(); + if let Some(arg) = &arguments.vararg { + all_arguments.push(arg); + } + if let Some(arg) = &arguments.kwarg { + all_arguments.push(arg); + } + + // Look for arguments that weren't included in the docstring. + let mut missing_args: BTreeSet<&str> = Default::default(); + for arg in all_arguments { + let arg_name = arg.node.arg.as_str(); + if arg_name.starts_with('_') { + continue; + } + if docstrings_args.contains(&arg_name) { + continue; + } + missing_args.insert(arg_name); + } + + if !missing_args.is_empty() { + let names = missing_args + .into_iter() + .map(String::from) + .sorted() + .collect(); + checker.add_check(Check::new( + CheckKind::DocumentAllArguments(names), + Range::from_located(parent), + )); + } + } + } +} + +fn check_parameters_section( + checker: &mut Checker, + definition: &Definition, + context: &SectionContext, +) { + // Collect the list of arguments documented in the docstring. + let mut docstring_args: BTreeSet<&str> = Default::default(); + let section_level_indent = leading_space(context.line); + for i in 1..context.following_lines.len() { + let current_line = context.following_lines[i - 1]; + let current_leading_space = leading_space(current_line); + let next_line = context.following_lines[i]; + if current_leading_space == section_level_indent + && (leading_space(next_line).len() > current_leading_space.len()) + && !next_line.trim().is_empty() + { + let parameters = if let Some(semi_index) = current_line.find(':') { + // If the parameter has a type annotation, exclude it. + ¤t_line[..semi_index] + } else { + // Otherwise, it's just a list of parameters on the current line. + current_line.trim() + }; + // Notably, NumPy lets you put multiple parameters of the same type on the same line. + for parameter in parameters.split(',') { + docstring_args.insert(parameter.trim()); + } + } + } + // Validate that all arguments were documented. + check_missing_args(checker, definition, docstring_args); +} + pub fn check_numpy_section( checker: &mut Checker, definition: &Definition, context: &SectionContext, ) { - // TODO(charlie): Implement `_check_parameters_section`. check_common_section(checker, definition, context); check_blanks_and_section_underline(checker, definition, context); @@ -381,4 +503,10 @@ pub fn check_numpy_section( )) } } + + if checker.settings.enabled.contains(&CheckCode::D417) { + if titlecase(&context.section_name) == "Parameters" { + check_parameters_section(checker, definition, context); + } + } } diff --git a/src/linter.rs b/src/linter.rs index ae7b2e2c70..d94bb2c207 100644 --- a/src/linter.rs +++ b/src/linter.rs @@ -277,6 +277,8 @@ mod tests { #[test_case(CheckCode::D211, Path::new("D.py"); "D211")] #[test_case(CheckCode::D212, Path::new("D.py"); "D212")] #[test_case(CheckCode::D213, Path::new("D.py"); "D213")] + #[test_case(CheckCode::D214, Path::new("sections.py"); "D214")] + #[test_case(CheckCode::D215, Path::new("sections.py"); "D215")] #[test_case(CheckCode::D300, Path::new("D.py"); "D300")] #[test_case(CheckCode::D400, Path::new("D.py"); "D400")] #[test_case(CheckCode::D402, Path::new("D.py"); "D402")] @@ -293,6 +295,8 @@ 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::D417, Path::new("sections.py"); "D417_0")] + #[test_case(CheckCode::D417, Path::new("canonical_numpy_examples.py"); "D417_1")] #[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__D214_sections.py.snap b/src/snapshots/ruff__linter__tests__D214_sections.py.snap new file mode 100644 index 0000000000..91faef696d --- /dev/null +++ b/src/snapshots/ruff__linter__tests__D214_sections.py.snap @@ -0,0 +1,14 @@ +--- +source: src/linter.rs +expression: checks +--- +- kind: + SectionNotOverIndented: Returns + location: + row: 135 + column: 5 + end_location: + row: 141 + column: 8 + fix: ~ + diff --git a/src/snapshots/ruff__linter__tests__D215_sections.py.snap b/src/snapshots/ruff__linter__tests__D215_sections.py.snap new file mode 100644 index 0000000000..d54e347e6b --- /dev/null +++ b/src/snapshots/ruff__linter__tests__D215_sections.py.snap @@ -0,0 +1,23 @@ +--- +source: src/linter.rs +expression: checks +--- +- kind: + SectionUnderlineNotOverIndented: Returns + location: + row: 147 + column: 5 + end_location: + row: 153 + column: 8 + fix: ~ +- kind: + SectionUnderlineNotOverIndented: Returns + location: + row: 161 + column: 5 + end_location: + row: 165 + column: 8 + fix: ~ + diff --git a/src/snapshots/ruff__linter__tests__D417_canonical_numpy_examples.py.snap b/src/snapshots/ruff__linter__tests__D417_canonical_numpy_examples.py.snap new file mode 100644 index 0000000000..60c615f917 --- /dev/null +++ b/src/snapshots/ruff__linter__tests__D417_canonical_numpy_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 new file mode 100644 index 0000000000..0635c6e9bc --- /dev/null +++ b/src/snapshots/ruff__linter__tests__D417_sections.py.snap @@ -0,0 +1,50 @@ +--- +source: src/linter.rs +expression: checks +--- +- kind: + DocumentAllArguments: + - y + location: + row: 389 + column: 1 + end_location: + row: 401 + column: 1 + fix: ~ +- kind: + DocumentAllArguments: + - test + - y + - z + location: + row: 425 + column: 5 + end_location: + row: 436 + column: 5 + fix: ~ +- kind: + DocumentAllArguments: + - test + - y + - z + location: + row: 440 + column: 5 + end_location: + row: 455 + column: 5 + fix: ~ +- kind: + DocumentAllArguments: + - a + - z + location: + row: 459 + column: 5 + end_location: + row: 471 + column: 5 + fix: ~ + diff --git a/src/visibility.rs b/src/visibility.rs index 21180660e7..64e3dd8a8a 100644 --- a/src/visibility.rs +++ b/src/visibility.rs @@ -26,6 +26,17 @@ pub struct VisibleScope { pub visibility: Visibility, } +/// Returns `true` if a function is a "static method". +pub fn is_static(stmt: &Stmt) -> bool { + match &stmt.node { + StmtKind::FunctionDef { decorator_list, .. } + | StmtKind::AsyncFunctionDef { decorator_list, .. } => decorator_list + .iter() + .any(|expr| match_name_or_attr(expr, "staticmethod")), + _ => panic!("Found non-FunctionDef in is_overload"), + } +} + /// Returns `true` if a function definition is an `@overload`. pub fn is_overload(stmt: &Stmt) -> bool { match &stmt.node {