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