1//! All the logic needed to run rustfix on a test that failed compilation
2
3use super::Flag;
4use crate::{
5 build_manager::BuildManager,
6 display,
7 parser::OptWithLine,
8 per_test_config::{Comments, Revisioned, TestConfig},
9 Error, Errored, TestOk,
10};
11use rustfix::{CodeFix, Filter, Suggestion};
12use spanned::{Span, Spanned};
13use 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)]
22pub 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
31impl RustfixMode {
32 pub(crate) fn enabled(self) -> bool {
33 self != RustfixMode::Disabled
34 }
35}
36
37impl 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
102fn 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
166fn 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

Provided by KDAB

Privacy Policy
Learn Rust with the experts
Find out more