Skip to main content

rustdoc/doctest/
rust.rs

1//! Doctest functionality used only for doctests in `.rs` source files.
2
3use std::cell::Cell;
4use std::str::FromStr;
5use std::sync::Arc;
6
7use proc_macro2::{TokenStream, TokenTree};
8use rustc_attr_ir::{AttributeKind, CfgEntry, find_attr};
9use rustc_attr_parsing::eval_config_entry;
10use rustc_hir::def_id::{CRATE_DEF_ID, LocalDefId};
11use rustc_hir::{self as hir, CRATE_HIR_ID, intravisit};
12use rustc_middle::hir::nested_filter;
13use rustc_middle::ty::TyCtxt;
14use rustc_resolve::rustdoc::span_of_fragments;
15use rustc_span::source_map::SourceMap;
16use rustc_span::{BytePos, DUMMY_SP, FileName, Pos, Span, sym};
17
18use super::{DocTestVisitor, ScrapedDocTest};
19use crate::clean::cfg::Cfg;
20use crate::clean::{Attributes, CfgInfo};
21use crate::html::markdown::{self, CodeBlockPosition, CodeLineMapping, ErrorCodes, LangString};
22
23struct RustCollector {
24    source_map: Arc<SourceMap>,
25    tests: Vec<ScrapedDocTest>,
26    cur_path: Vec<String>,
27    position: Span,
28    global_crate_attrs: Vec<String>,
29}
30
31impl RustCollector {
32    fn get_filename(&self) -> FileName {
33        let filename = self.source_map.span_to_filename(self.position);
34        filename
35    }
36
37    fn get_base_line(&self) -> usize {
38        let sp_lo = self.position.lo().to_usize();
39        let loc = self.source_map.lookup_char_pos(BytePos(sp_lo as u32));
40        loc.line
41    }
42}
43
44impl DocTestVisitor for RustCollector {
45    fn visit_test(
46        &mut self,
47        test: String,
48        config: LangString,
49        position: CodeBlockPosition,
50        code_mappings: Vec<CodeLineMapping>,
51    ) {
52        let base_line = self.get_base_line();
53        let line = match position {
54            CodeBlockPosition::Relative(rel_line) => base_line + rel_line.offset(),
55            CodeBlockPosition::Span(sp) => self.source_map.lookup_char_pos(sp.lo()).line,
56        };
57        let count = Cell::new(base_line);
58        let span = if line > base_line {
59            match self.source_map.span_extend_while(self.position, |c| {
60                if c == '\n' {
61                    let count_v = count.get();
62                    count.set(count_v + 1);
63                    if count_v >= line {
64                        return false;
65                    }
66                }
67                true
68            }) {
69                Ok(sp) => self.source_map.span_extend_to_line(sp.shrink_to_hi()),
70                _ => self.position,
71            }
72        } else {
73            self.position
74        };
75        self.tests.push(ScrapedDocTest::new(
76            self.get_filename(),
77            line,
78            self.cur_path.clone(),
79            config,
80            test,
81            span,
82            code_mappings,
83            self.global_crate_attrs.clone(),
84        ));
85    }
86
87    fn visit_header(&mut self, _name: &str, _level: u32) {}
88}
89
90pub(super) struct HirCollector<'tcx> {
91    codes: ErrorCodes,
92    tcx: TyCtxt<'tcx>,
93    collector: RustCollector,
94}
95
96impl<'tcx> HirCollector<'tcx> {
97    pub fn new(codes: ErrorCodes, tcx: TyCtxt<'tcx>) -> Self {
98        let collector = RustCollector {
99            source_map: tcx.sess.psess.clone_source_map(),
100            cur_path: vec![],
101            position: DUMMY_SP,
102            tests: vec![],
103            global_crate_attrs: Vec::new(),
104        };
105        Self { codes, tcx, collector }
106    }
107
108    pub fn collect_crate(mut self) -> Vec<ScrapedDocTest> {
109        let tcx = self.tcx;
110        self.visit_testable(None, CRATE_DEF_ID, tcx.hir_span(CRATE_HIR_ID), |this| {
111            tcx.hir_walk_toplevel_module(this)
112        });
113        self.collector.tests
114    }
115}
116
117impl HirCollector<'_> {
118    fn visit_testable<F: FnOnce(&mut Self)>(
119        &mut self,
120        name: Option<String>,
121        def_id: LocalDefId,
122        sp: Span,
123        nested: F,
124    ) {
125        let hir_attrs = self.tcx.hir_attrs(self.tcx.local_def_id_to_hir_id(def_id));
126
127        let mut cfg_info = CfgInfo::default();
128        let mut found_features = 0;
129
130        let source_map = self.tcx.sess.source_map();
131        let old_global_crate_attrs_len = self.collector.global_crate_attrs.len();
132        // This loop does two things:
133        //
134        // 1. Collect `#[target_feature(...)]`.
135        // 2. Collect `#[doc(test(attr(...)))]`.
136        for attr in hir_attrs.iter() {
137            let rustc_attr_ir::Attribute::Parsed(attr) = attr else { continue };
138            if let AttributeKind::TargetFeature { features, .. } = attr {
139                for (feature, _) in features {
140                    found_features += 1;
141                    cfg_info.current_cfg &= Cfg(CfgEntry::NameValue {
142                        name: sym::target_feature,
143                        value: Some(*feature),
144                        span: DUMMY_SP,
145                    });
146                }
147            } else if let AttributeKind::Doc(d) = attr {
148                for attr_span in &d.test_attrs {
149                    // FIXME: This is ugly, remove when `test_attrs` has been ported to new
150                    // attribute API.
151                    if let Ok(snippet) = source_map.span_to_snippet(*attr_span)
152                        && let Ok(stream) = TokenStream::from_str(&snippet)
153                    {
154                        let mut iter = stream.into_iter().peekable();
155                        while let Some(token) = iter.next() {
156                            if let TokenTree::Ident(i) = token {
157                                let i = i.to_string();
158                                let peek = iter.peek();
159                                // From this ident, we can have things like:
160                                //
161                                // * Group: `allow(...)`
162                                // * Name/value: `crate_name = "..."`
163                                // * Tokens: `html_no_url`
164                                //
165                                // So we peek next element to know what case we are in.
166                                match peek {
167                                    Some(TokenTree::Group(g)) => {
168                                        let g = g.to_string();
169                                        iter.next();
170                                        // Add the additional attributes to the `global_crate_attrs`
171                                        // vector
172                                        self.collector.global_crate_attrs.push(format!("{i}{g}"));
173                                    }
174                                    // If next item is `=`, it means it's a name value so we will
175                                    // need to get the value as well.
176                                    Some(TokenTree::Punct(p)) if p.as_char() == '=' => {
177                                        let p = p.to_string();
178                                        iter.next();
179                                        if let Some(last) = iter.next() {
180                                            // Add the additional attributes to the
181                                            // `global_crate_attrs` vector.
182                                            self.collector
183                                                .global_crate_attrs
184                                                .push(format!("{i}{p}{last}"));
185                                        }
186                                    }
187                                    _ => {
188                                        // Add the additional attributes to the `global_crate_attrs`
189                                        // vector.
190                                        self.collector.global_crate_attrs.push(i.to_string());
191                                    }
192                                }
193                            }
194                        }
195                    }
196                }
197            }
198        }
199
200        // We only look at the `target_feature` attributes as the `cfg` attributes have already been
201        // applied at this point, so no need to take them into account again.
202        if found_features != 0
203            && !eval_config_entry(&self.tcx.sess, &cfg_info.current_cfg.inner()).as_bool()
204        {
205            return;
206        }
207
208        let mut has_name = false;
209        if let Some(name) = name {
210            self.collector.cur_path.push(name);
211            has_name = true;
212        }
213
214        // The collapse-docs pass won't combine sugared/raw doc attributes, or included files with
215        // anything else, this will combine them for us.
216        let attrs = Attributes::from_hir(hir_attrs);
217        if let Some(doc) = attrs.opt_doc_value() {
218            let span = span_of_fragments(&attrs.doc_strings).unwrap_or(sp);
219            self.collector.position = if span.edition().at_least_rust_2024() {
220                span
221            } else {
222                // this span affects filesystem path resolution,
223                // so we need to keep it the same as it was previously
224                let first_doc_str_span = find_attr!(hir_attrs, DocComment { span, .. } => *span);
225                let cause_span = first_doc_str_span
226                    .map(|sp| sp.ctxt().outer_expn().expansion_cause().unwrap_or(sp));
227                cause_span.unwrap_or(DUMMY_SP)
228            };
229            markdown::find_testable_code(
230                &doc,
231                &mut self.collector,
232                self.codes,
233                Some(&crate::html::markdown::ExtraInfo::new(
234                    self.tcx,
235                    def_id,
236                    span,
237                    Some(&attrs.doc_strings),
238                )),
239            );
240        }
241
242        nested(self);
243
244        // Restore global_crate_attrs to it's previous size/content
245        self.collector.global_crate_attrs.truncate(old_global_crate_attrs_len);
246
247        if has_name {
248            self.collector.cur_path.pop();
249        }
250    }
251}
252
253impl<'tcx> intravisit::Visitor<'tcx> for HirCollector<'tcx> {
254    type NestedFilter = nested_filter::All;
255
256    fn maybe_tcx(&mut self) -> Self::MaybeTyCtxt {
257        self.tcx
258    }
259
260    fn visit_item(&mut self, item: &'tcx hir::Item<'_>) {
261        let name = match &item.kind {
262            hir::ItemKind::Impl(impl_) => {
263                Some(rustc_hir_pretty::id_to_string(&self.tcx, impl_.self_ty.hir_id))
264            }
265            _ => item.kind.ident().map(|ident| ident.to_string()),
266        };
267
268        self.visit_testable(name, item.owner_id.def_id, item.span, |this| {
269            intravisit::walk_item(this, item);
270        });
271    }
272
273    fn visit_trait_item(&mut self, item: &'tcx hir::TraitItem<'_>) {
274        self.visit_testable(
275            Some(item.ident.to_string()),
276            item.owner_id.def_id,
277            item.span,
278            |this| {
279                intravisit::walk_trait_item(this, item);
280            },
281        );
282    }
283
284    fn visit_impl_item(&mut self, item: &'tcx hir::ImplItem<'_>) {
285        self.visit_testable(
286            Some(item.ident.to_string()),
287            item.owner_id.def_id,
288            item.span,
289            |this| {
290                intravisit::walk_impl_item(this, item);
291            },
292        );
293    }
294
295    fn visit_foreign_item(&mut self, item: &'tcx hir::ForeignItem<'_>) {
296        self.visit_testable(
297            Some(item.ident.to_string()),
298            item.owner_id.def_id,
299            item.span,
300            |this| {
301                intravisit::walk_foreign_item(this, item);
302            },
303        );
304    }
305
306    fn visit_variant(&mut self, v: &'tcx hir::Variant<'_>) {
307        self.visit_testable(Some(v.ident.to_string()), v.def_id, v.span, |this| {
308            intravisit::walk_variant(this, v);
309        });
310    }
311
312    fn visit_field_def(&mut self, f: &'tcx hir::FieldDef<'_>) {
313        self.visit_testable(Some(f.ident.to_string()), f.def_id, f.span, |this| {
314            intravisit::walk_field_def(this, f);
315        });
316    }
317}