wowlab_tidy/languages/rust/rules/docs/
doc_inline_reexport.rs1use crate::{Example, FileCtx, Violation, infra::parse, violation};
2
3#[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
70const LOCAL_PREFIXES: &[&str] = &["pub use crate::", "pub use self::", "pub use super::"];
72const EXTERNAL_PREFIXES: &[&str] = &["pub use std::", "pub use core::", "pub use alloc::"];
74const DOC_INLINE: &str = "#[doc(inline)]";
76const 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});