rustdoc/passes/
check_doc_test_visibility.rs1use rustc_hir as hir;
9use rustc_macros::Diagnostic;
10use rustc_middle::lint::LintLevelSource;
11use tracing::debug;
12
13use crate::clean::utils::inherits_doc_hidden;
14use crate::clean::{self, *};
15use crate::core::DocContext;
16use crate::html::markdown::{
17 CodeBlockPosition, CodeLineMapping, ErrorCodes, Ignore, LangString, find_testable_code,
18};
19use crate::visit::DocVisitor;
20
21struct DocTestVisibilityLinter<'a, 'tcx> {
22 cx: &'a mut DocContext<'tcx>,
23}
24
25pub(super) fn check_doc_test_visibility(krate: Crate, cx: &mut DocContext<'_>) -> Crate {
26 let mut coll = DocTestVisibilityLinter { cx };
27 coll.visit_crate(&krate);
28 krate
29}
30
31impl DocVisitor<'_> for DocTestVisibilityLinter<'_, '_> {
32 fn visit_item(&mut self, item: &Item) {
33 look_for_tests(self.cx, &item.doc_value(), item);
34
35 self.visit_item_recur(item)
36 }
37}
38
39pub(crate) struct Tests {
40 pub(crate) found_tests: usize,
41}
42
43impl crate::doctest::DocTestVisitor for Tests {
44 fn visit_test(
45 &mut self,
46 _: String,
47 config: LangString,
48 _: CodeBlockPosition,
49 _: Vec<CodeLineMapping>,
50 ) {
51 if config.rust && config.ignore == Ignore::None {
52 self.found_tests += 1;
53 }
54 }
55}
56
57pub(crate) fn should_have_doc_example(cx: &DocContext<'_>, item: &clean::Item) -> bool {
58 if !(cx.cache.effective_visibilities.is_directly_public(cx.tcx, item.item_id.expect_def_id())
59 || item.is_exported_decl_macro())
60 || matches!(
61 item.kind,
62 ItemKind::StructField(_)
63 | ItemKind::Variant(_)
64 | ItemKind::TypeAlias(_)
65 | ItemKind::Static(_)
66 | ItemKind::Constant(..)
67 | ItemKind::ExternCrate { .. }
68 | ItemKind::Import(_)
69 | ItemKind::PrimitiveType(_)
70 | ItemKind::Keyword
71 | ItemKind::Attribute
72 | ItemKind::Module(_)
73 | ItemKind::TraitAlias(_)
74 | ItemKind::ForeignFunction(..)
75 | ItemKind::ForeignStatic(..)
76 | ItemKind::ForeignType
77 | ItemKind::AssocType(..)
78 | ItemKind::AssocConstant(..)
79 | ItemKind::Impl(_)
80 | ItemKind::PlaceholderImpl
81 )
82 {
83 return false;
84 }
85
86 let def_id = item.item_id.expect_def_id().expect_local();
89
90 if let Some(parent_def_id) = cx.tcx.opt_local_parent(def_id)
92 && matches!(
93 cx.tcx.hir_node_by_def_id(parent_def_id),
94 hir::Node::Item(hir::Item {
95 kind: hir::ItemKind::Impl(hir::Impl { of_trait: Some(_), .. }),
96 ..
97 })
98 )
99 {
100 return false;
101 }
102
103 if (!cx.document_hidden()
104 && (cx.tcx.is_doc_hidden(def_id.to_def_id()) || inherits_doc_hidden(cx.tcx, def_id, None)))
105 || cx.tcx.def_span(def_id.to_def_id()).in_derive_expansion()
106 {
107 return false;
108 }
109 let level_spec = cx.tcx.lint_level_spec_at_node(
110 crate::lint::MISSING_DOC_CODE_EXAMPLES,
111 cx.tcx.local_def_id_to_hir_id(def_id),
112 );
113 !level_spec.is_allow() || matches!(level_spec.src, LintLevelSource::Default)
114}
115
116pub(crate) fn look_for_tests(cx: &DocContext<'_>, dox: &str, item: &Item) {
117 #[derive(Diagnostic)]
118 #[diag("missing code example in this documentation")]
119 struct MissingCodeExample;
120
121 #[derive(Diagnostic)]
122 #[diag("documentation test in private item")]
123 struct DoctestInPrivateItem;
124
125 let Some(hir_id) = DocContext::as_local_hir_id(cx.tcx, item.item_id) else {
126 return;
128 };
129
130 let mut tests = Tests { found_tests: 0 };
131
132 find_testable_code(dox, &mut tests, ErrorCodes::No, None);
133
134 if tests.found_tests == 0 && cx.tcx.features().rustdoc_missing_doc_code_examples() {
135 if should_have_doc_example(cx, item) {
136 debug!("reporting error for {item:?} (hir_id={hir_id:?})");
137 let sp = item.attr_span(cx.tcx);
138 cx.tcx.emit_node_span_lint(
139 crate::lint::MISSING_DOC_CODE_EXAMPLES,
140 hir_id,
141 sp,
142 MissingCodeExample,
143 );
144 }
145 } else if tests.found_tests > 0
146 && !cx.cache.effective_visibilities.is_exported(cx.tcx, item.item_id.expect_def_id())
147 {
148 cx.tcx.emit_node_span_lint(
149 crate::lint::PRIVATE_DOC_TESTS,
150 hir_id,
151 item.attr_span(cx.tcx),
152 DoctestInPrivateItem,
153 );
154 }
155}