Repository: astral-sh/ruff
File: crates/ruff_linter/src/rules/pydocstyle/rules/sections.rs (Rust)

Code before:
            }
        }
    }

    if !missing_arg_names.is_empty() {
        if let Some(definition) = docstring.definition.name() {
            let names = missing_arg_names.into_iter().sorted().collect();
            checker.report_diagnostic(
                UndocumentedParam {
                    definition: definition.to_string(),
                    names,
                },
                function.identifier(),
            );
        }
    }
}

fn should_ignore_kwarg_check(checker: &Checker, parameter: &Parameter) -> bool {
    if let Some(annotation) = &parameter.annotation {
        checker
            .semantic()
            .match_typing_expr(map_subscript(annotation), "Unpack")
    } else {
        false
    }
}

// See: `GOOGLE_ARGS_REGEX` in `pydocstyle/checker.py`.
static GOOGLE_ARGS_REGEX: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r"^\s*(\*?\*?\w+)\s*(\(.*?\))?\s*:(\r\n|\n)?\s*.+").unwrap());

fn args_section(context: &SectionContext) -> FxHashSet<String> {
    let mut following_lines = context.following_lines();
    let Some(first_line) = following_lines.next() else {
        return FxHashSet::default();
    };

    // Normalize leading whitespace, by removing any lines with less indentation
    // than the first.
    let leading_space = leading_space(first_line.as_str());
    let relevant_lines = std::iter::once(first_line)
        .chain(following_lines)
        .filter(|line| {
            line.is_empty()
                || (line.starts_with(leading_space) && find_underline(line, '-').is_none())
        })
        .map(|line| line.as_str())
        .join("\n");
    let args_content = dedent(&relevant_lines);


Review comment (suggestion):
Small nit here:

```suggestion
    parameter.annotation.is_some_and(|annotation|
        checker
            .semantic()
            .match_typing_expr(map_subscript(annotation), "Unpack")
    ) 
```

We sometimes make functions like this only take the `&SemanticModel` from `checker.semantic()` instead of a whole `&Checker` too, but I don't feel too strongly about that.

Code after:
                && !docstrings_args.contains(arg_name)
                && !docstrings_args.contains(&starred_arg_name)
            {
                missing_arg_names.insert(starred_arg_name);
            }
        }
    }

    if !missing_arg_names.is_empty() {
        if let Some(definition) = docstring.definition.name() {
            let names = missing_arg_names.into_iter().sorted().collect();
            checker.report_diagnostic(
                UndocumentedParam {
                    definition: definition.to_string(),
                    names,
                },
                function.identifier(),
            );
        }
    }
}

/// Returns `true` if the parameter is annotated with `typing.Unpack`
fn has_unpack_annotation(checker: &Checker, parameter: &Parameter) -> bool {
    parameter.annotation.as_ref().is_some_and(|annotation| {
        checker
            .semantic()
            .match_typing_expr(map_subscript(annotation), "Unpack")
    })
}

// See: `GOOGLE_ARGS_REGEX` in `pydocstyle/checker.py`.
static GOOGLE_ARGS_REGEX: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r"^\s*(\*?\*?\w+)\s*(\(.*?\))?\s*:(\r\n|\n)?\s*.+").unwrap());

fn args_section(context: &SectionContext) -> FxHashSet<String> {
    let mut following_lines = context.following_lines();
    let Some(first_line) = following_lines.next() else {
        return FxHashSet::default();
    };

    // Normalize leading whitespace, by removing any lines with less indentation
    // than the first.
    let leading_space = leading_space(first_line.as_str());
    let relevant_lines = std::iter::once(first_line)
        .chain(following_lines)
        .filter(|line| {
            line.is_empty()
                || (line.starts_with(leading_space) && find_underline(line, '-').is_none())
        })
        .map(|line| line.as_str())