1use 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 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 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 match peek {
167 Some(TokenTree::Group(g)) => {
168 let g = g.to_string();
169 iter.next();
170 self.collector.global_crate_attrs.push(format!("{i}{g}"));
173 }
174 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 self.collector
183 .global_crate_attrs
184 .push(format!("{i}{p}{last}"));
185 }
186 }
187 _ => {
188 self.collector.global_crate_attrs.push(i.to_string());
191 }
192 }
193 }
194 }
195 }
196 }
197 }
198 }
199
200 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 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 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 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}