Skip to content

Commit 815c447

Browse files
camelidGuillaumeGomez
authored andcommitted
Parse full doctest source; extract helper for parsing code
It doesn't really make sense to skip part of the source when we're parsing it, so parse the whole doctest. This simplifies things too.
1 parent d06a05e commit 815c447

File tree

1 file changed

+104
-90
lines changed

1 file changed

+104
-90
lines changed

Diff for: src/librustdoc/doctest/make.rs

+104-90
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use std::io;
66
use rustc_ast as ast;
77
use rustc_data_structures::sync::Lrc;
88
use rustc_errors::emitter::stderr_destination;
9-
use rustc_errors::ColorConfig;
9+
use rustc_errors::{ColorConfig, FatalError};
1010
use rustc_parse::new_parser_from_source_str;
1111
use rustc_parse::parser::attr::InnerAttrPolicy;
1212
use rustc_session::parse::ParseSess;
@@ -55,15 +55,103 @@ pub(crate) fn make_test(
5555

5656
// Uses librustc_ast to parse the doctest and find if there's a main fn and the extern
5757
// crate already is included.
58+
let Ok((already_has_main, already_has_extern_crate)) =
59+
check_for_main_and_extern_crate(crate_name, s.to_owned(), edition, &mut supports_color)
60+
else {
61+
// If the parser panicked due to a fatal error, pass the test code through unchanged.
62+
// The error will be reported during compilation.
63+
return (s.to_owned(), 0, false);
64+
};
65+
66+
// Don't inject `extern crate std` because it's already injected by the
67+
// compiler.
68+
if !already_has_extern_crate && !opts.no_crate_inject && crate_name != Some("std") {
69+
if let Some(crate_name) = crate_name {
70+
// Don't inject `extern crate` if the crate is never used.
71+
// NOTE: this is terribly inaccurate because it doesn't actually
72+
// parse the source, but only has false positives, not false
73+
// negatives.
74+
if s.contains(crate_name) {
75+
// rustdoc implicitly inserts an `extern crate` item for the own crate
76+
// which may be unused, so we need to allow the lint.
77+
prog.push_str("#[allow(unused_extern_crates)]\n");
78+
79+
prog.push_str(&format!("extern crate r#{crate_name};\n"));
80+
line_offset += 1;
81+
}
82+
}
83+
}
84+
85+
// FIXME: This code cannot yet handle no_std test cases yet
86+
if dont_insert_main || already_has_main || prog.contains("![no_std]") {
87+
prog.push_str(everything_else);
88+
} else {
89+
let returns_result = everything_else.trim_end().ends_with("(())");
90+
// Give each doctest main function a unique name.
91+
// This is for example needed for the tooling around `-C instrument-coverage`.
92+
let inner_fn_name = if let Some(test_id) = test_id {
93+
format!("_doctest_main_{test_id}")
94+
} else {
95+
"_inner".into()
96+
};
97+
let inner_attr = if test_id.is_some() { "#[allow(non_snake_case)] " } else { "" };
98+
let (main_pre, main_post) = if returns_result {
99+
(
100+
format!(
101+
"fn main() {{ {inner_attr}fn {inner_fn_name}() -> Result<(), impl core::fmt::Debug> {{\n",
102+
),
103+
format!("\n}} {inner_fn_name}().unwrap() }}"),
104+
)
105+
} else if test_id.is_some() {
106+
(
107+
format!("fn main() {{ {inner_attr}fn {inner_fn_name}() {{\n",),
108+
format!("\n}} {inner_fn_name}() }}"),
109+
)
110+
} else {
111+
("fn main() {\n".into(), "\n}".into())
112+
};
113+
// Note on newlines: We insert a line/newline *before*, and *after*
114+
// the doctest and adjust the `line_offset` accordingly.
115+
// In the case of `-C instrument-coverage`, this means that the generated
116+
// inner `main` function spans from the doctest opening codeblock to the
117+
// closing one. For example
118+
// /// ``` <- start of the inner main
119+
// /// <- code under doctest
120+
// /// ``` <- end of the inner main
121+
line_offset += 1;
122+
123+
// add extra 4 spaces for each line to offset the code block
124+
let content = if opts.insert_indent_space {
125+
everything_else
126+
.lines()
127+
.map(|line| format!(" {}", line))
128+
.collect::<Vec<String>>()
129+
.join("\n")
130+
} else {
131+
everything_else.to_string()
132+
};
133+
prog.extend([&main_pre, content.as_str(), &main_post].iter().cloned());
134+
}
135+
136+
debug!("final doctest:\n{prog}");
137+
138+
(prog, line_offset, supports_color)
139+
}
140+
141+
fn check_for_main_and_extern_crate(
142+
crate_name: Option<&str>,
143+
source: String,
144+
edition: Edition,
145+
supports_color: &mut bool,
146+
) -> Result<(bool, bool), FatalError> {
58147
let result = rustc_driver::catch_fatal_errors(|| {
59148
rustc_span::create_session_if_not_set_then(edition, |_| {
60149
use rustc_errors::emitter::{Emitter, HumanEmitter};
61150
use rustc_errors::DiagCtxt;
62151
use rustc_parse::parser::ForceCollect;
63152
use rustc_span::source_map::FilePathMapping;
64153

65-
let filename = FileName::anon_source_code(s);
66-
let source = crates + everything_else;
154+
let filename = FileName::anon_source_code(&source);
67155

68156
// Any errors in parsing should also appear when the doctest is compiled for real, so just
69157
// send all the errors that librustc_ast emits directly into a `Sink` instead of stderr.
@@ -72,7 +160,7 @@ pub(crate) fn make_test(
72160
rustc_driver::DEFAULT_LOCALE_RESOURCES.to_vec(),
73161
false,
74162
);
75-
supports_color =
163+
*supports_color =
76164
HumanEmitter::new(stderr_destination(ColorConfig::Auto), fallback_bundle.clone())
77165
.supports_color();
78166

@@ -86,13 +174,14 @@ pub(crate) fn make_test(
86174
let mut found_extern_crate = crate_name.is_none();
87175
let mut found_macro = false;
88176

89-
let mut parser = match new_parser_from_source_str(&psess, filename, source) {
90-
Ok(p) => p,
91-
Err(errs) => {
92-
errs.into_iter().for_each(|err| err.cancel());
93-
return (found_main, found_extern_crate, found_macro);
94-
}
95-
};
177+
let mut parser =
178+
match new_parser_from_source_str(&psess, filename, source.clone()) {
179+
Ok(p) => p,
180+
Err(errs) => {
181+
errs.into_iter().for_each(|err| err.cancel());
182+
return (found_main, found_extern_crate, found_macro);
183+
}
184+
};
96185

97186
loop {
98187
match parser.parse_item(ForceCollect::No) {
@@ -146,18 +235,15 @@ pub(crate) fn make_test(
146235
(found_main, found_extern_crate, found_macro)
147236
})
148237
});
149-
let Ok((already_has_main, already_has_extern_crate, found_macro)) = result else {
150-
// If the parser panicked due to a fatal error, pass the test code through unchanged.
151-
// The error will be reported during compilation.
152-
return (s.to_owned(), 0, false);
153-
};
238+
let (already_has_main, already_has_extern_crate, found_macro) = result?;
154239

155240
// If a doctest's `fn main` is being masked by a wrapper macro, the parsing loop above won't
156241
// see it. In that case, run the old text-based scan to see if they at least have a main
157242
// function written inside a macro invocation. See
158243
// https://github.com/rust-lang/rust/issues/56898
159244
let already_has_main = if found_macro && !already_has_main {
160-
s.lines()
245+
source
246+
.lines()
161247
.map(|line| {
162248
let comment = line.find("//");
163249
if let Some(comment_begins) = comment { &line[0..comment_begins] } else { line }
@@ -167,79 +253,7 @@ pub(crate) fn make_test(
167253
already_has_main
168254
};
169255

170-
// Don't inject `extern crate std` because it's already injected by the
171-
// compiler.
172-
if !already_has_extern_crate && !opts.no_crate_inject && crate_name != Some("std") {
173-
if let Some(crate_name) = crate_name {
174-
// Don't inject `extern crate` if the crate is never used.
175-
// NOTE: this is terribly inaccurate because it doesn't actually
176-
// parse the source, but only has false positives, not false
177-
// negatives.
178-
if s.contains(crate_name) {
179-
// rustdoc implicitly inserts an `extern crate` item for the own crate
180-
// which may be unused, so we need to allow the lint.
181-
prog.push_str("#[allow(unused_extern_crates)]\n");
182-
183-
prog.push_str(&format!("extern crate r#{crate_name};\n"));
184-
line_offset += 1;
185-
}
186-
}
187-
}
188-
189-
// FIXME: This code cannot yet handle no_std test cases yet
190-
if dont_insert_main || already_has_main || prog.contains("![no_std]") {
191-
prog.push_str(everything_else);
192-
} else {
193-
let returns_result = everything_else.trim_end().ends_with("(())");
194-
// Give each doctest main function a unique name.
195-
// This is for example needed for the tooling around `-C instrument-coverage`.
196-
let inner_fn_name = if let Some(test_id) = test_id {
197-
format!("_doctest_main_{test_id}")
198-
} else {
199-
"_inner".into()
200-
};
201-
let inner_attr = if test_id.is_some() { "#[allow(non_snake_case)] " } else { "" };
202-
let (main_pre, main_post) = if returns_result {
203-
(
204-
format!(
205-
"fn main() {{ {inner_attr}fn {inner_fn_name}() -> Result<(), impl core::fmt::Debug> {{\n",
206-
),
207-
format!("\n}} {inner_fn_name}().unwrap() }}"),
208-
)
209-
} else if test_id.is_some() {
210-
(
211-
format!("fn main() {{ {inner_attr}fn {inner_fn_name}() {{\n",),
212-
format!("\n}} {inner_fn_name}() }}"),
213-
)
214-
} else {
215-
("fn main() {\n".into(), "\n}".into())
216-
};
217-
// Note on newlines: We insert a line/newline *before*, and *after*
218-
// the doctest and adjust the `line_offset` accordingly.
219-
// In the case of `-C instrument-coverage`, this means that the generated
220-
// inner `main` function spans from the doctest opening codeblock to the
221-
// closing one. For example
222-
// /// ``` <- start of the inner main
223-
// /// <- code under doctest
224-
// /// ``` <- end of the inner main
225-
line_offset += 1;
226-
227-
// add extra 4 spaces for each line to offset the code block
228-
let content = if opts.insert_indent_space {
229-
everything_else
230-
.lines()
231-
.map(|line| format!(" {}", line))
232-
.collect::<Vec<String>>()
233-
.join("\n")
234-
} else {
235-
everything_else.to_string()
236-
};
237-
prog.extend([&main_pre, content.as_str(), &main_post].iter().cloned());
238-
}
239-
240-
debug!("final doctest:\n{prog}");
241-
242-
(prog, line_offset, supports_color)
256+
Ok((already_has_main, already_has_extern_crate))
243257
}
244258

245259
fn check_if_attr_is_complete(source: &str, edition: Edition) -> bool {

0 commit comments

Comments
 (0)