rust.rs - source (original) (raw)

rustdoc/doctest/

rust.rs

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