rustdoc/doctest/
rust.rs

1//! Doctest functionality used only for doctests in `.rs` source files.
2
3use std::cell::Cell;
4use std::env;
5use std::sync::Arc;
6
7use rustc_data_structures::fx::FxHashSet;
8use rustc_hir::def_id::{CRATE_DEF_ID, LocalDefId};
9use rustc_hir::{self as hir, CRATE_HIR_ID, intravisit};
10use rustc_middle::hir::nested_filter;
11use rustc_middle::ty::TyCtxt;
12use rustc_resolve::rustdoc::span_of_fragments;
13use rustc_span::source_map::SourceMap;
14use rustc_span::{BytePos, DUMMY_SP, FileName, Pos, Span};
15
16use super::{DocTestVisitor, ScrapedDocTest};
17use crate::clean::{Attributes, extract_cfg_from_attrs};
18use crate::html::markdown::{self, ErrorCodes, LangString, MdRelLine};
19
20struct RustCollector {
21    source_map: Arc<SourceMap>,
22    tests: Vec<ScrapedDocTest>,
23    cur_path: Vec<String>,
24    position: Span,
25}
26
27impl RustCollector {
28    fn get_filename(&self) -> FileName {
29        let filename = self.source_map.span_to_filename(self.position);
30        if let FileName::Real(ref filename) = filename {
31            let path = filename.remapped_path_if_available();
32            // Strip the cwd prefix from the path. This will likely exist if
33            // the path was not remapped.
34            let path = env::current_dir()
35                .map(|cur_dir| path.strip_prefix(&cur_dir).unwrap_or(path))
36                .unwrap_or(path);
37            return path.to_owned().into();
38        }
39        filename
40    }
41
42    fn get_base_line(&self) -> usize {
43        let sp_lo = self.position.lo().to_usize();
44        let loc = self.source_map.lookup_char_pos(BytePos(sp_lo as u32));
45        loc.line
46    }
47}
48
49impl DocTestVisitor for RustCollector {
50    fn visit_test(&mut self, test: String, config: LangString, rel_line: MdRelLine) {
51        let base_line = self.get_base_line();
52        let line = base_line + rel_line.offset();
53        let count = Cell::new(base_line);
54        let span = if line > base_line {
55            match self.source_map.span_extend_while(self.position, |c| {
56                if c == '\n' {
57                    let count_v = count.get();
58                    count.set(count_v + 1);
59                    if count_v >= line {
60                        return false;
61                    }
62                }
63                true
64            }) {
65                Ok(sp) => self.source_map.span_extend_to_line(sp.shrink_to_hi()),
66                _ => self.position,
67            }
68        } else {
69            self.position
70        };
71        self.tests.push(ScrapedDocTest::new(
72            self.get_filename(),
73            line,
74            self.cur_path.clone(),
75            config,
76            test,
77            span,
78        ));
79    }
80
81    fn visit_header(&mut self, _name: &str, _level: u32) {}
82}
83
84pub(super) struct HirCollector<'tcx> {
85    codes: ErrorCodes,
86    tcx: TyCtxt<'tcx>,
87    collector: RustCollector,
88}
89
90impl<'tcx> HirCollector<'tcx> {
91    pub fn new(codes: ErrorCodes, tcx: TyCtxt<'tcx>) -> Self {
92        let collector = RustCollector {
93            source_map: tcx.sess.psess.clone_source_map(),
94            cur_path: vec![],
95            position: DUMMY_SP,
96            tests: vec![],
97        };
98        Self { codes, tcx, collector }
99    }
100
101    pub fn collect_crate(mut self) -> Vec<ScrapedDocTest> {
102        let tcx = self.tcx;
103        self.visit_testable(None, CRATE_DEF_ID, tcx.hir_span(CRATE_HIR_ID), |this| {
104            tcx.hir_walk_toplevel_module(this)
105        });
106        self.collector.tests
107    }
108}
109
110impl HirCollector<'_> {
111    fn visit_testable<F: FnOnce(&mut Self)>(
112        &mut self,
113        name: Option<String>,
114        def_id: LocalDefId,
115        sp: Span,
116        nested: F,
117    ) {
118        let ast_attrs = self.tcx.hir_attrs(self.tcx.local_def_id_to_hir_id(def_id));
119        if let Some(ref cfg) =
120            extract_cfg_from_attrs(ast_attrs.iter(), self.tcx, &FxHashSet::default())
121            && !cfg.matches(&self.tcx.sess.psess)
122        {
123            return;
124        }
125
126        let mut has_name = false;
127        if let Some(name) = name {
128            self.collector.cur_path.push(name);
129            has_name = true;
130        }
131
132        // The collapse-docs pass won't combine sugared/raw doc attributes, or included files with
133        // anything else, this will combine them for us.
134        let attrs = Attributes::from_hir(ast_attrs);
135        if let Some(doc) = attrs.opt_doc_value() {
136            let span = span_of_fragments(&attrs.doc_strings).unwrap_or(sp);
137            self.collector.position = if span.edition().at_least_rust_2024() {
138                span
139            } else {
140                // this span affects filesystem path resolution,
141                // so we need to keep it the same as it was previously
142                ast_attrs
143                    .iter()
144                    .find(|attr| attr.doc_str().is_some())
145                    .map(|attr| {
146                        attr.span().ctxt().outer_expn().expansion_cause().unwrap_or(attr.span())
147                    })
148                    .unwrap_or(DUMMY_SP)
149            };
150            markdown::find_testable_code(
151                &doc,
152                &mut self.collector,
153                self.codes,
154                Some(&crate::html::markdown::ExtraInfo::new(self.tcx, def_id, span)),
155            );
156        }
157
158        nested(self);
159
160        if has_name {
161            self.collector.cur_path.pop();
162        }
163    }
164}
165
166impl<'tcx> intravisit::Visitor<'tcx> for HirCollector<'tcx> {
167    type NestedFilter = nested_filter::All;
168
169    fn maybe_tcx(&mut self) -> Self::MaybeTyCtxt {
170        self.tcx
171    }
172
173    fn visit_item(&mut self, item: &'tcx hir::Item<'_>) {
174        let name = match &item.kind {
175            hir::ItemKind::Impl(impl_) => {
176                Some(rustc_hir_pretty::id_to_string(&self.tcx, impl_.self_ty.hir_id))
177            }
178            _ => item.kind.ident().map(|ident| ident.to_string()),
179        };
180
181        self.visit_testable(name, item.owner_id.def_id, item.span, |this| {
182            intravisit::walk_item(this, item);
183        });
184    }
185
186    fn visit_trait_item(&mut self, item: &'tcx hir::TraitItem<'_>) {
187        self.visit_testable(
188            Some(item.ident.to_string()),
189            item.owner_id.def_id,
190            item.span,
191            |this| {
192                intravisit::walk_trait_item(this, item);
193            },
194        );
195    }
196
197    fn visit_impl_item(&mut self, item: &'tcx hir::ImplItem<'_>) {
198        self.visit_testable(
199            Some(item.ident.to_string()),
200            item.owner_id.def_id,
201            item.span,
202            |this| {
203                intravisit::walk_impl_item(this, item);
204            },
205        );
206    }
207
208    fn visit_foreign_item(&mut self, item: &'tcx hir::ForeignItem<'_>) {
209        self.visit_testable(
210            Some(item.ident.to_string()),
211            item.owner_id.def_id,
212            item.span,
213            |this| {
214                intravisit::walk_foreign_item(this, item);
215            },
216        );
217    }
218
219    fn visit_variant(&mut self, v: &'tcx hir::Variant<'_>) {
220        self.visit_testable(Some(v.ident.to_string()), v.def_id, v.span, |this| {
221            intravisit::walk_variant(this, v);
222        });
223    }
224
225    fn visit_field_def(&mut self, f: &'tcx hir::FieldDef<'_>) {
226        self.visit_testable(Some(f.ident.to_string()), f.def_id, f.span, |this| {
227            intravisit::walk_field_def(this, f);
228        });
229    }
230}