Skip to content

Commit 8ebb34e

Browse files
authored
[pydocstyle] Treat backslash-escaped *args and **kwargs as documented (D417) (#29536)
## Summary Closes #29535. Sphinx-style docstrings often escape the stars as `\*args` and `\*\*kwargs`, because a bare `*` starts emphasis in reST. D417 didn't account for that, so it reported those parameters as undocumented even though they were documented. It went wrong in a different way for each style. In NumPy-style sections, the name before the `:` was taken verbatim, so `\*\*kwargs` never matched `**kwargs`. In Google-style sections, the regex didn't accept a leading `\`, so the whole line was skipped. I added a small `unescape_parameter_name` helper in `sections.rs` that strips backslashes from the documented name, and both parsers now use it. The Google regex also accepts up to two escaped stars now. Since a backslash can't be part of a Python parameter name, removing all of them should be safe. This also covers non-raw docstrings that write `\\*\\*kwargs`. ## Test Plan I added five cases to `D417.py`: escaped NumPy and Google docstrings with every parameter documented, the same two styles with one parameter actually missing, and a non-raw docstring. Without the fix, all five `d417` tests fail and report `*args` and `**kwargs` as missing. With the fix, the fully documented functions aren't flagged. In the missing variants, only the parameters that really are missing get reported. The snapshots are updated, and `cargo test -p ruff_linter -- pydocstyle` and clippy both pass. I used Claude (via T3 Code) to help with the implementation. I've reviewed the change and can explain it.
1 parent e02754d commit 8ebb34e

8 files changed

Lines changed: 125 additions & 6 deletions

‎crates/ruff_linter/resources/test/fixtures/pydocstyle/D417.py‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,3 +241,57 @@ def function_with_unpack_and_missing_arg_doc_should_fail(query: str, **kwargs: U
241241
Args:
242242
**kwargs: keyword arguments
243243
"""
244+
245+
246+
# regression test for https://github.com/astral-sh/ruff/issues/29535.
247+
# reStructuredText requires `*` to be escaped with a backslash.
248+
def escaped_stars_numpy(x, *args, **kwargs):
249+
r"""Do something.
250+
251+
Parameters
252+
----------
253+
x : int
254+
The value.
255+
\*args : tuple
256+
Variable arguments.
257+
\*\*kwargs : dict
258+
Keyword arguments.
259+
"""
260+
261+
262+
def escaped_stars_numpy_missing(x, *args, **kwargs):
263+
r"""Do something.
264+
265+
Parameters
266+
----------
267+
\*\*kwargs : dict
268+
Keyword arguments.
269+
"""
270+
271+
272+
def escaped_stars_google(x, *args, **kwargs):
273+
r"""Do something.
274+
275+
Args:
276+
x: the value
277+
\*args: variable arguments
278+
\*\*kwargs: keyword arguments
279+
"""
280+
281+
282+
def escaped_stars_google_missing(x, *args, **kwargs):
283+
r"""Do something.
284+
285+
Args:
286+
\*\*kwargs: keyword arguments
287+
"""
288+
289+
290+
def escaped_stars_non_raw(x, *args, **kwargs):
291+
"""Do something.
292+
293+
Args:
294+
x: the value
295+
\\*args: variable arguments
296+
\\*\\*kwargs: keyword arguments
297+
"""

‎crates/ruff_linter/src/rules/pydocstyle/mod.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ mod tests {
198198
let diagnostics = test_path(
199199
Path::new("pydocstyle/D417.py"),
200200
&settings::LinterSettings {
201-
// With explicit numpy convention, we shouldn't flag anything.
201+
// With explicit numpy convention, we should only flag the NumPy-style docstrings.
202202
pydocstyle: Settings {
203203
convention: Some(Convention::Numpy),
204204
..Settings::default()

‎crates/ruff_linter/src/rules/pydocstyle/rules/sections.rs‎

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1967,8 +1967,15 @@ fn has_unpack_annotation(checker: &Checker, parameter: &Parameter) -> bool {
19671967
}
19681968

19691969
// See: `GOOGLE_ARGS_REGEX` in `pydocstyle/checker.py`.
1970-
static GOOGLE_ARGS_REGEX: LazyLock<Regex> =
1971-
LazyLock::new(|| Regex::new(r"^\s*(\*?\*?\w+)\s*(\(.*?\))?\s*:(\r\n|\n)?\s*.+").unwrap());
1970+
static GOOGLE_ARGS_REGEX: LazyLock<Regex> = LazyLock::new(|| {
1971+
Regex::new(r"^\s*((?:\\*\*){0,2}\w+)\s*(\(.*?\))?\s*:(\r\n|\n)?\s*.+").unwrap()
1972+
});
1973+
1974+
/// Removes the backslashes that escape `*` in reStructuredText, so that `\*\*kwargs` (or
1975+
/// `\\*\\*kwargs` in a non-raw docstring) is treated as `**kwargs`.
1976+
fn unescape_parameter_name(name: &str) -> String {
1977+
name.replace('\\', "")
1978+
}
19721979

19731980
fn args_section(context: &SectionContext) -> FxHashSet<String> {
19741981
let mut following_lines = context.following_lines();
@@ -2018,7 +2025,11 @@ fn args_section(context: &SectionContext) -> FxHashSet<String> {
20182025

20192026
matches
20202027
.iter()
2021-
.filter_map(|captures| captures.get(1).map(|arg_name| arg_name.as_str().to_owned()))
2028+
.filter_map(|captures| {
2029+
captures
2030+
.get(1)
2031+
.map(|arg_name| unescape_parameter_name(arg_name.as_str()))
2032+
})
20222033
.collect::<FxHashSet<String>>()
20232034
}
20242035

@@ -2051,7 +2062,7 @@ fn parameters_section(checker: &Checker, docstring: &Docstring, context: &Sectio
20512062
// Notably, NumPy lets you put multiple parameters of the same type on the same
20522063
// line.
20532064
for parameter in parameters.split(',') {
2054-
docstring_args.insert(parameter.trim().to_owned());
2065+
docstring_args.insert(unescape_parameter_name(parameter.trim()));
20552066
}
20562067
}
20572068

‎crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__d417_google.snap‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,3 +111,11 @@ D417 Missing argument description in the docstring for `function_with_unpack_and
111111
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
112112
239 | """Function with Unpack kwargs but missing query arg documentation.
113113
|
114+
115+
D417 Missing argument descriptions in the docstring for `escaped_stars_google_missing`: `*args`, `x`
116+
--> D417.py:282:5
117+
|
118+
282 | def escaped_stars_google_missing(x, *args, **kwargs):
119+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
120+
283 | r"""Do something.
121+
|

‎crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__d417_google_ignore_var_parameters.snap‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,11 @@ D417 Missing argument description in the docstring for `function_with_unpack_and
9393
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
9494
239 | """Function with Unpack kwargs but missing query arg documentation.
9595
|
96+
97+
D417 Missing argument description in the docstring for `escaped_stars_google_missing`: `x`
98+
--> D417.py:282:5
99+
|
100+
282 | def escaped_stars_google_missing(x, *args, **kwargs):
101+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
102+
283 | r"""Do something.
103+
|
Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11
---
22
source: crates/ruff_linter/src/rules/pydocstyle/mod.rs
33
---
4-
4+
D417 Missing argument descriptions in the docstring for `escaped_stars_numpy_missing`: `*args`, `x`
5+
--> D417.py:262:5
6+
|
7+
262 | def escaped_stars_numpy_missing(x, *args, **kwargs):
8+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
9+
263 | r"""Do something.
10+
|

‎crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__d417_unspecified.snap‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,3 +111,19 @@ D417 Missing argument description in the docstring for `function_with_unpack_and
111111
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
112112
239 | """Function with Unpack kwargs but missing query arg documentation.
113113
|
114+
115+
D417 Missing argument descriptions in the docstring for `escaped_stars_numpy_missing`: `*args`, `x`
116+
--> D417.py:262:5
117+
|
118+
262 | def escaped_stars_numpy_missing(x, *args, **kwargs):
119+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
120+
263 | r"""Do something.
121+
|
122+
123+
D417 Missing argument descriptions in the docstring for `escaped_stars_google_missing`: `*args`, `x`
124+
--> D417.py:282:5
125+
|
126+
282 | def escaped_stars_google_missing(x, *args, **kwargs):
127+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
128+
283 | r"""Do something.
129+
|

‎crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__d417_unspecified_ignore_var_parameters.snap‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,3 +111,19 @@ D417 Missing argument description in the docstring for `function_with_unpack_and
111111
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
112112
239 | """Function with Unpack kwargs but missing query arg documentation.
113113
|
114+
115+
D417 Missing argument descriptions in the docstring for `escaped_stars_numpy_missing`: `*args`, `x`
116+
--> D417.py:262:5
117+
|
118+
262 | def escaped_stars_numpy_missing(x, *args, **kwargs):
119+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
120+
263 | r"""Do something.
121+
|
122+
123+
D417 Missing argument descriptions in the docstring for `escaped_stars_google_missing`: `*args`, `x`
124+
--> D417.py:282:5
125+
|
126+
282 | def escaped_stars_google_missing(x, *args, **kwargs):
127+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
128+
283 | r"""Do something.
129+
|

0 commit comments

Comments
 (0)