blob: f229f77c97846f9f3792bef76d47156cb9b41df0 [file] [log] [blame]
//! Logic for transforming the raw code given by the user into something actually
//! runnable, e.g. by adding a `main` function if it doesn't already exist.
use std::fmt::{self, Write as _};
use std::io;
use std::sync::Arc;
use rustc_ast::token::{Delimiter, TokenKind};
use rustc_ast::tokenstream::TokenTree;
use rustc_ast::{self as ast, AttrStyle, HasAttrs, StmtKind};
use rustc_errors::emitter::stderr_destination;
use rustc_errors::{ColorConfig, DiagCtxtHandle};
use rustc_parse::new_parser_from_source_str;
use rustc_session::parse::ParseSess;
use rustc_span::edition::{DEFAULT_EDITION, Edition};
use rustc_span::source_map::SourceMap;
use rustc_span::symbol::sym;
use rustc_span::{DUMMY_SP, FileName, Span, kw};
use tracing::debug;
use super::GlobalTestOptions;
use crate::display::Joined as _;
use crate::html::markdown::LangString;
#[derive(Default)]
struct ParseSourceInfo {
has_main_fn: bool,
already_has_extern_crate: bool,
supports_color: bool,
has_global_allocator: bool,
has_macro_def: bool,
everything_else: String,
crates: String,
crate_attrs: String,
maybe_crate_attrs: String,
}
/// Builder type for `DocTestBuilder`.
pub(crate) struct BuildDocTestBuilder<'a> {
source: &'a str,
crate_name: Option<&'a str>,
edition: Edition,
can_merge_doctests: bool,
// If `test_id` is `None`, it means we're generating code for a code example "run" link.
test_id: Option<String>,
lang_str: Option<&'a LangString>,
span: Span,
global_crate_attrs: Vec<String>,
}
impl<'a> BuildDocTestBuilder<'a> {
pub(crate) fn new(source: &'a str) -> Self {
Self {
source,
crate_name: None,
edition: DEFAULT_EDITION,
can_merge_doctests: false,
test_id: None,
lang_str: None,
span: DUMMY_SP,
global_crate_attrs: Vec::new(),
}
}
#[inline]
pub(crate) fn crate_name(mut self, crate_name: &'a str) -> Self {
self.crate_name = Some(crate_name);
self
}
#[inline]
pub(crate) fn can_merge_doctests(mut self, can_merge_doctests: bool) -> Self {
self.can_merge_doctests = can_merge_doctests;
self
}
#[inline]
pub(crate) fn test_id(mut self, test_id: String) -> Self {
self.test_id = Some(test_id);
self
}
#[inline]
pub(crate) fn lang_str(mut self, lang_str: &'a LangString) -> Self {
self.lang_str = Some(lang_str);
self
}
#[inline]
pub(crate) fn span(mut self, span: Span) -> Self {
self.span = span;
self
}
#[inline]
pub(crate) fn edition(mut self, edition: Edition) -> Self {
self.edition = edition;
self
}
#[inline]
pub(crate) fn global_crate_attrs(mut self, global_crate_attrs: Vec<String>) -> Self {
self.global_crate_attrs = global_crate_attrs;
self
}
pub(crate) fn build(self, dcx: Option<DiagCtxtHandle<'_>>) -> DocTestBuilder {
let BuildDocTestBuilder {
source,
crate_name,
edition,
can_merge_doctests,
// If `test_id` is `None`, it means we're generating code for a code example "run" link.
test_id,
lang_str,
span,
global_crate_attrs,
} = self;
let can_merge_doctests = can_merge_doctests
&& lang_str.is_some_and(|lang_str| {
!lang_str.compile_fail && !lang_str.test_harness && !lang_str.standalone_crate
});
let result = rustc_driver::catch_fatal_errors(|| {
rustc_span::create_session_if_not_set_then(edition, |_| {
parse_source(source, &crate_name, dcx, span)
})
});
let Ok(Ok(ParseSourceInfo {
has_main_fn,
already_has_extern_crate,
supports_color,
has_global_allocator,
has_macro_def,
everything_else,
crates,
crate_attrs,
maybe_crate_attrs,
})) = result
else {
// If the AST returned an error, we don't want this doctest to be merged with the
// others.
return DocTestBuilder::invalid(
Vec::new(),
String::new(),
String::new(),
String::new(),
source.to_string(),
test_id,
);
};
debug!("crate_attrs:\n{crate_attrs}{maybe_crate_attrs}");
debug!("crates:\n{crates}");
debug!("after:\n{everything_else}");
// If it contains `#[feature]` or `#[no_std]`, we don't want it to be merged either.
let can_be_merged = can_merge_doctests
&& !has_global_allocator
&& crate_attrs.is_empty()
// If this is a merged doctest and a defined macro uses `$crate`, then the path will
// not work, so better not put it into merged doctests.
&& !(has_macro_def && everything_else.contains("$crate"));
DocTestBuilder {
supports_color,
has_main_fn,
global_crate_attrs,
crate_attrs,
maybe_crate_attrs,
crates,
everything_else,
already_has_extern_crate,
test_id,
invalid_ast: false,
can_be_merged,
}
}
}
/// This struct contains information about the doctest itself which is then used to generate
/// doctest source code appropriately.
pub(crate) struct DocTestBuilder {
pub(crate) supports_color: bool,
pub(crate) already_has_extern_crate: bool,
pub(crate) has_main_fn: bool,
pub(crate) global_crate_attrs: Vec<String>,
pub(crate) crate_attrs: String,
/// If this is a merged doctest, it will be put into `everything_else`, otherwise it will
/// put into `crate_attrs`.
pub(crate) maybe_crate_attrs: String,
pub(crate) crates: String,
pub(crate) everything_else: String,
pub(crate) test_id: Option<String>,
pub(crate) invalid_ast: bool,
pub(crate) can_be_merged: bool,
}
/// Contains needed information for doctest to be correctly generated with expected "wrapping".
pub(crate) struct WrapperInfo {
pub(crate) before: String,
pub(crate) after: String,
pub(crate) returns_result: bool,
insert_indent_space: bool,
}
impl WrapperInfo {
fn len(&self) -> usize {
self.before.len() + self.after.len()
}
}
/// Contains a doctest information. Can be converted into code with the `to_string()` method.
pub(crate) enum DocTestWrapResult {
Valid {
crate_level_code: String,
/// This field can be `None` if one of the following conditions is true:
///
/// * The doctest's codeblock has the `test_harness` attribute.
/// * The doctest has a `main` function.
/// * The doctest has the `![no_std]` attribute.
wrapper: Option<WrapperInfo>,
/// Contains the doctest processed code without the wrappers (which are stored in the
/// `wrapper` field).
code: String,
},
/// Contains the original source code.
SyntaxError(String),
}
impl std::string::ToString for DocTestWrapResult {
fn to_string(&self) -> String {
match self {
Self::SyntaxError(s) => s.clone(),
Self::Valid { crate_level_code, wrapper, code } => {
let mut prog_len = code.len() + crate_level_code.len();
if let Some(wrapper) = wrapper {
prog_len += wrapper.len();
if wrapper.insert_indent_space {
prog_len += code.lines().count() * 4;
}
}
let mut prog = String::with_capacity(prog_len);
prog.push_str(crate_level_code);
if let Some(wrapper) = wrapper {
prog.push_str(&wrapper.before);
// add extra 4 spaces for each line to offset the code block
if wrapper.insert_indent_space {
write!(
prog,
"{}",
fmt::from_fn(|f| code
.lines()
.map(|line| fmt::from_fn(move |f| write!(f, " {line}")))
.joined("\n", f))
)
.unwrap();
} else {
prog.push_str(code);
}
prog.push_str(&wrapper.after);
} else {
prog.push_str(code);
}
prog
}
}
}
}
impl DocTestBuilder {
fn invalid(
global_crate_attrs: Vec<String>,
crate_attrs: String,
maybe_crate_attrs: String,
crates: String,
everything_else: String,
test_id: Option<String>,
) -> Self {
Self {
supports_color: false,
has_main_fn: false,
global_crate_attrs,
crate_attrs,
maybe_crate_attrs,
crates,
everything_else,
already_has_extern_crate: false,
test_id,
invalid_ast: true,
can_be_merged: false,
}
}
/// Transforms a test into code that can be compiled into a Rust binary, and returns the number of
/// lines before the test code begins.
pub(crate) fn generate_unique_doctest(
&self,
test_code: &str,
dont_insert_main: bool,
opts: &GlobalTestOptions,
crate_name: Option<&str>,
) -> (DocTestWrapResult, usize) {
if self.invalid_ast {
// If the AST failed to compile, no need to go generate a complete doctest, the error
// will be better this way.
debug!("invalid AST:\n{test_code}");
return (DocTestWrapResult::SyntaxError(test_code.to_string()), 0);
}
let mut line_offset = 0;
let mut crate_level_code = String::new();
let processed_code = self.everything_else.trim();
if self.global_crate_attrs.is_empty() {
// If there aren't any attributes supplied by #![doc(test(attr(...)))], then allow some
// lints that are commonly triggered in doctests. The crate-level test attributes are
// commonly used to make tests fail in case they trigger warnings, so having this there in
// that case may cause some tests to pass when they shouldn't have.
crate_level_code.push_str("#![allow(unused)]\n");
line_offset += 1;
}
// Next, any attributes that came from #![doc(test(attr(...)))].
for attr in &self.global_crate_attrs {
crate_level_code.push_str(&format!("#![{attr}]\n"));
line_offset += 1;
}
// Now push any outer attributes from the example, assuming they
// are intended to be crate attributes.
if !self.crate_attrs.is_empty() {
crate_level_code.push_str(&self.crate_attrs);
if !self.crate_attrs.ends_with('\n') {
crate_level_code.push('\n');
}
}
if !self.maybe_crate_attrs.is_empty() {
crate_level_code.push_str(&self.maybe_crate_attrs);
if !self.maybe_crate_attrs.ends_with('\n') {
crate_level_code.push('\n');
}
}
if !self.crates.is_empty() {
crate_level_code.push_str(&self.crates);
if !self.crates.ends_with('\n') {
crate_level_code.push('\n');
}
}
// Don't inject `extern crate std` because it's already injected by the
// compiler.
if !self.already_has_extern_crate &&
!opts.no_crate_inject &&
let Some(crate_name) = crate_name &&
crate_name != "std" &&
// Don't inject `extern crate` if the crate is never used.
// NOTE: this is terribly inaccurate because it doesn't actually
// parse the source, but only has false positives, not false
// negatives.
test_code.contains(crate_name)
{
// rustdoc implicitly inserts an `extern crate` item for the own crate
// which may be unused, so we need to allow the lint.
crate_level_code.push_str("#[allow(unused_extern_crates)]\n");
crate_level_code.push_str(&format!("extern crate r#{crate_name};\n"));
line_offset += 1;
}
// FIXME: This code cannot yet handle no_std test cases yet
let wrapper = if dont_insert_main
|| self.has_main_fn
|| crate_level_code.contains("![no_std]")
{
None
} else {
let returns_result = processed_code.ends_with("(())");
// Give each doctest main function a unique name.
// This is for example needed for the tooling around `-C instrument-coverage`.
let inner_fn_name = if let Some(ref test_id) = self.test_id {
format!("_doctest_main_{test_id}")
} else {
"_inner".into()
};
let inner_attr = if self.test_id.is_some() { "#[allow(non_snake_case)] " } else { "" };
let (main_pre, main_post) = if returns_result {
(
format!(
"fn main() {{ {inner_attr}fn {inner_fn_name}() -> core::result::Result<(), impl core::fmt::Debug> {{\n",
),
format!("\n}} {inner_fn_name}().unwrap() }}"),
)
} else if self.test_id.is_some() {
(
format!("fn main() {{ {inner_attr}fn {inner_fn_name}() {{\n",),
format!("\n}} {inner_fn_name}() }}"),
)
} else {
("fn main() {\n".into(), "\n}".into())
};
// Note on newlines: We insert a line/newline *before*, and *after*
// the doctest and adjust the `line_offset` accordingly.
// In the case of `-C instrument-coverage`, this means that the generated
// inner `main` function spans from the doctest opening codeblock to the
// closing one. For example
// /// ``` <- start of the inner main
// /// <- code under doctest
// /// ``` <- end of the inner main
line_offset += 1;
Some(WrapperInfo {
before: main_pre,
after: main_post,
returns_result,
insert_indent_space: opts.insert_indent_space,
})
};
(
DocTestWrapResult::Valid {
code: processed_code.to_string(),
wrapper,
crate_level_code,
},
line_offset,
)
}
}
fn reset_error_count(psess: &ParseSess) {
// Reset errors so that they won't be reported as compiler bugs when dropping the
// dcx. Any errors in the tests will be reported when the test file is compiled,
// Note that we still need to cancel the errors above otherwise `Diag` will panic on
// drop.
psess.dcx().reset_err_count();
}
const DOCTEST_CODE_WRAPPER: &str = "fn f(){";
fn parse_source(
source: &str,
crate_name: &Option<&str>,
parent_dcx: Option<DiagCtxtHandle<'_>>,
span: Span,
) -> Result<ParseSourceInfo, ()> {
use rustc_errors::DiagCtxt;
use rustc_errors::emitter::{Emitter, HumanEmitter};
use rustc_span::source_map::FilePathMapping;
let mut info =
ParseSourceInfo { already_has_extern_crate: crate_name.is_none(), ..Default::default() };
let wrapped_source = format!("{DOCTEST_CODE_WRAPPER}{source}\n}}");
let filename = FileName::anon_source_code(&wrapped_source);
let sm = Arc::new(SourceMap::new(FilePathMapping::empty()));
let translator = rustc_driver::default_translator();
info.supports_color =
HumanEmitter::new(stderr_destination(ColorConfig::Auto), translator.clone())
.supports_color();
// Any errors in parsing should also appear when the doctest is compiled for real, so just
// send all the errors that the parser emits directly into a `Sink` instead of stderr.
let emitter = HumanEmitter::new(Box::new(io::sink()), translator);
// FIXME(misdreavus): pass `-Z treat-err-as-bug` to the doctest parser
let dcx = DiagCtxt::new(Box::new(emitter)).disable_warnings();
let psess = ParseSess::with_dcx(dcx, sm);
let mut parser = match new_parser_from_source_str(&psess, filename, wrapped_source) {
Ok(p) => p,
Err(errs) => {
errs.into_iter().for_each(|err| err.cancel());
reset_error_count(&psess);
return Err(());
}
};
fn push_to_s(s: &mut String, source: &str, span: rustc_span::Span, prev_span_hi: &mut usize) {
let extra_len = DOCTEST_CODE_WRAPPER.len();
// We need to shift by the length of `DOCTEST_CODE_WRAPPER` because we
// added it at the beginning of the source we provided to the parser.
let mut hi = span.hi().0 as usize - extra_len;
if hi > source.len() {
hi = source.len();
}
s.push_str(&source[*prev_span_hi..hi]);
*prev_span_hi = hi;
}
fn check_item(item: &ast::Item, info: &mut ParseSourceInfo, crate_name: &Option<&str>) -> bool {
let mut is_extern_crate = false;
if !info.has_global_allocator
&& item.attrs.iter().any(|attr| attr.has_name(sym::global_allocator))
{
info.has_global_allocator = true;
}
match item.kind {
ast::ItemKind::Fn(ref fn_item) if !info.has_main_fn => {
if fn_item.ident.name == sym::main {
info.has_main_fn = true;
}
}
ast::ItemKind::ExternCrate(original, ident) => {
is_extern_crate = true;
if !info.already_has_extern_crate
&& let Some(crate_name) = crate_name
{
info.already_has_extern_crate = match original {
Some(name) => name.as_str() == *crate_name,
None => ident.as_str() == *crate_name,
};
}
}
ast::ItemKind::MacroDef(..) => {
info.has_macro_def = true;
}
_ => {}
}
is_extern_crate
}
let mut prev_span_hi = 0;
let not_crate_attrs = &[sym::forbid, sym::allow, sym::warn, sym::deny, sym::expect];
let parsed = parser.parse_item(rustc_parse::parser::ForceCollect::No);
let result = match parsed {
Ok(Some(ref item))
if let ast::ItemKind::Fn(ref fn_item) = item.kind
&& let Some(ref body) = fn_item.body =>
{
for attr in &item.attrs {
if attr.style == AttrStyle::Outer || attr.has_any_name(not_crate_attrs) {
// There is one exception to these attributes:
// `#![allow(internal_features)]`. If this attribute is used, we need to
// consider it only as a crate-level attribute.
if attr.has_name(sym::allow)
&& let Some(list) = attr.meta_item_list()
&& list.iter().any(|sub_attr| sub_attr.has_name(sym::internal_features))
{
push_to_s(&mut info.crate_attrs, source, attr.span, &mut prev_span_hi);
} else {
push_to_s(
&mut info.maybe_crate_attrs,
source,
attr.span,
&mut prev_span_hi,
);
}
} else {
push_to_s(&mut info.crate_attrs, source, attr.span, &mut prev_span_hi);
}
}
let mut has_non_items = false;
for stmt in &body.stmts {
let mut is_extern_crate = false;
match stmt.kind {
StmtKind::Item(ref item) => {
is_extern_crate = check_item(item, &mut info, crate_name);
}
// We assume that the macro calls will expand to item(s) even though they could
// expand to statements and expressions.
StmtKind::MacCall(ref mac_call) => {
if !info.has_main_fn {
// For backward compatibility, we look for the token sequence `fn main(…)`
// in the macro input (!) to crudely detect main functions "masked by a
// wrapper macro". For the record, this is a horrible heuristic!
// See <https://github.com/rust-lang/rust/issues/56898>.
let mut iter = mac_call.mac.args.tokens.iter();
while let Some(token) = iter.next() {
if let TokenTree::Token(token, _) = token
&& let TokenKind::Ident(kw::Fn, _) = token.kind
&& let Some(TokenTree::Token(ident, _)) = iter.peek()
&& let TokenKind::Ident(sym::main, _) = ident.kind
&& let Some(TokenTree::Delimited(.., Delimiter::Parenthesis, _)) = {
iter.next();
iter.peek()
}
{
info.has_main_fn = true;
break;
}
}
}
}
StmtKind::Expr(ref expr) => {
if matches!(expr.kind, ast::ExprKind::Err(_)) {
reset_error_count(&psess);
return Err(());
}
has_non_items = true;
}
StmtKind::Let(_) | StmtKind::Semi(_) | StmtKind::Empty => has_non_items = true,
}
// Weirdly enough, the `Stmt` span doesn't include its attributes, so we need to
// tweak the span to include the attributes as well.
let mut span = stmt.span;
if let Some(attr) =
stmt.kind.attrs().iter().find(|attr| attr.style == AttrStyle::Outer)
{
span = span.with_lo(attr.span.lo());
}
if info.everything_else.is_empty()
&& (!info.maybe_crate_attrs.is_empty() || !info.crate_attrs.is_empty())
{
// To keep the doctest code "as close as possible" to the original, we insert
// all the code located between this new span and the previous span which
// might contain code comments and backlines.
push_to_s(&mut info.crates, source, span.shrink_to_lo(), &mut prev_span_hi);
}
if !is_extern_crate {
push_to_s(&mut info.everything_else, source, span, &mut prev_span_hi);
} else {
push_to_s(&mut info.crates, source, span, &mut prev_span_hi);
}
}
if has_non_items {
if info.has_main_fn
&& let Some(dcx) = parent_dcx
&& !span.is_dummy()
{
dcx.span_warn(
span,
"the `main` function of this doctest won't be run as it contains \
expressions at the top level, meaning that the whole doctest code will be \
wrapped in a function",
);
}
info.has_main_fn = false;
}
Ok(info)
}
Err(e) => {
e.cancel();
Err(())
}
_ => Err(()),
};
reset_error_count(&psess);
result
}