Skip to main content

wowlab_tidy/languages/rust/rules/docs/
doc_inline_reexport.rs

1use crate::{Example, FileCtx, Violation, infra::parse, violation};
2
3/// Pass/fail cases for `example_tests!`.
4#[rustfmt::skip]
5const EXAMPLES: &[Example] = &[
6    Example {
7        label: "local re-export without doc(inline)",
8        code: "pub use crate::foo::Bar;",
9        pass: false,
10    },
11    Example {
12        label: "local re-export with doc(inline)",
13        code: "#[doc(inline)]\npub use crate::foo::Bar;",
14        pass: true,
15    },
16    Example {
17        label: "local re-export with doc(hidden)",
18        code: "#[doc(hidden)]\npub use crate::internal::Bar;",
19        pass: true,
20    },
21    Example {
22        label: "self re-export without doc(inline)",
23        code: "pub use self::foo::Bar;",
24        pass: false,
25    },
26    Example {
27        label: "super re-export without doc(inline)",
28        code: "pub use super::foo::Bar;",
29        pass: false,
30    },
31    Example {
32        label: "same-line attribute on local re-export",
33        code: "#[doc(inline)] pub use crate::foo::Bar;",
34        pass: true,
35    },
36    Example {
37        label: "inlined std re-export",
38        code: "#[doc(inline)]\npub use std::fmt::Debug;",
39        pass: false,
40    },
41    Example {
42        label: "same-line inlined core re-export",
43        code: "#[doc(inline)] pub use core::fmt::Debug;",
44        pass: false,
45    },
46    Example {
47        label: "std re-export without inline is correct",
48        code: "pub use std::fmt::Debug;",
49        pass: true,
50    },
51    Example {
52        label: "non-pub use is not a re-export",
53        code: "use crate::foo::Bar;",
54        pass: true,
55    },
56    Example {
57        label: "commented-out re-export",
58        code: "// pub use crate::foo::Bar;",
59        pass: true,
60    },
61];
62
63crate::line_rule!(
64    doc_inline_reexport,
65    "Require `#[doc(inline)]` on local re-exports and forbid it on external ones.",
66    "Local re-exports should inline into module docs while external items stay visibly external (M-DOC-INLINE).",
67    Low,
68);
69
70/// Re-export path prefixes that point at items of this crate.
71const LOCAL_PREFIXES: &[&str] = &["pub use crate::", "pub use self::", "pub use super::"];
72/// Re-export path prefixes that point at external items.
73const EXTERNAL_PREFIXES: &[&str] = &["pub use std::", "pub use core::", "pub use alloc::"];
74/// Attribute that inlines a re-export into rustdoc.
75const DOC_INLINE: &str = "#[doc(inline)]";
76/// Attribute that hides a re-export from rustdoc.
77const DOC_HIDDEN: &str = "#[doc(hidden)]";
78
79fn check_doc_inline_reexport(ctx: &FileCtx<'_>) -> Vec<Violation> {
80    let mut out = Vec::new();
81
82    for (i, line) in ctx.lines.iter().enumerate() {
83        let trimmed = line.trim();
84
85        if parse::is_comment(trimmed) {
86            continue;
87        }
88
89        if LOCAL_PREFIXES.iter().any(|p| trimmed.starts_with(p)) && !has_doc_attr_above(ctx, i) {
90            out.push(violation(
91                ctx.rel,
92                i + 1,
93                "mark local re-exports with `#[doc(inline)]` (or `#[doc(hidden)]`)",
94            ));
95        }
96
97        if let Some(rest) = trimmed.strip_prefix(DOC_INLINE) {
98            if inlines_external(ctx, i, rest) {
99                out.push(violation(
100                    ctx.rel,
101                    i + 1,
102                    "do not `#[doc(inline)]` re-exports of external items",
103                ));
104            }
105        }
106    }
107
108    out
109}
110
111fn has_doc_attr_above(ctx: &FileCtx<'_>, index: usize) -> bool {
112    let previous_line = index
113        .checked_sub(1)
114        .and_then(|prev| ctx.lines.get(prev))
115        .map(|line| line.trim());
116
117    previous_line.is_some_and(|prev| prev.starts_with(DOC_INLINE) || prev.starts_with(DOC_HIDDEN))
118}
119
120fn inlines_external(ctx: &FileCtx<'_>, index: usize, rest: &str) -> bool {
121    let same_line = rest.trim_start();
122
123    if EXTERNAL_PREFIXES.iter().any(|p| same_line.starts_with(p)) {
124        return true;
125    }
126
127    ctx.lines
128        .get(index + 1)
129        .map(|line| line.trim())
130        .is_some_and(|next| EXTERNAL_PREFIXES.iter().any(|p| next.starts_with(p)))
131}
132
133crate::tidy_test!(check_doc_inline_reexport, {
134    crate::example_tests!(EXAMPLES, check_doc_inline_reexport);
135});