| 1 | //! All the logic needed to run rustfix on a test that failed compilation |
| 2 | |
| 3 | use super::Flag; |
| 4 | use crate::{ |
| 5 | build_manager::BuildManager, |
| 6 | display, |
| 7 | parser::OptWithLine, |
| 8 | per_test_config::{Comments, Revisioned, TestConfig}, |
| 9 | Error, Errored, TestOk, |
| 10 | }; |
| 11 | use rustfix::{CodeFix, Filter, Suggestion}; |
| 12 | use spanned::{Span, Spanned}; |
| 13 | use std::{ |
| 14 | collections::HashSet, |
| 15 | path::{Path, PathBuf}, |
| 16 | process::Output, |
| 17 | sync::Arc, |
| 18 | }; |
| 19 | |
| 20 | /// When to run rustfix on tests |
| 21 | #[derive (Copy, Clone, Debug, PartialEq, Eq)] |
| 22 | pub enum RustfixMode { |
| 23 | /// Do not run rustfix on the test |
| 24 | Disabled, |
| 25 | /// Apply only `MachineApplicable` suggestions emitted by the test |
| 26 | MachineApplicable, |
| 27 | /// Apply all suggestions emitted by the test |
| 28 | Everything, |
| 29 | } |
| 30 | |
| 31 | impl RustfixMode { |
| 32 | pub(crate) fn enabled(self) -> bool { |
| 33 | self != RustfixMode::Disabled |
| 34 | } |
| 35 | } |
| 36 | |
| 37 | impl Flag for RustfixMode { |
| 38 | fn clone_inner(&self) -> Box<dyn Flag> { |
| 39 | Box::new(*self) |
| 40 | } |
| 41 | fn must_be_unique(&self) -> bool { |
| 42 | true |
| 43 | } |
| 44 | fn post_test_action( |
| 45 | &self, |
| 46 | config: &TestConfig, |
| 47 | output: &Output, |
| 48 | build_manager: &BuildManager, |
| 49 | ) -> Result<(), Errored> { |
| 50 | let global_rustfix = match config.exit_status()? { |
| 51 | Some(Spanned { |
| 52 | content: 101 | 0, .. |
| 53 | }) => RustfixMode::Disabled, |
| 54 | _ => *self, |
| 55 | }; |
| 56 | let output = output.clone(); |
| 57 | let no_run_rustfix = config.find_one_custom("no-rustfix" )?; |
| 58 | let fixes = if no_run_rustfix.is_none() && global_rustfix.enabled() { |
| 59 | fix(&output.stderr, config.status.path(), global_rustfix).map_err(|err| Errored { |
| 60 | command: format!("rustfix {}" , display(config.status.path())), |
| 61 | errors: vec![Error::Rustfix(err)], |
| 62 | stderr: output.stderr, |
| 63 | stdout: output.stdout, |
| 64 | })? |
| 65 | } else { |
| 66 | Vec::new() |
| 67 | }; |
| 68 | |
| 69 | let mut errors = Vec::new(); |
| 70 | let fixed_paths = match fixes.as_slice() { |
| 71 | [] => Vec::new(), |
| 72 | [single] => { |
| 73 | vec![config.check_output(single.as_bytes(), &mut errors, "fixed" )] |
| 74 | } |
| 75 | _ => fixes |
| 76 | .iter() |
| 77 | .enumerate() |
| 78 | .map(|(i, fix)| { |
| 79 | config.check_output(fix.as_bytes(), &mut errors, &format!(" {}.fixed" , i + 1)) |
| 80 | }) |
| 81 | .collect(), |
| 82 | }; |
| 83 | |
| 84 | if fixes.len() != 1 { |
| 85 | // Remove an unused .fixed file |
| 86 | config.check_output(&[], &mut errors, "fixed" ); |
| 87 | } |
| 88 | |
| 89 | if !errors.is_empty() { |
| 90 | return Err(Errored { |
| 91 | command: format!("checking {}" , display(config.status.path())), |
| 92 | errors, |
| 93 | stderr: vec![], |
| 94 | stdout: vec![], |
| 95 | }); |
| 96 | } |
| 97 | |
| 98 | compile_fixed(config, build_manager, fixed_paths) |
| 99 | } |
| 100 | } |
| 101 | |
| 102 | fn fix(stderr: &[u8], path: &Path, mode: RustfixMode) -> anyhow::Result<Vec<String>> { |
| 103 | let suggestions = std::str::from_utf8(stderr) |
| 104 | .unwrap() |
| 105 | .lines() |
| 106 | .filter_map(|line| { |
| 107 | if !line.starts_with('{' ) { |
| 108 | return None; |
| 109 | } |
| 110 | let diagnostic = serde_json::from_str(line).unwrap_or_else(|err| { |
| 111 | panic!("could not deserialize diagnostics json for rustfix {err}: {line}" ) |
| 112 | }); |
| 113 | rustfix::collect_suggestions( |
| 114 | &diagnostic, |
| 115 | &HashSet::new(), |
| 116 | if mode == RustfixMode::Everything { |
| 117 | Filter::Everything |
| 118 | } else { |
| 119 | Filter::MachineApplicableOnly |
| 120 | }, |
| 121 | ) |
| 122 | }) |
| 123 | .collect::<Vec<_>>(); |
| 124 | if suggestions.is_empty() { |
| 125 | return Ok(Vec::new()); |
| 126 | } |
| 127 | |
| 128 | let max_solutions = suggestions |
| 129 | .iter() |
| 130 | .map(|suggestion| suggestion.solutions.len()) |
| 131 | .max() |
| 132 | .unwrap(); |
| 133 | let src = std::fs::read_to_string(path).unwrap(); |
| 134 | let mut fixes = (0..max_solutions) |
| 135 | .map(|_| CodeFix::new(&src)) |
| 136 | .collect::<Vec<_>>(); |
| 137 | for Suggestion { |
| 138 | message, |
| 139 | snippets, |
| 140 | solutions, |
| 141 | } in suggestions |
| 142 | { |
| 143 | for snippet in &snippets { |
| 144 | anyhow::ensure!( |
| 145 | Path::new(&snippet.file_name) == path, |
| 146 | "cannot apply suggestions for ` {}` since main file is ` {}`. Please use `//@no-rustfix` to disable rustfix" , |
| 147 | snippet.file_name, |
| 148 | path.display() |
| 149 | ); |
| 150 | } |
| 151 | |
| 152 | let repeat_first = std::iter::from_fn(|| solutions.first()); |
| 153 | for (solution, fix) in solutions.iter().chain(repeat_first).zip(&mut fixes) { |
| 154 | // TODO: use CodeFix::apply_solution when rustfix 0.8.5 is published |
| 155 | fix.apply(&Suggestion { |
| 156 | solutions: vec![solution.clone()], |
| 157 | message: message.clone(), |
| 158 | snippets: snippets.clone(), |
| 159 | })?; |
| 160 | } |
| 161 | } |
| 162 | |
| 163 | fixes.into_iter().map(|fix| Ok(fix.finish()?)).collect() |
| 164 | } |
| 165 | |
| 166 | fn compile_fixed( |
| 167 | config: &TestConfig, |
| 168 | build_manager: &BuildManager, |
| 169 | fixed_paths: Vec<PathBuf>, |
| 170 | ) -> Result<(), Errored> { |
| 171 | // picking the crate name from the file name is problematic when `.revision_name` is inserted, |
| 172 | // so we compute it here before replacing the path. |
| 173 | let crate_name = config |
| 174 | .status |
| 175 | .path() |
| 176 | .file_stem() |
| 177 | .unwrap() |
| 178 | .to_str() |
| 179 | .unwrap() |
| 180 | .replace('-' , "_" ); |
| 181 | |
| 182 | let rustfix_comments = Arc::new(Comments { |
| 183 | revisions: None, |
| 184 | revisioned: std::iter::once(( |
| 185 | vec![], |
| 186 | Revisioned { |
| 187 | span: Span::default(), |
| 188 | ignore: vec![], |
| 189 | only: vec![], |
| 190 | stderr_per_bitwidth: false, |
| 191 | compile_flags: config.collect(|r| r.compile_flags.iter().cloned()), |
| 192 | env_vars: config.collect(|r| r.env_vars.iter().cloned()), |
| 193 | normalize_stderr: vec![], |
| 194 | normalize_stdout: vec![], |
| 195 | error_in_other_files: vec![], |
| 196 | error_matches: vec![], |
| 197 | require_annotations_for_level: Default::default(), |
| 198 | diagnostic_code_prefix: OptWithLine::new(String::new(), Span::default()), |
| 199 | custom: config.comments().flat_map(|r| r.custom.clone()).collect(), |
| 200 | exit_status: OptWithLine::new(0, Span::default()), |
| 201 | require_annotations: OptWithLine::default(), |
| 202 | }, |
| 203 | )) |
| 204 | .collect(), |
| 205 | }); |
| 206 | |
| 207 | for (i, fixed_path) in fixed_paths.into_iter().enumerate() { |
| 208 | let fixed_config = TestConfig { |
| 209 | config: config.config.clone(), |
| 210 | comments: rustfix_comments.clone(), |
| 211 | aux_dir: config.aux_dir.clone(), |
| 212 | status: config.status.for_path(&fixed_path), |
| 213 | }; |
| 214 | let mut cmd = fixed_config.build_command(build_manager)?; |
| 215 | cmd.arg("--crate-name" ) |
| 216 | .arg(format!("__ {crate_name}_ {}" , i + 1)); |
| 217 | build_manager.add_new_job(fixed_config, move |fixed_config| { |
| 218 | let output = cmd.output().unwrap(); |
| 219 | fixed_config.aborted()?; |
| 220 | if output.status.success() { |
| 221 | Ok(TestOk::Ok) |
| 222 | } else { |
| 223 | let diagnostics = fixed_config.process(&output.stderr); |
| 224 | Err(Errored { |
| 225 | command: format!(" {cmd:?}" ), |
| 226 | errors: vec![Error::ExitStatus { |
| 227 | expected: 0, |
| 228 | status: output.status, |
| 229 | reason: Spanned::new( |
| 230 | "after rustfix is applied, all errors should be gone, but weren't" |
| 231 | .into(), |
| 232 | diagnostics |
| 233 | .messages |
| 234 | .iter() |
| 235 | .flatten() |
| 236 | .chain(diagnostics.messages_from_unknown_file_or_line.iter()) |
| 237 | .find_map(|message| message.span.clone()) |
| 238 | .unwrap_or_default(), |
| 239 | ), |
| 240 | }], |
| 241 | stderr: diagnostics.rendered, |
| 242 | stdout: output.stdout, |
| 243 | }) |
| 244 | } |
| 245 | }); |
| 246 | } |
| 247 | |
| 248 | Ok(()) |
| 249 | } |
| 250 | |