Lukasz Anforowicz | 0bef264 | 2023-01-05 09:20:31 -0800 | [diff] [blame] | 1 | // 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 | |
| 8 | use anyhow::anyhow; |
| 9 | use either::Either; |
| 10 | use rustc_interface::interface::Compiler; |
| 11 | use rustc_interface::Queries; |
| 12 | use 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`. |
| 21 | pub fn run_compiler<F, R>(rustc_args: &[String], callback: F) -> anyhow::Result<R> |
| 22 | where |
| 23 | F: FnOnce(TyCtxt) -> anyhow::Result<R> + Send, |
| 24 | R: Send, |
| 25 | { |
| 26 | AfterAnalysisCallback::new(rustc_args, callback).run() |
| 27 | } |
| 28 | |
| 29 | struct AfterAnalysisCallback<'a, F, R> |
| 30 | where |
| 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 | |
| 38 | impl<'a, F, R> AfterAnalysisCallback<'a, F, R> |
| 39 | where |
| 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 | |
| 89 | impl<'a, F, R> rustc_driver::Callbacks for AfterAnalysisCallback<'a, F, R> |
| 90 | where |
| 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`. |
| 120 | fn enter_tcx<'tcx, F, T>( |
| 121 | queries: &'tcx Queries<'tcx>, |
| 122 | f: F, |
| 123 | ) -> rustc_interface::interface::Result<T> |
| 124 | where |
| 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)] |
| 133 | pub 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 | } |