blob: 4e9b1ee36f3dbd77f7e815ccb4b651e6c83127d8 [file] [log] [blame]
//! Doctest functionality used only for doctests in `.rs` source files.
use std::cell::Cell;
use std::env;
use std::sync::Arc;
use rustc_ast_pretty::pprust;
use rustc_data_structures::fx::FxHashSet;
use rustc_hir::def_id::{CRATE_DEF_ID, LocalDefId};
use rustc_hir::{self as hir, CRATE_HIR_ID, intravisit};
use rustc_middle::hir::nested_filter;
use rustc_middle::ty::TyCtxt;
use rustc_resolve::rustdoc::span_of_fragments;
use rustc_span::source_map::SourceMap;
use rustc_span::{BytePos, DUMMY_SP, FileName, Pos, Span, sym};
use super::{DocTestVisitor, ScrapedDocTest};
use crate::clean::{Attributes, extract_cfg_from_attrs};
use crate::html::markdown::{self, ErrorCodes, LangString, MdRelLine};
struct RustCollector {
source_map: Arc<SourceMap>,
tests: Vec<ScrapedDocTest>,
cur_path: Vec<String>,
position: Span,
global_crate_attrs: Vec<String>,
}
impl RustCollector {
fn get_filename(&self) -> FileName {
let filename = self.source_map.span_to_filename(self.position);
if let FileName::Real(ref filename) = filename {
let path = filename.remapped_path_if_available();
// Strip the cwd prefix from the path. This will likely exist if
// the path was not remapped.
let path = env::current_dir()
.map(|cur_dir| path.strip_prefix(&cur_dir).unwrap_or(path))
.unwrap_or(path);
return path.to_owned().into();
}
filename
}
fn get_base_line(&self) -> usize {
let sp_lo = self.position.lo().to_usize();
let loc = self.source_map.lookup_char_pos(BytePos(sp_lo as u32));
loc.line
}
}
impl DocTestVisitor for RustCollector {
fn visit_test(&mut self, test: String, config: LangString, rel_line: MdRelLine) {
let base_line = self.get_base_line();
let line = base_line + rel_line.offset();
let count = Cell::new(base_line);
let span = if line > base_line {
match self.source_map.span_extend_while(self.position, |c| {
if c == '\n' {
let count_v = count.get();
count.set(count_v + 1);
if count_v >= line {
return false;
}
}
true
}) {
Ok(sp) => self.source_map.span_extend_to_line(sp.shrink_to_hi()),
_ => self.position,
}
} else {
self.position
};
self.tests.push(ScrapedDocTest::new(
self.get_filename(),
line,
self.cur_path.clone(),
config,
test,
span,
self.global_crate_attrs.clone(),
));
}
fn visit_header(&mut self, _name: &str, _level: u32) {}
}
pub(super) struct HirCollector<'tcx> {
codes: ErrorCodes,
tcx: TyCtxt<'tcx>,
collector: RustCollector,
}
impl<'tcx> HirCollector<'tcx> {
pub fn new(codes: ErrorCodes, tcx: TyCtxt<'tcx>) -> Self {
let collector = RustCollector {
source_map: tcx.sess.psess.clone_source_map(),
cur_path: vec![],
position: DUMMY_SP,
tests: vec![],
global_crate_attrs: Vec::new(),
};
Self { codes, tcx, collector }
}
pub fn collect_crate(mut self) -> Vec<ScrapedDocTest> {
let tcx = self.tcx;
self.visit_testable(None, CRATE_DEF_ID, tcx.hir_span(CRATE_HIR_ID), |this| {
tcx.hir_walk_toplevel_module(this)
});
self.collector.tests
}
}
impl HirCollector<'_> {
fn visit_testable<F: FnOnce(&mut Self)>(
&mut self,
name: Option<String>,
def_id: LocalDefId,
sp: Span,
nested: F,
) {
let ast_attrs = self.tcx.hir_attrs(self.tcx.local_def_id_to_hir_id(def_id));
if let Some(ref cfg) =
extract_cfg_from_attrs(ast_attrs.iter(), self.tcx, &FxHashSet::default())
&& !cfg.matches(&self.tcx.sess.psess)
{
return;
}
let mut has_name = false;
if let Some(name) = name {
self.collector.cur_path.push(name);
has_name = true;
}
// The collapse-docs pass won't combine sugared/raw doc attributes, or included files with
// anything else, this will combine them for us.
let attrs = Attributes::from_hir(ast_attrs);
if let Some(doc) = attrs.opt_doc_value() {
let span = span_of_fragments(&attrs.doc_strings).unwrap_or(sp);
self.collector.position = if span.edition().at_least_rust_2024() {
span
} else {
// this span affects filesystem path resolution,
// so we need to keep it the same as it was previously
ast_attrs
.iter()
.find(|attr| attr.doc_str().is_some())
.map(|attr| {
attr.span().ctxt().outer_expn().expansion_cause().unwrap_or(attr.span())
})
.unwrap_or(DUMMY_SP)
};
markdown::find_testable_code(
&doc,
&mut self.collector,
self.codes,
Some(&crate::html::markdown::ExtraInfo::new(self.tcx, def_id, span)),
);
}
nested(self);
if has_name {
self.collector.cur_path.pop();
}
}
}
impl<'tcx> intravisit::Visitor<'tcx> for HirCollector<'tcx> {
type NestedFilter = nested_filter::All;
fn maybe_tcx(&mut self) -> Self::MaybeTyCtxt {
self.tcx
}
fn visit_mod(&mut self, m: &'tcx hir::Mod<'tcx>, _s: Span, hir_id: hir::HirId) {
let attrs = self.tcx.hir_attrs(hir_id);
if !attrs.is_empty() {
// Try collecting `#![doc(test(attr(...)))]` from the attribute module
let old_len = self.collector.global_crate_attrs.len();
for doc_test_attrs in attrs
.iter()
.filter(|a| a.has_name(sym::doc))
.flat_map(|a| a.meta_item_list().unwrap_or_default())
.filter(|a| a.has_name(sym::test))
{
let Some(doc_test_attrs) = doc_test_attrs.meta_item_list() else { continue };
for attr in doc_test_attrs
.iter()
.filter(|a| a.has_name(sym::attr))
.flat_map(|a| a.meta_item_list().unwrap_or_default())
.map(|i| pprust::meta_list_item_to_string(i))
{
// Add the additional attributes to the global_crate_attrs vector
self.collector.global_crate_attrs.push(attr);
}
}
let r = intravisit::walk_mod(self, m);
// Restore global_crate_attrs to it's previous size/content
self.collector.global_crate_attrs.truncate(old_len);
r
} else {
intravisit::walk_mod(self, m)
}
}
fn visit_item(&mut self, item: &'tcx hir::Item<'_>) {
let name = match &item.kind {
hir::ItemKind::Impl(impl_) => {
Some(rustc_hir_pretty::id_to_string(&self.tcx, impl_.self_ty.hir_id))
}
_ => item.kind.ident().map(|ident| ident.to_string()),
};
self.visit_testable(name, item.owner_id.def_id, item.span, |this| {
intravisit::walk_item(this, item);
});
}
fn visit_trait_item(&mut self, item: &'tcx hir::TraitItem<'_>) {
self.visit_testable(
Some(item.ident.to_string()),
item.owner_id.def_id,
item.span,
|this| {
intravisit::walk_trait_item(this, item);
},
);
}
fn visit_impl_item(&mut self, item: &'tcx hir::ImplItem<'_>) {
self.visit_testable(
Some(item.ident.to_string()),
item.owner_id.def_id,
item.span,
|this| {
intravisit::walk_impl_item(this, item);
},
);
}
fn visit_foreign_item(&mut self, item: &'tcx hir::ForeignItem<'_>) {
self.visit_testable(
Some(item.ident.to_string()),
item.owner_id.def_id,
item.span,
|this| {
intravisit::walk_foreign_item(this, item);
},
);
}
fn visit_variant(&mut self, v: &'tcx hir::Variant<'_>) {
self.visit_testable(Some(v.ident.to_string()), v.def_id, v.span, |this| {
intravisit::walk_variant(this, v);
});
}
fn visit_field_def(&mut self, f: &'tcx hir::FieldDef<'_>) {
self.visit_testable(Some(f.ident.to_string()), f.def_id, f.span, |this| {
intravisit::walk_field_def(this, f);
});
}
}