From b2c16749fb3e9c2a067b409d2a3ae27c889f47d2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 14 Sep 2026 13:23:39 +0900 Subject: [PATCH] fix(quality): keep rustdoc attached across multi-line attributes check_docstrings.py skipped only lines starting with '#[', so the continuation lines of a multi-line attribute reset the documented state and a documented public item was reported as undocumented (observed on TEPP#372). Track bracket depth so every attribute line is transparent. RED: new test_multi_line_attributes_do_not_detach_rustdoc failed with 2 != 1 before the fix. Python line+branch coverage stays at 100%. Co-Authored-By: Claude Fable 5.1 --- .../docstring-checker-multiline-attributes.md | 6 +++++ scripts/check_docstrings.py | 9 ++++++- tests/quality/test_check_docstrings.py | 26 +++++++++++++++++++ 3 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 CHANGELOG.d/docstring-checker-multiline-attributes.md diff --git a/CHANGELOG.d/docstring-checker-multiline-attributes.md b/CHANGELOG.d/docstring-checker-multiline-attributes.md new file mode 100644 index 000000000..2d9be9b23 --- /dev/null +++ b/CHANGELOG.d/docstring-checker-multiline-attributes.md @@ -0,0 +1,6 @@ +### Fixed + +- `scripts/check_docstrings.py` no longer reports a documented public item as + undocumented when a multi-line attribute (for example `#[expect(...)]`) + sits between its `///` block and the item; attribute continuation lines + are now transparent up to the closing bracket. diff --git a/scripts/check_docstrings.py b/scripts/check_docstrings.py index ea11417cf..5390f8906 100644 --- a/scripts/check_docstrings.py +++ b/scripts/check_docstrings.py @@ -29,12 +29,19 @@ def validate_source(path: Path) -> list[str]: errors.append(f"{path}: missing crate/module-level //! rustdoc") documented = False + open_attribute_brackets = 0 for line_number, line in enumerate(lines, start=1): stripped = line.strip() + if open_attribute_brackets: + open_attribute_brackets += stripped.count("[") - stripped.count("]") + continue if stripped.startswith("///") or stripped.startswith("#[doc"): documented = True continue - if stripped.startswith("#[") or not stripped: + if stripped.startswith("#["): + open_attribute_brackets = stripped.count("[") - stripped.count("]") + continue + if not stripped: continue if PUBLIC_ITEM_PATTERN.match(line): if not documented: diff --git a/tests/quality/test_check_docstrings.py b/tests/quality/test_check_docstrings.py index 750f0e350..fd7406915 100644 --- a/tests/quality/test_check_docstrings.py +++ b/tests/quality/test_check_docstrings.py @@ -78,6 +78,32 @@ def test_documented_and_undocumented_items(self) -> None: self.assertEqual(len(errors), 1) self.assertIn("public item lacks", errors[0]) + def test_multi_line_attributes_do_not_detach_rustdoc(self) -> None: + """Continuation lines of a multi-line attribute stay transparent.""" + + with tempfile.TemporaryDirectory() as temporary: + source = Path(temporary) / "lib.rs" + source.write_text( + "//! Module docs.\n" + "\n" + "/// Documented behind a multi-line attribute.\n" + "#[expect(\n" + " clippy::missing_panics_doc,\n" + " reason = \"bounded constants cannot fail\"\n" + ")]\n" + "pub fn documented() {}\n" + "\n" + "#[cfg_attr(\n" + " feature = \"serde\",\n" + " derive(serde::Serialize)\n" + ")]\n" + "pub struct Undocumented;\n", + encoding="utf-8", + ) + errors = docstrings.validate_source(source) + self.assertEqual(len(errors), 1) + self.assertIn(":14: public item lacks", errors[0]) + def test_missing_module_docs_are_reported(self) -> None: """Crate or module documentation is mandatory."""