[`pydoclint`] Implement `docstring-extraneous-parameter` (`DOC102`) (#20376)

## Summary

Implement `docstring-extraneous-parameter` (`DOC102`). This rule checks
that all parameters present in a functions docstring are also present in
its signature.

Split from #13280, per this
[comment](https://github.com/astral-sh/ruff/pull/13280#issuecomment-3280575506).

Part of #12434.

## Test Plan

Test cases added.

---------

Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
This commit is contained in:
Auguste Lalande 2025-10-16 11:26:51 -04:00 committed by GitHub
parent 058fc37542
commit 03696687ea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 1286 additions and 16 deletions

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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),

View File

@ -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"))]

View File

@ -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<String> {
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<SectionStyle>) -> 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<ParameterEntry<'a>>,
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<SectionStyle>) -> 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<GenericSection>,
yields: Option<GenericSection>,
raises: Option<RaisesSection<'a>>,
parameters: Option<ParametersSection<'a>>,
}
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(&section, style));
}
SectionKind::Raises => {
docstring_sections.raises = Some(RaisesSection::from_section(&section, 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<SectionStyle>) -> Vec<QualifiedName<'_>> {
fn parse_parameters(
content: &str,
content_start: TextSize,
style: Option<SectionStyle>,
) -> Vec<ParameterEntry<'_>> {
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<SectionStyle>) -> Vec<QualifiedNam
}
}
/// Parses Google-style docstring sections of the form:
/// Parses Google-style "Args" sections of the form:
///
/// ```python
/// Args:
/// a (int): The first number to add.
/// b (int): The second number to add.
/// ```
fn parse_parameters_google(content: &str, content_start: TextSize) -> Vec<ParameterEntry<'_>> {
let mut entries: Vec<ParameterEntry> = 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<ParameterEntry<'_>> {
let mut entries: Vec<ParameterEntry> = 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<SectionStyle>) -> Vec<QualifiedName<'_>> {
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<QualifiedName<'_>> {
fn parse_raises_google(content: &str) -> Vec<QualifiedName<'_>> {
let mut entries: Vec<QualifiedName> = 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<QualifiedName<'_>> {
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<QualifiedName<'_>> {
/// DivisionByZero
/// If attempting to divide by zero.
/// ```
fn parse_entries_numpy(content: &str) -> Vec<QualifiedName<'_>> {
fn parse_raises_numpy(content: &str) -> Vec<QualifiedName<'_>> {
let mut entries: Vec<QualifiedName> = 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) {

View File

@ -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

View File

@ -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

3
ruff.schema.json generated
View File

@ -3186,6 +3186,9 @@
"DJ012",
"DJ013",
"DOC",
"DOC1",
"DOC10",
"DOC102",
"DOC2",
"DOC20",
"DOC201",