blob: 60977a036767e734aa265d9cda8a846789a4c147 [file] [log] [blame]
Lukasz Anforowicz0bef2642023-01-05 09:20:31 -08001// Part of the Crubit project, under the Apache License v2.0 with LLVM
2// Exceptions. See /LICENSE for license information.
3// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
4
5//! The `run_compiler` crate mostly wraps and simplifies a subset of APIs
6//! from the `rustc_driver` module, providing an easy way to `run_compiler`.
7
8use anyhow::anyhow;
9use either::Either;
10use rustc_interface::interface::Compiler;
11use rustc_interface::Queries;
12use rustc_middle::ty::TyCtxt; // See also <internal link>/ty.html#import-conventions
13
14/// Wrapper around `rustc_driver::RunCompiler::run` that exposes a
15/// simplified API:
16/// - Takes a `callback` that will be invoked from within Rust compiler, after
17/// parsing and analysis are done,
18/// - Compilation will stop after parsing, analysis, and the `callback` are
19/// done,
20/// - Returns the combined results from the Rust compiler *and* the `callback`.
21pub fn run_compiler<F, R>(rustc_args: &[String], callback: F) -> anyhow::Result<R>
22where
23 F: FnOnce(TyCtxt) -> anyhow::Result<R> + Send,
24 R: Send,
25{
26 AfterAnalysisCallback::new(rustc_args, callback).run()
27}
28
29struct AfterAnalysisCallback<'a, F, R>
30where
31 F: FnOnce(TyCtxt) -> anyhow::Result<R> + Send,
32 R: Send,
33{
34 args: &'a [String],
35 callback_or_result: Either<F, anyhow::Result<R>>,
36}
37
38impl<'a, F, R> AfterAnalysisCallback<'a, F, R>
39where
40 F: FnOnce(TyCtxt) -> anyhow::Result<R> + Send,
41 R: Send,
42{
43 fn new(args: &'a [String], callback: F) -> Self {
44 Self { args, callback_or_result: Either::Left(callback) }
45 }
46
47 /// Runs Rust compiler, and then invokes the stored callback (with
48 /// `TyCtxt` of the parsed+analyzed Rust crate as the callback's
49 /// argument), and then finally returns the combined results
50 /// from Rust compiler *and* the callback.
51 fn run(mut self) -> anyhow::Result<R> {
52 // Rust compiler unwinds with a special sentinel value to abort compilation on
53 // fatal errors. We use `catch_fatal_errors` to 1) catch such panics and
54 // translate them into a Result, and 2) resume and propagate other panics.
55 use rustc_interface::interface::Result;
56 let rustc_result: Result<Result<()>> = rustc_driver::catch_fatal_errors(|| {
57 rustc_driver::RunCompiler::new(self.args, &mut self).run()
58 });
59
60 // Flatten `Result<Result<T, ...>>` into `Result<T, ...>` (i.e. combine the
61 // result from `RunCompiler::run` and `catch_fatal_errors`).
62 //
63 // TODO(lukasza): Use `Result::flatten` API when it gets stabilized. See also
64 // https://github.com/rust-lang/rust/issues/70142
65 let rustc_result: Result<()> = rustc_result.and_then(|result| result);
66
67 // Translate `rustc_interface::interface::Result` into `anyhow::Result`. (Can't
68 // use `?` because the trait `std::error::Error` is not implemented for
69 // `ErrorGuaranteed` which is required by the impl of
70 // `From<ErrorGuaranteed>` for `anyhow::Error`.)
71 let rustc_result: anyhow::Result<()> = rustc_result.map_err(|_err| {
72 // We can ignore `_err` because it has no payload / because this type has only
73 // one valid/possible value.
74 anyhow!("Errors reported by Rust compiler.")
75 });
76
77 // Return either `rustc_result` or `self.callback_result` or a new error.
78 rustc_result.and_then(|()| {
79 self.callback_or_result.right_or_else(|_left| {
80 // When rustc cmdline arguments (i.e. `self.args`) are empty (or contain
81 // `--help`) then the `after_analysis` callback won't be invoked. Handle
82 // this case by emitting an explicit error at the Crubit level.
83 Err(anyhow!("The Rust compiler had no crate to compile and analyze"))
84 })
85 })
86 }
87}
88
89impl<'a, F, R> rustc_driver::Callbacks for AfterAnalysisCallback<'a, F, R>
90where
91 F: FnOnce(TyCtxt) -> anyhow::Result<R> + Send,
92 R: Send,
93{
94 fn after_analysis<'tcx>(
95 &mut self,
96 _compiler: &Compiler,
97 queries: &'tcx Queries<'tcx>,
98 ) -> rustc_driver::Compilation {
99 let rustc_result = enter_tcx(queries, |tcx| {
100 let callback = {
101 let temporary_placeholder = Either::Right(Err(anyhow::anyhow!("unused")));
102 std::mem::replace(&mut self.callback_or_result, temporary_placeholder)
103 .left_or_else(|_| panic!("`after_analysis` should only run once"))
104 };
105 self.callback_or_result = Either::Right(callback(tcx));
106 });
107
108 // `expect`ing no errors in `rustc_result`, because `after_analysis` is only
109 // called by `rustc_driver` if earlier compiler analysis was successful
110 // (which as the *last* compilation phase presumably covers *all*
111 // errors).
112 rustc_result.expect("Expecting no compile errors inside `after_analysis` callback.");
113
114 rustc_driver::Compilation::Stop
115 }
116}
117
118/// Helper (used by `run_compiler` and `run_compiler_for_testing`) for invoking
119/// functions operating on `TyCtxt`.
120fn enter_tcx<'tcx, F, T>(
121 queries: &'tcx Queries<'tcx>,
122 f: F,
123) -> rustc_interface::interface::Result<T>
124where
125 F: FnOnce(TyCtxt<'tcx>) -> T + Send,
126 T: Send,
127{
128 let query_context = queries.global_ctxt()?;
129 Ok(query_context.peek_mut().enter(f))
130}
131
132#[cfg(test)]
133pub mod tests {
134 use super::run_compiler;
135 use rustc_middle::ty::TyCtxt; // See also <internal link>/ty.html#import-conventions
136 use std::path::PathBuf;
137 use tempfile::tempdir;
138
139 const DEFAULT_RUST_SOURCE_FOR_TESTING: &'static str = r#" pub mod public_module {
140 pub fn public_function() {
141 private_function()
142 }
143
144 fn private_function() {}
145 }
146 "#;
147
148 #[test]
149 fn test_run_compiler_rustc_error_propagation() -> anyhow::Result<()> {
150 let rustc_args = vec![
151 "run_compiler_unittest_executable".to_string(),
152 "--unrecognized-rustc-flag".to_string(),
153 ];
154 let err = run_compiler(&rustc_args, |_tcx| Ok(()))
155 .expect_err("--unrecognized-rustc-flag should trigger an error");
156
157 let msg = format!("{err:#}");
158 assert_eq!("Errors reported by Rust compiler.", msg);
159 Ok(())
160 }
161
162 /// `test_run_compiler_empty_args` tests that we gracefully handle scenarios
163 /// where `rustc` doesn't compile anything (e.g. when there are no
164 /// cmdline args).
165 #[test]
166 fn test_run_compiler_no_args_except_argv0() -> anyhow::Result<()> {
167 let rustc_args = vec!["run_compiler_unittest_executable".to_string()];
168 let err = run_compiler(&rustc_args, |_tcx| Ok(()))
169 .expect_err("Empty `rustc_args` should trigger an error");
170
171 let msg = format!("{err:#}");
172 assert_eq!("The Rust compiler had no crate to compile and analyze", msg);
173 Ok(())
174 }
175
176 /// `test_run_compiler_help` tests that we gracefully handle scenarios where
177 /// `rustc` doesn't compile anything (e.g. when passing `--help`).
178 #[test]
179 fn test_run_compiler_help() -> anyhow::Result<()> {
180 let rustc_args = vec!["run_compiler_unittest_executable".to_string(), "--help".to_string()];
181 let err = run_compiler(&rustc_args, |_tcx| Ok(()))
182 .expect_err("--help passed to rustc should trigger an error");
183
184 let msg = format!("{err:#}");
185 assert_eq!("The Rust compiler had no crate to compile and analyze", msg);
186 Ok(())
187 }
188
189 /// `test_run_compiler_no_output_file` tests that we stop the compilation
190 /// midway (i.e. that we return `Stop` from `after_analysis`).
191 #[test]
192 fn test_run_compiler_no_output_file() -> anyhow::Result<()> {
193 let tmpdir = tempdir()?;
194
195 let rs_path = tmpdir.path().join("input_crate.rs");
196 std::fs::write(&rs_path, DEFAULT_RUST_SOURCE_FOR_TESTING)?;
197
198 let out_path = tmpdir.path().join("unexpected_output.o");
199
200 let rustc_args = vec![
201 // Default parameters.
202 "run_compiler_unittest_executable".to_string(),
203 "--crate-type=lib".to_string(),
204 format!("--sysroot={}", get_sysroot_for_testing().display()),
205 rs_path.display().to_string(),
206 // Test-specific parameter: asking for after-analysis output
207 "-o".to_string(),
208 out_path.display().to_string(),
209 ];
210
211 run_compiler(&rustc_args, |_tcx| Ok(()))?;
212
213 // Verify that compilation didn't continue after the initial analysis.
214 assert!(!out_path.exists());
215 Ok(())
216 }
217
218 /// Returns the `rustc` sysroot that is suitable for the environment where unit
219 /// tests run.
220 ///
221 /// The sysroot is used internally by `run_compiler_for_testing`, but it may
222 /// also be passed as `--sysroot=...` in `rustc_args` argument of `run_compiler`
223 pub fn get_sysroot_for_testing() -> PathBuf {
224 let runfiles = runfiles::Runfiles::create().unwrap();
225 runfiles.rlocation(if std::env::var("LEGACY_TOOLCHAIN_RUST_TEST").is_ok() {
226 "google3/third_party/unsupported_toolchains/rust/toolchains/nightly"
227 } else {
228 "google3/nowhere/llvm/rust"
229 })
230 }
231
232 #[test]
233 #[should_panic(expected = "Test inputs shouldn't cause compilation errors")]
234 fn test_run_compiler_for_testing_panic_when_test_input_contains_syntax_errors() {
235 run_compiler_for_testing("syntax error here", |_tcx| panic!("This part shouldn't execute"))
236 }
237
238 #[test]
239 #[should_panic(expected = "Test inputs shouldn't cause compilation errors")]
240 fn test_run_compiler_for_testing_panic_when_test_input_triggers_analysis_errors() {
241 run_compiler_for_testing("#![feature(no_such_feature)]", |_tcx| {
242 panic!("This part shouldn't execute")
243 })
244 }
245
246 #[test]
247 #[should_panic(expected = "Test inputs shouldn't cause compilation errors")]
248 fn test_run_compiler_for_testing_panic_when_test_input_triggers_warnings() {
249 run_compiler_for_testing("pub fn foo(unused_parameter: i32) {}", |_tcx| {
250 panic!("This part shouldn't execute")
251 })
252 }
253
254 #[test]
255 fn test_run_compiler_for_testing_nightly_features_ok_in_test_input() {
256 // This test arbitrarily picks `yeet_expr` as an example of a feature that
257 // hasn't yet been stabilized.
258 let test_src = r#"
259 // This test is supposed to test that *nightly* features are ok
260 // in the test input. The `forbid` directive below helps to
261 // ensure that we'll realize in the future when the `yeet_expr`
262 // feature gets stabilized, making it not quite fitting for use
263 // in this test.
264 #![forbid(stable_features)]
265
266 #![feature(yeet_expr)]
267 "#;
268 run_compiler_for_testing(test_src, |_tcx| ())
269 }
270
271 #[test]
272 fn test_run_compiler_for_testing_stabilized_features_ok_in_test_input() {
273 // This test arbitrarily picks `const_ptr_offset_from` as an example of a
274 // feature that has been already stabilized.
275 run_compiler_for_testing("#![feature(const_ptr_offset_from)]", |_tcx| ())
276 }
277
278 /// `run_compiler_for_testing` is similar to `run_compiler`: it invokes the
279 /// `callback` after parsing and analysis are done, but instead of taking
280 /// `rustc_args` it:
281 ///
282 /// * Invokes the Rust compiler on the given Rust `source`
283 /// * Hardcodes other compiler flags (e.g. picks Rust 2021 edition, and opts
284 /// into treating all warnings as errors).
285 pub fn run_compiler_for_testing<F, T>(source: impl Into<String>, callback: F) -> T
286 where
287 F: for<'tcx> FnOnce(TyCtxt<'tcx>) -> T + Send,
288 T: Send,
289 {
290 use rustc_session::config::{
291 CodegenOptions, CrateType, Input, Options, OutputType, OutputTypes,
292 };
293
294 const TEST_FILENAME: &str = "crubit_unittests.rs";
295
296 // Setting `output_types` that will trigger code gen - otherwise some parts of
297 // the analysis will be missing (e.g. `tcx.exported_symbols()`).
298 // The choice of `Bitcode` is somewhat arbitrary (e.g. `Assembly`,
299 // `Mir`, etc. would also trigger code gen).
300 let output_types = OutputTypes::new(&[(OutputType::Bitcode, None /* PathBuf */)]);
301
302 let opts = Options {
303 crate_types: vec![CrateType::Rlib], // Test inputs simulate library crates.
304 maybe_sysroot: Some(get_sysroot_for_testing()),
305 output_types,
306 edition: rustc_span::edition::Edition::Edition2021,
307 unstable_features: rustc_feature::UnstableFeatures::Allow,
308 lint_opts: vec![
309 ("warnings".to_string(), rustc_lint_defs::Level::Deny),
310 ("stable_features".to_string(), rustc_lint_defs::Level::Allow),
311 ],
312 cg: CodegenOptions {
313 // As pointed out in `panics_and_exceptions.md` the tool only supports `-C
314 // panic=abort` and therefore we explicitly opt into this config for tests.
315 panic: Some(rustc_target::spec::PanicStrategy::Abort),
316 ..Default::default()
317 },
318 ..Default::default()
319 };
320
321 let config = rustc_interface::interface::Config {
322 opts,
323 crate_cfg: Default::default(),
324 crate_check_cfg: Default::default(),
325 input: Input::Str {
326 name: rustc_span::FileName::Custom(TEST_FILENAME.to_string()),
327 input: source.into(),
328 },
329 input_path: None,
330 output_file: None,
331 output_dir: None,
332 file_loader: None,
333 lint_caps: Default::default(),
334 parse_sess_created: None,
335 register_lints: None,
336 override_queries: None,
337 make_codegen_backend: None,
338 registry: rustc_errors::registry::Registry::new(rustc_error_codes::DIAGNOSTICS),
339 };
340
341 rustc_interface::interface::run_compiler(config, |compiler| {
342 compiler.enter(|queries| {
343 use rustc_interface::interface::Result;
344 let result: Result<Result<()>> = super::enter_tcx(queries, |tcx| {
345 // Explicitly force full `analysis` stage to detect compilation
346 // errors that the earlier stages might miss. This helps ensure that the
347 // test inputs are valid Rust (even if `callback` wouldn't
348 // have triggered full analysis).
349 tcx.analysis(())
350 });
351
352 // Flatten the outer and inner results into a single result. (outer result
353 // comes from `enter_tcx`; inner result comes from `analysis`).
354 //
355 // TODO(lukasza): Use `Result::flatten` API when it gets stabilized. See also
356 // https://github.com/rust-lang/rust/issues/70142
357 let result: Result<()> = result.and_then(|result| result);
358
359 // `analysis` might succeed even if there are some lint / warning errors.
360 // Detecting these requires explicitly checking `compile_status`.
361 let result: Result<()> = result.and_then(|()| compiler.session().compile_status());
362
363 // Run the provided callback.
364 let result: Result<T> = result.and_then(|()| super::enter_tcx(queries, callback));
365 result.expect("Test inputs shouldn't cause compilation errors")
366 })
367 })
368 }
369}