| 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 |  | 
|---|