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."""