diff --git a/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC102_google.py b/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC102_google.py new file mode 100644 index 0000000000..f61500305e --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC102_google.py @@ -0,0 +1,264 @@ +# DOC102 +def add_numbers(b): + """ + Adds two numbers and returns the result. + + Args: + a (int): The first number to add. + b (int): The second number to add. + + Returns: + int: The sum of the two numbers. + """ + return a + b + + +# DOC102 +def multiply_list_elements(lst): + """ + Multiplies each element in a list by a given multiplier. + + Args: + lst (list of int): A list of integers. + multiplier (int): The multiplier for each element in the list. + + Returns: + list of int: A new list with each element multiplied. + """ + return [x * multiplier for x in lst] + + +# DOC102 +def find_max_value(): + """ + Finds the maximum value in a list of numbers. + + Args: + numbers (list of int): A list of integers to search through. + + Returns: + int: The maximum value found in the list. + """ + return max(numbers) + + +# DOC102 +def create_user_profile(location="here"): + """ + Creates a user profile with basic information. + + Args: + name (str): The name of the user. + age (int): The age of the user. + email (str): The user's email address. + location (str): The location of the user. + + Returns: + dict: A dictionary containing the user's profile. + """ + return { + 'name': name, + 'age': age, + 'email': email, + 'location': location + } + + +# DOC102 +def calculate_total_price(item_prices, discount): + """ + Calculates the total price after applying tax and a discount. + + Args: + item_prices (list of float): A list of prices for each item. + tax_rate (float): The tax rate to apply. + discount (float): The discount to subtract from the total. + + Returns: + float: The final total price after tax and discount. + """ + total = sum(item_prices) + total_with_tax = total + (total * tax_rate) + final_total = total_with_tax - discount + return final_total + + +# DOC102 +def send_email(subject, body, bcc_address=None): + """ + Sends an email to the specified recipients. + + Args: + subject (str): The subject of the email. + body (str): The content of the email. + to_address (str): The recipient's email address. + cc_address (str, optional): The email address for CC. Defaults to None. + bcc_address (str, optional): The email address for BCC. Defaults to None. + + Returns: + bool: True if the email was sent successfully, False otherwise. + """ + return True + + +# DOC102 +def concatenate_strings(*args): + """ + Concatenates multiple strings with a specified separator. + + Args: + separator (str): The separator to use between strings. + *args (str): Variable length argument list of strings to concatenate. + + Returns: + str: A single concatenated string. + """ + return separator.join(args) + + +# DOC102 +def process_order(order_id): + """ + Processes an order with a list of items and optional order details. + + Args: + order_id (int): The unique identifier for the order. + *items (str): Variable length argument list of items in the order. + **details (dict): Additional details such as shipping method and address. + + Returns: + dict: A dictionary containing the order summary. + """ + return { + 'order_id': order_id, + 'items': items, + 'details': details + } + + +class Calculator: + """ + A simple calculator class that can perform basic arithmetic operations. + """ + + # DOC102 + def __init__(self): + """ + Initializes the calculator with an initial value. + + Args: + value (int, optional): The initial value of the calculator. Defaults to 0. + """ + self.value = value + + # DOC102 + def add(self, number2): + """ + Adds a number to the current value. + + Args: + number (int or float): The number to add to the current value. + + Returns: + int or float: The updated value after addition. + """ + self.value += number + number2 + return self.value + + # DOC102 + @classmethod + def from_string(cls): + """ + Creates a Calculator instance from a string representation of a number. + + Args: + value_str (str): The string representing the initial value. + + Returns: + Calculator: A new instance of Calculator initialized with the value from the string. + """ + value = float(value_str) + return cls(value) + + # DOC102 + @staticmethod + def is_valid_number(): + """ + Checks if a given number is valid (int or float). + + Args: + number (any): The value to check. + + Returns: + bool: True if the number is valid, False otherwise. + """ + return isinstance(number, (int, float)) + +# OK +def foo(param1, param2, *args, **kwargs): + """Foo. + + Args: + param1 (int): The first parameter. + param2 (:obj:`str`, optional): The second parameter. Defaults to None. + Second line of description: should be indented. + *args: Variable length argument list. + **kwargs: Arbitrary keyword arguments. + """ + return + +# OK +def on_server_unloaded(self, server_context: ServerContext) -> None: + ''' Execute ``on_server_unloaded`` from ``server_lifecycle.py`` (if + it is defined) when the server cleanly exits. (Before stopping the + server's ``IOLoop``.) + + Args: + server_context (ServerContext) : + + .. warning:: + In practice this code may not run, since servers are often killed + by a signal. + + + ''' + return self._lifecycle_handler.on_server_unloaded(server_context) + +# OK +def function_with_kwargs(param1, param2, **kwargs): + """Function with **kwargs parameter. + + Args: + param1 (int): The first parameter. + param2 (str): The second parameter. + extra_param (str): An extra parameter that may be passed via **kwargs. + another_extra (int): Another extra parameter. + """ + return + +# OK +def add_numbers(b): + """ + Adds two numbers and returns the result. + + Args: + b: The second number to add. + + Returns: + int: The sum of the two numbers. + """ + return + +# DOC102 +def add_numbers(b): + """ + Adds two numbers and returns the result. + + Args: + a: The first number to add. + b: The second number to add. + + Returns: + int: The sum of the two numbers. + """ + return a + b diff --git a/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC102_numpy.py b/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC102_numpy.py new file mode 100644 index 0000000000..fb7f86b25f --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC102_numpy.py @@ -0,0 +1,372 @@ +# DOC102 +def add_numbers(b): + """ + Adds two numbers and returns the result. + + Parameters + ---------- + a : int + The first number to add. + b : int + The second number to add. + + Returns + ------- + int + The sum of the two numbers. + """ + return a + b + + +# DOC102 +def multiply_list_elements(lst): + """ + Multiplies each element in a list by a given multiplier. + + Parameters + ---------- + lst : list of int + A list of integers. + multiplier : int + The multiplier for each element in the list. + + Returns + ------- + list of int + A new list with each element multiplied. + """ + return [x * multiplier for x in lst] + + +# DOC102 +def find_max_value(): + """ + Finds the maximum value in a list of numbers. + + Parameters + ---------- + numbers : list of int + A list of integers to search through. + + Returns + ------- + int + The maximum value found in the list. + """ + return max(numbers) + + +# DOC102 +def create_user_profile(location="here"): + """ + Creates a user profile with basic information. + + Parameters + ---------- + name : str + The name of the user. + age : int + The age of the user. + email : str + The user's email address. + location : str, optional + The location of the user, by default "here". + + Returns + ------- + dict + A dictionary containing the user's profile. + """ + return { + 'name': name, + 'age': age, + 'email': email, + 'location': location + } + + +# DOC102 +def calculate_total_price(item_prices, discount): + """ + Calculates the total price after applying tax and a discount. + + Parameters + ---------- + item_prices : list of float + A list of prices for each item. + tax_rate : float + The tax rate to apply. + discount : float + The discount to subtract from the total. + + Returns + ------- + float + The final total price after tax and discount. + """ + total = sum(item_prices) + total_with_tax = total + (total * tax_rate) + final_total = total_with_tax - discount + return final_total + + +# DOC102 +def send_email(subject, body, bcc_address=None): + """ + Sends an email to the specified recipients. + + Parameters + ---------- + subject : str + The subject of the email. + body : str + The content of the email. + to_address : str + The recipient's email address. + cc_address : str, optional + The email address for CC, by default None. + bcc_address : str, optional + The email address for BCC, by default None. + + Returns + ------- + bool + True if the email was sent successfully, False otherwise. + """ + return True + + +# DOC102 +def concatenate_strings(*args): + """ + Concatenates multiple strings with a specified separator. + + Parameters + ---------- + separator : str + The separator to use between strings. + *args : str + Variable length argument list of strings to concatenate. + + Returns + ------- + str + A single concatenated string. + """ + return True + + +# DOC102 +def process_order(order_id): + """ + Processes an order with a list of items and optional order details. + + Parameters + ---------- + order_id : int + The unique identifier for the order. + *items : str + Variable length argument list of items in the order. + **details : dict + Additional details such as shipping method and address. + + Returns + ------- + dict + A dictionary containing the order summary. + """ + return { + 'order_id': order_id, + 'items': items, + 'details': details + } + + +class Calculator: + """ + A simple calculator class that can perform basic arithmetic operations. + """ + + # DOC102 + def __init__(self): + """ + Initializes the calculator with an initial value. + + Parameters + ---------- + value : int, optional + The initial value of the calculator, by default 0. + """ + self.value = value + + # DOC102 + def add(self, number2): + """ + Adds two numbers to the current value. + + Parameters + ---------- + number : int or float + The first number to add. + number2 : int or float + The second number to add. + + Returns + ------- + int or float + The updated value after addition. + """ + self.value += number + number2 + return self.value + + # DOC102 + @classmethod + def from_string(cls): + """ + Creates a Calculator instance from a string representation of a number. + + Parameters + ---------- + value_str : str + The string representing the initial value. + + Returns + ------- + Calculator + A new instance of Calculator initialized with the value from the string. + """ + value = float(value_str) + return cls(value) + + # DOC102 + @staticmethod + def is_valid_number(): + """ + Checks if a given number is valid (int or float). + + Parameters + ---------- + number : any + The value to check. + + Returns + ------- + bool + True if the number is valid, False otherwise. + """ + return isinstance(number, (int, float)) + +# OK +def function_with_kwargs(param1, param2, **kwargs): + """Function with **kwargs parameter. + + Parameters + ---------- + param1 : int + The first parameter. + param2 : str + The second parameter. + extra_param : str + An extra parameter that may be passed via **kwargs. + another_extra : int + Another extra parameter. + """ + return True + +# OK +def add_numbers(b): + """ + Adds two numbers and returns the result. + + Parameters + ---------- + b + The second number to add. + + Returns + ------- + int + The sum of the two numbers. + """ + return a + b + +# DOC102 +def add_numbers(b): + """ + Adds two numbers and returns the result. + + Parameters + ---------- + a + The first number to add. + b + The second number to add. + + Returns + ------- + int + The sum of the two numbers. + """ + return a + b + +class Foo: + # OK + def send_help(self, *args: Any) -> Any: + """|coro| + + Shows the help command for the specified entity if given. + The entity can be a command or a cog. + + If no entity is given, then it'll show help for the + entire bot. + + If the entity is a string, then it looks up whether it's a + :class:`Cog` or a :class:`Command`. + + .. note:: + + Due to the way this function works, instead of returning + something similar to :meth:`~.commands.HelpCommand.command_not_found` + this returns :class:`None` on bad input or no help command. + + Parameters + ---------- + entity: Optional[Union[:class:`Command`, :class:`Cog`, :class:`str`]] + The entity to show help for. + + Returns + ------- + Any + The result of the help command, if any. + """ + return + + # OK + @classmethod + async def convert(cls, ctx: Context, argument: str) -> Self: + """|coro| + + The method that actually converters an argument to the flag mapping. + + Parameters + ---------- + cls: Type[:class:`FlagConverter`] + The flag converter class. + ctx: :class:`Context` + The invocation context. + argument: :class:`str` + The argument to convert from. + + Raises + ------ + FlagError + A flag related parsing error. + CommandError + A command related error. + + Returns + ------- + :class:`FlagConverter` + The flag converter instance with all flags parsed. + """ + return diff --git a/crates/ruff_linter/src/checkers/ast/analyze/definitions.rs b/crates/ruff_linter/src/checkers/ast/analyze/definitions.rs index e3a2567bbf..4a3fe560be 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::DocstringExtraneousParameter, Rule::DocstringMissingReturns, Rule::DocstringExtraneousReturns, Rule::DocstringMissingYields, diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index 6b2a317bae..86e3bf8ebc 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -988,6 +988,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (FastApi, "003") => (RuleGroup::Stable, rules::fastapi::rules::FastApiUnusedPathParameter), // pydoclint + (Pydoclint, "102") => (RuleGroup::Preview, rules::pydoclint::rules::DocstringExtraneousParameter), (Pydoclint, "201") => (RuleGroup::Preview, rules::pydoclint::rules::DocstringMissingReturns), (Pydoclint, "202") => (RuleGroup::Preview, rules::pydoclint::rules::DocstringExtraneousReturns), (Pydoclint, "402") => (RuleGroup::Preview, rules::pydoclint::rules::DocstringMissingYields), diff --git a/crates/ruff_linter/src/rules/pydoclint/mod.rs b/crates/ruff_linter/src/rules/pydoclint/mod.rs index 9fae2230a6..69326b0c51 100644 --- a/crates/ruff_linter/src/rules/pydoclint/mod.rs +++ b/crates/ruff_linter/src/rules/pydoclint/mod.rs @@ -28,6 +28,7 @@ mod tests { Ok(()) } + #[test_case(Rule::DocstringExtraneousParameter, Path::new("DOC102_google.py"))] #[test_case(Rule::DocstringMissingReturns, Path::new("DOC201_google.py"))] #[test_case(Rule::DocstringExtraneousReturns, Path::new("DOC202_google.py"))] #[test_case(Rule::DocstringMissingYields, Path::new("DOC402_google.py"))] @@ -50,6 +51,7 @@ mod tests { Ok(()) } + #[test_case(Rule::DocstringExtraneousParameter, Path::new("DOC102_numpy.py"))] #[test_case(Rule::DocstringMissingReturns, Path::new("DOC201_numpy.py"))] #[test_case(Rule::DocstringExtraneousReturns, Path::new("DOC202_numpy.py"))] #[test_case(Rule::DocstringMissingYields, Path::new("DOC402_numpy.py"))] 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 8db3c53808..dd4f8ee8a7 100644 --- a/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs +++ b/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs @@ -1,14 +1,14 @@ use itertools::Itertools; use ruff_macros::{ViolationMetadata, derive_message_formats}; -use ruff_python_ast::helpers::map_callable; -use ruff_python_ast::helpers::map_subscript; +use ruff_python_ast::helpers::{map_callable, map_subscript}; 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::{function_type, visibility}; use ruff_python_semantic::{Definition, SemanticModel}; -use ruff_source_file::NewlineWithTrailingNewline; -use ruff_text_size::{Ranged, TextRange}; +use ruff_python_stdlib::identifiers::is_identifier; +use ruff_source_file::{LineRanges, NewlineWithTrailingNewline}; +use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; use crate::Violation; use crate::checkers::ast::Checker; @@ -18,6 +18,62 @@ use crate::docstrings::styles::SectionStyle; use crate::registry::Rule; use crate::rules::pydocstyle::settings::Convention; +/// ## What it does +/// Checks for function docstrings that include parameters which are not +/// in the function signature. +/// +/// ## Why is this bad? +/// If a docstring documents a parameter which is not in the function signature, +/// it can be misleading to users and/or a sign of incomplete documentation or +/// refactors. +/// +/// ## Example +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Args: +/// distance: Distance traveled. +/// time: Time spent traveling. +/// acceleration: Rate of change of speed. +/// +/// Returns: +/// Speed as distance divided by time. +/// """ +/// return distance / time +/// ``` +/// +/// Use instead: +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Args: +/// distance: Distance traveled. +/// time: Time spent traveling. +/// +/// Returns: +/// Speed as distance divided by time. +/// """ +/// return distance / time +/// ``` +#[derive(ViolationMetadata)] +pub(crate) struct DocstringExtraneousParameter { + id: String, +} + +impl Violation for DocstringExtraneousParameter { + #[derive_message_formats] + fn message(&self) -> String { + let DocstringExtraneousParameter { id } = self; + format!("Documented parameter `{id}` is not in the function's signature") + } + + fn fix_title(&self) -> Option { + Some("Remove the extraneous parameter from the docstring".to_string()) + } +} + /// ## What it does /// Checks for functions with `return` statements that do not have "Returns" /// sections in their docstrings. @@ -396,6 +452,19 @@ impl GenericSection { } } +/// A parameter in a docstring with its text range. +#[derive(Debug, Clone)] +struct ParameterEntry<'a> { + name: &'a str, + range: TextRange, +} + +impl Ranged for ParameterEntry<'_> { + fn range(&self) -> TextRange { + self.range + } +} + /// A "Raises" section in a docstring. #[derive(Debug)] struct RaisesSection<'a> { @@ -414,17 +483,46 @@ impl<'a> RaisesSection<'a> { /// a "Raises" section. fn from_section(section: &SectionContext<'a>, style: Option) -> Self { Self { - raised_exceptions: parse_entries(section.following_lines_str(), style), + raised_exceptions: parse_raises(section.following_lines_str(), style), range: section.range(), } } } +/// An "Args" or "Parameters" section in a docstring. +#[derive(Debug)] +struct ParametersSection<'a> { + parameters: Vec>, + range: TextRange, +} + +impl Ranged for ParametersSection<'_> { + fn range(&self) -> TextRange { + self.range + } +} + +impl<'a> ParametersSection<'a> { + /// Return the parameters for the docstring, or `None` if the docstring does not contain + /// an "Args" or "Parameters" section. + fn from_section(section: &SectionContext<'a>, style: Option) -> Self { + Self { + parameters: parse_parameters( + section.following_lines_str(), + section.following_range().start(), + style, + ), + range: section.section_name_range(), + } + } +} + #[derive(Debug, Default)] struct DocstringSections<'a> { returns: Option, yields: Option, raises: Option>, + parameters: Option>, } impl<'a> DocstringSections<'a> { @@ -432,6 +530,10 @@ 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::Raises => { docstring_sections.raises = Some(RaisesSection::from_section(§ion, style)); } @@ -448,18 +550,22 @@ impl<'a> DocstringSections<'a> { } } -/// Parse the entries in a "Raises" section of a docstring. +/// Parse the entries in a "Parameters" section of a docstring. /// /// Attempts to parse using the specified [`SectionStyle`], falling back to the other style if no /// entries are found. -fn parse_entries(content: &str, style: Option) -> Vec> { +fn parse_parameters( + content: &str, + content_start: TextSize, + style: Option, +) -> Vec> { match style { - Some(SectionStyle::Google) => parse_entries_google(content), - Some(SectionStyle::Numpy) => parse_entries_numpy(content), + Some(SectionStyle::Google) => parse_parameters_google(content, content_start), + Some(SectionStyle::Numpy) => parse_parameters_numpy(content, content_start), None => { - let entries = parse_entries_google(content); + let entries = parse_parameters_google(content, content_start); if entries.is_empty() { - parse_entries_numpy(content) + parse_parameters_numpy(content, content_start) } else { entries } @@ -467,14 +573,134 @@ fn parse_entries(content: &str, style: Option) -> Vec Vec> { + let mut entries: Vec = Vec::new(); + // Find first entry to determine indentation + let Some(first_arg) = content.lines().next() else { + return entries; + }; + let indentation = &first_arg[..first_arg.len() - first_arg.trim_start().len()]; + + let mut current_pos = TextSize::ZERO; + for line in content.lines() { + let line_start = current_pos; + current_pos = content.full_line_end(line_start); + + if let Some(entry) = line.strip_prefix(indentation) { + if entry + .chars() + .next() + .is_some_and(|first_char| !first_char.is_whitespace()) + { + let Some((before_colon, _)) = entry.split_once(':') else { + continue; + }; + if let Some(param) = before_colon.split_whitespace().next() { + let param_name = param.trim_start_matches('*'); + if is_identifier(param_name) { + let param_start = line_start + indentation.text_len(); + let param_end = param_start + param.text_len(); + + entries.push(ParameterEntry { + name: param_name, + range: TextRange::new( + content_start + param_start, + content_start + param_end, + ), + }); + } + } + } + } + } + entries +} + +/// Parses NumPy-style "Parameters" sections of the form: +/// +/// ```python +/// Parameters +/// ---------- +/// a : int +/// The first number to add. +/// b : int +/// The second number to add. +/// ``` +fn parse_parameters_numpy(content: &str, content_start: TextSize) -> Vec> { + let mut entries: Vec = Vec::new(); + let mut lines = content.lines(); + let Some(dashes) = lines.next() else { + return entries; + }; + let indentation = &dashes[..dashes.len() - dashes.trim_start().len()]; + + let mut current_pos = content.full_line_end(dashes.text_len()); + for potential in lines { + let line_start = current_pos; + current_pos = content.full_line_end(line_start); + + if let Some(entry) = potential.strip_prefix(indentation) { + if entry + .chars() + .next() + .is_some_and(|first_char| !first_char.is_whitespace()) + { + if let Some(before_colon) = entry.split(':').next() { + let param = before_colon.trim_end(); + let param_name = param.trim_start_matches('*'); + if is_identifier(param_name) { + let param_start = line_start + indentation.text_len(); + let param_end = param_start + param.text_len(); + + entries.push(ParameterEntry { + name: param_name, + range: TextRange::new( + content_start + param_start, + content_start + param_end, + ), + }); + } + } + } + } + } + entries +} + +/// Parse the entries in a "Raises" section of a docstring. +/// +/// Attempts to parse using the specified [`SectionStyle`], falling back to the other style if no +/// entries are found. +fn parse_raises(content: &str, style: Option) -> Vec> { + match style { + Some(SectionStyle::Google) => parse_raises_google(content), + Some(SectionStyle::Numpy) => parse_raises_numpy(content), + None => { + let entries = parse_raises_google(content); + if entries.is_empty() { + parse_raises_numpy(content) + } else { + entries + } + } + } +} + +/// Parses Google-style "Raises" section of the form: /// /// ```python /// Raises: /// FasterThanLightError: If speed is greater than the speed of light. /// DivisionByZero: If attempting to divide by zero. /// ``` -fn parse_entries_google(content: &str) -> Vec> { +fn parse_raises_google(content: &str) -> Vec> { let mut entries: Vec = Vec::new(); for potential in content.lines() { let Some(colon_idx) = potential.find(':') else { @@ -486,7 +712,7 @@ fn parse_entries_google(content: &str) -> Vec> { entries } -/// Parses NumPy-style docstring sections of the form: +/// Parses NumPy-style "Raises" section of the form: /// /// ```python /// Raises @@ -496,7 +722,7 @@ fn parse_entries_google(content: &str) -> Vec> { /// DivisionByZero /// If attempting to divide by zero. /// ``` -fn parse_entries_numpy(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 { @@ -867,6 +1093,17 @@ 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> { + 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 +} + fn is_one_line(docstring: &Docstring) -> bool { let mut non_empty_line_count = 0; for line in NewlineWithTrailingNewline::from(docstring.body().as_str()) { @@ -880,7 +1117,7 @@ fn is_one_line(docstring: &Docstring) -> bool { true } -/// DOC201, DOC202, DOC402, DOC403, DOC501, DOC502 +/// DOC102, DOC201, DOC202, DOC402, DOC403, DOC501, DOC502 pub(crate) fn check_docstring( checker: &Checker, definition: &Definition, @@ -920,6 +1157,8 @@ pub(crate) fn check_docstring( visitor.finish() }; + let signature_parameters = parameters_from_signature(docstring); + // DOC201 if checker.is_rule_enabled(Rule::DocstringMissingReturns) { if should_document_returns(function_def) @@ -1008,6 +1247,25 @@ pub(crate) fn check_docstring( } } + // DOC102 + if checker.is_rule_enabled(Rule::DocstringExtraneousParameter) { + // Don't report extraneous parameters if the signature defines *args or **kwargs + 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) { + checker.report_diagnostic( + DocstringExtraneousParameter { + id: docstring_param.name.to_string(), + }, + docstring_param.range(), + ); + } + } + } + } + } + // Avoid applying "extraneous" rules to abstract methods. An abstract method's docstring _could_ // document that it raises an exception without including the exception in the implementation. if !visibility::is_abstract(&function_def.decorator_list, semantic) { diff --git a/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-extraneous-parameter_DOC102_google.py.snap b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-extraneous-parameter_DOC102_google.py.snap new file mode 100644 index 0000000000..1a0dd86341 --- /dev/null +++ b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-extraneous-parameter_DOC102_google.py.snap @@ -0,0 +1,180 @@ +--- +source: crates/ruff_linter/src/rules/pydoclint/mod.rs +--- +DOC102 Documented parameter `a` is not in the function's signature + --> DOC102_google.py:7:9 + | +6 | Args: +7 | a (int): The first number to add. + | ^ +8 | b (int): The second number to add. + | +help: Remove the extraneous parameter from the docstring + +DOC102 Documented parameter `multiplier` is not in the function's signature + --> DOC102_google.py:23:9 + | +21 | Args: +22 | lst (list of int): A list of integers. +23 | multiplier (int): The multiplier for each element in the list. + | ^^^^^^^^^^ +24 | +25 | Returns: + | +help: Remove the extraneous parameter from the docstring + +DOC102 Documented parameter `numbers` is not in the function's signature + --> DOC102_google.py:37:9 + | +36 | Args: +37 | numbers (list of int): A list of integers to search through. + | ^^^^^^^ +38 | +39 | Returns: + | +help: Remove the extraneous parameter from the docstring + +DOC102 Documented parameter `name` is not in the function's signature + --> DOC102_google.py:51:9 + | +50 | Args: +51 | name (str): The name of the user. + | ^^^^ +52 | age (int): The age of the user. +53 | email (str): The user's email address. + | +help: Remove the extraneous parameter from the docstring + +DOC102 Documented parameter `age` is not in the function's signature + --> DOC102_google.py:52:9 + | +50 | Args: +51 | name (str): The name of the user. +52 | age (int): The age of the user. + | ^^^ +53 | email (str): The user's email address. +54 | location (str): The location of the user. + | +help: Remove the extraneous parameter from the docstring + +DOC102 Documented parameter `email` is not in the function's signature + --> DOC102_google.py:53:9 + | +51 | name (str): The name of the user. +52 | age (int): The age of the user. +53 | email (str): The user's email address. + | ^^^^^ +54 | location (str): The location of the user. + | +help: Remove the extraneous parameter from the docstring + +DOC102 Documented parameter `tax_rate` is not in the function's signature + --> DOC102_google.py:74:9 + | +72 | Args: +73 | item_prices (list of float): A list of prices for each item. +74 | tax_rate (float): The tax rate to apply. + | ^^^^^^^^ +75 | discount (float): The discount to subtract from the total. + | +help: Remove the extraneous parameter from the docstring + +DOC102 Documented parameter `to_address` is not in the function's signature + --> DOC102_google.py:94:9 + | +92 | subject (str): The subject of the email. +93 | body (str): The content of the email. +94 | to_address (str): The recipient's email address. + | ^^^^^^^^^^ +95 | cc_address (str, optional): The email address for CC. Defaults to None. +96 | bcc_address (str, optional): The email address for BCC. Defaults to None. + | +help: Remove the extraneous parameter from the docstring + +DOC102 Documented parameter `cc_address` is not in the function's signature + --> DOC102_google.py:95:9 + | +93 | body (str): The content of the email. +94 | to_address (str): The recipient's email address. +95 | cc_address (str, optional): The email address for CC. Defaults to None. + | ^^^^^^^^^^ +96 | bcc_address (str, optional): The email address for BCC. Defaults to None. + | +help: Remove the extraneous parameter from the docstring + +DOC102 Documented parameter `items` is not in the function's signature + --> DOC102_google.py:126:9 + | +124 | Args: +125 | order_id (int): The unique identifier for the order. +126 | *items (str): Variable length argument list of items in the order. + | ^^^^^^ +127 | **details (dict): Additional details such as shipping method and address. + | +help: Remove the extraneous parameter from the docstring + +DOC102 Documented parameter `details` is not in the function's signature + --> DOC102_google.py:127:9 + | +125 | order_id (int): The unique identifier for the order. +126 | *items (str): Variable length argument list of items in the order. +127 | **details (dict): Additional details such as shipping method and address. + | ^^^^^^^^^ +128 | +129 | Returns: + | +help: Remove the extraneous parameter from the docstring + +DOC102 Documented parameter `value` is not in the function's signature + --> DOC102_google.py:150:13 + | +149 | Args: +150 | value (int, optional): The initial value of the calculator. Defaults to 0. + | ^^^^^ +151 | """ +152 | self.value = value + | +help: Remove the extraneous parameter from the docstring + +DOC102 Documented parameter `number` is not in the function's signature + --> DOC102_google.py:160:13 + | +159 | Args: +160 | number (int or float): The number to add to the current value. + | ^^^^^^ +161 | +162 | Returns: + | +help: Remove the extraneous parameter from the docstring + +DOC102 Documented parameter `value_str` is not in the function's signature + --> DOC102_google.py:175:13 + | +174 | Args: +175 | value_str (str): The string representing the initial value. + | ^^^^^^^^^ +176 | +177 | Returns: + | +help: Remove the extraneous parameter from the docstring + +DOC102 Documented parameter `number` is not in the function's signature + --> DOC102_google.py:190:13 + | +189 | Args: +190 | number (any): The value to check. + | ^^^^^^ +191 | +192 | Returns: + | +help: Remove the extraneous parameter from the docstring + +DOC102 Documented parameter `a` is not in the function's signature + --> DOC102_google.py:258:9 + | +257 | Args: +258 | a: The first number to add. + | ^ +259 | b: The second number to add. + | +help: Remove the extraneous parameter from the docstring diff --git a/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-extraneous-parameter_DOC102_numpy.py.snap b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-extraneous-parameter_DOC102_numpy.py.snap new file mode 100644 index 0000000000..c4566953d0 --- /dev/null +++ b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-extraneous-parameter_DOC102_numpy.py.snap @@ -0,0 +1,189 @@ +--- +source: crates/ruff_linter/src/rules/pydoclint/mod.rs +--- +DOC102 Documented parameter `a` is not in the function's signature + --> DOC102_numpy.py:8:5 + | + 6 | Parameters + 7 | ---------- + 8 | a : int + | ^ + 9 | The first number to add. +10 | b : int + | +help: Remove the extraneous parameter from the docstring + +DOC102 Documented parameter `multiplier` is not in the function's signature + --> DOC102_numpy.py:30:5 + | +28 | lst : list of int +29 | A list of integers. +30 | multiplier : int + | ^^^^^^^^^^ +31 | The multiplier for each element in the list. + | +help: Remove the extraneous parameter from the docstring + +DOC102 Documented parameter `numbers` is not in the function's signature + --> DOC102_numpy.py:48:5 + | +46 | Parameters +47 | ---------- +48 | numbers : list of int + | ^^^^^^^ +49 | A list of integers to search through. + | +help: Remove the extraneous parameter from the docstring + +DOC102 Documented parameter `name` is not in the function's signature + --> DOC102_numpy.py:66:5 + | +64 | Parameters +65 | ---------- +66 | name : str + | ^^^^ +67 | The name of the user. +68 | age : int + | +help: Remove the extraneous parameter from the docstring + +DOC102 Documented parameter `age` is not in the function's signature + --> DOC102_numpy.py:68:5 + | +66 | name : str +67 | The name of the user. +68 | age : int + | ^^^ +69 | The age of the user. +70 | email : str + | +help: Remove the extraneous parameter from the docstring + +DOC102 Documented parameter `email` is not in the function's signature + --> DOC102_numpy.py:70:5 + | +68 | age : int +69 | The age of the user. +70 | email : str + | ^^^^^ +71 | The user's email address. +72 | location : str, optional + | +help: Remove the extraneous parameter from the docstring + +DOC102 Documented parameter `tax_rate` is not in the function's signature + --> DOC102_numpy.py:97:5 + | +95 | item_prices : list of float +96 | A list of prices for each item. +97 | tax_rate : float + | ^^^^^^^^ +98 | The tax rate to apply. +99 | discount : float + | +help: Remove the extraneous parameter from the docstring + +DOC102 Documented parameter `to_address` is not in the function's signature + --> DOC102_numpy.py:124:5 + | +122 | body : str +123 | The content of the email. +124 | to_address : str + | ^^^^^^^^^^ +125 | The recipient's email address. +126 | cc_address : str, optional + | +help: Remove the extraneous parameter from the docstring + +DOC102 Documented parameter `cc_address` is not in the function's signature + --> DOC102_numpy.py:126:5 + | +124 | to_address : str +125 | The recipient's email address. +126 | cc_address : str, optional + | ^^^^^^^^^^ +127 | The email address for CC, by default None. +128 | bcc_address : str, optional + | +help: Remove the extraneous parameter from the docstring + +DOC102 Documented parameter `items` is not in the function's signature + --> DOC102_numpy.py:168:5 + | +166 | order_id : int +167 | The unique identifier for the order. +168 | *items : str + | ^^^^^^ +169 | Variable length argument list of items in the order. +170 | **details : dict + | +help: Remove the extraneous parameter from the docstring + +DOC102 Documented parameter `details` is not in the function's signature + --> DOC102_numpy.py:170:5 + | +168 | *items : str +169 | Variable length argument list of items in the order. +170 | **details : dict + | ^^^^^^^^^ +171 | Additional details such as shipping method and address. + | +help: Remove the extraneous parameter from the docstring + +DOC102 Documented parameter `value` is not in the function's signature + --> DOC102_numpy.py:197:9 + | +195 | Parameters +196 | ---------- +197 | value : int, optional + | ^^^^^ +198 | The initial value of the calculator, by default 0. +199 | """ + | +help: Remove the extraneous parameter from the docstring + +DOC102 Documented parameter `number` is not in the function's signature + --> DOC102_numpy.py:209:9 + | +207 | Parameters +208 | ---------- +209 | number : int or float + | ^^^^^^ +210 | The first number to add. +211 | number2 : int or float + | +help: Remove the extraneous parameter from the docstring + +DOC102 Documented parameter `value_str` is not in the function's signature + --> DOC102_numpy.py:230:9 + | +228 | Parameters +229 | ---------- +230 | value_str : str + | ^^^^^^^^^ +231 | The string representing the initial value. + | +help: Remove the extraneous parameter from the docstring + +DOC102 Documented parameter `number` is not in the function's signature + --> DOC102_numpy.py:249:9 + | +247 | Parameters +248 | ---------- +249 | number : any + | ^^^^^^ +250 | The value to check. + | +help: Remove the extraneous parameter from the docstring + +DOC102 Documented parameter `a` is not in the function's signature + --> DOC102_numpy.py:300:5 + | +298 | Parameters +299 | ---------- +300 | a + | ^ +301 | The first number to add. +302 | b + | +help: Remove the extraneous parameter from the docstring diff --git a/ruff.schema.json b/ruff.schema.json index 33ccab9364..b44f308d65 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -3186,6 +3186,9 @@ "DJ012", "DJ013", "DOC", + "DOC1", + "DOC10", + "DOC102", "DOC2", "DOC20", "DOC201",