use super::Flag;
use crate::{
build_manager::BuildManager,
display,
parser::OptWithLine,
per_test_config::{Comments, Revisioned, TestConfig},
Error, Errored, TestOk,
};
use rustfix::{CodeFix, Filter, Suggestion};
use spanned::{Span, Spanned};
use std::{
collections::HashSet,
path::{Path, PathBuf},
process::Output,
sync::Arc,
};
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum RustfixMode {
Disabled,
MachineApplicable,
Everything,
}
impl RustfixMode {
pub(crate) fn enabled(self) -> bool {
self != RustfixMode::Disabled
}
}
impl Flag for RustfixMode {
fn clone_inner(&self) -> Box<dyn Flag> {
Box::new(*self)
}
fn must_be_unique(&self) -> bool {
true
}
fn post_test_action(
&self,
config: &TestConfig,
output: &Output,
build_manager: &BuildManager,
) -> Result<(), Errored> {
let global_rustfix = match config.exit_status()? {
Some(Spanned {
content: 101 | 0, ..
}) => RustfixMode::Disabled,
_ => *self,
};
let output = output.clone();
let no_run_rustfix = config.find_one_custom("no-rustfix")?;
let fixes = if no_run_rustfix.is_none() && global_rustfix.enabled() {
fix(&output.stderr, config.status.path(), global_rustfix).map_err(|err| Errored {
command: format!("rustfix {}", display(config.status.path())),
errors: vec![Error::Rustfix(err)],
stderr: output.stderr,
stdout: output.stdout,
})?
} else {
Vec::new()
};
let mut errors = Vec::new();
let fixed_paths = match fixes.as_slice() {
[] => Vec::new(),
[single] => {
vec![config.check_output(single.as_bytes(), &mut errors, "fixed")]
}
_ => fixes
.iter()
.enumerate()
.map(|(i, fix)| {
config.check_output(fix.as_bytes(), &mut errors, &format!("{}.fixed", i + 1))
})
.collect(),
};
if fixes.len() != 1 {
config.check_output(&[], &mut errors, "fixed");
}
if !errors.is_empty() {
return Err(Errored {
command: format!("checking {}", display(config.status.path())),
errors,
stderr: vec![],
stdout: vec![],
});
}
compile_fixed(config, build_manager, fixed_paths)
}
}
fn fix(stderr: &[u8], path: &Path, mode: RustfixMode) -> anyhow::Result<Vec<String>> {
let suggestions = std::str::from_utf8(stderr)
.unwrap()
.lines()
.filter_map(|line| {
if !line.starts_with('{') {
return None;
}
let diagnostic = serde_json::from_str(line).unwrap_or_else(|err| {
panic!("could not deserialize diagnostics json for rustfix {err}:{line}")
});
rustfix::collect_suggestions(
&diagnostic,
&HashSet::new(),
if mode == RustfixMode::Everything {
Filter::Everything
} else {
Filter::MachineApplicableOnly
},
)
})
.collect::<Vec<_>>();
if suggestions.is_empty() {
return Ok(Vec::new());
}
let max_solutions = suggestions
.iter()
.map(|suggestion| suggestion.solutions.len())
.max()
.unwrap();
let src = std::fs::read_to_string(path).unwrap();
let mut fixes = (0..max_solutions)
.map(|_| CodeFix::new(&src))
.collect::<Vec<_>>();
for Suggestion {
message,
snippets,
solutions,
} in suggestions
{
for snippet in &snippets {
anyhow::ensure!(
Path::new(&snippet.file_name) == path,
"cannot apply suggestions for `{}` since main file is `{}`. Please use `//@no-rustfix` to disable rustfix",
snippet.file_name,
path.display()
);
}
let repeat_first = std::iter::from_fn(|| solutions.first());
for (solution, fix) in solutions.iter().chain(repeat_first).zip(&mut fixes) {
fix.apply(&Suggestion {
solutions: vec![solution.clone()],
message: message.clone(),
snippets: snippets.clone(),
})?;
}
}
fixes.into_iter().map(|fix| Ok(fix.finish()?)).collect()
}
fn compile_fixed(
config: &TestConfig,
build_manager: &BuildManager,
fixed_paths: Vec<PathBuf>,
) -> Result<(), Errored> {
let crate_name = config
.status
.path()
.file_stem()
.unwrap()
.to_str()
.unwrap()
.replace('-', "_");
let rustfix_comments = Arc::new(Comments {
revisions: None,
revisioned: std::iter::once((
vec![],
Revisioned {
span: Span::default(),
ignore: vec![],
only: vec![],
stderr_per_bitwidth: false,
compile_flags: config.collect(|r| r.compile_flags.iter().cloned()),
env_vars: config.collect(|r| r.env_vars.iter().cloned()),
normalize_stderr: vec![],
normalize_stdout: vec![],
error_in_other_files: vec![],
error_matches: vec![],
require_annotations_for_level: Default::default(),
diagnostic_code_prefix: OptWithLine::new(String::new(), Span::default()),
custom: config.comments().flat_map(|r| r.custom.clone()).collect(),
exit_status: OptWithLine::new(0, Span::default()),
require_annotations: OptWithLine::default(),
},
))
.collect(),
});
for (i, fixed_path) in fixed_paths.into_iter().enumerate() {
let fixed_config = TestConfig {
config: config.config.clone(),
comments: rustfix_comments.clone(),
aux_dir: config.aux_dir.clone(),
status: config.status.for_path(&fixed_path),
};
let mut cmd = fixed_config.build_command(build_manager)?;
cmd.arg("--crate-name")
.arg(format!("__{crate_name}_{}", i + 1));
build_manager.add_new_job(fixed_config, move |fixed_config| {
let output = cmd.output().unwrap();
fixed_config.aborted()?;
if output.status.success() {
Ok(TestOk::Ok)
} else {
let diagnostics = fixed_config.process(&output.stderr);
Err(Errored {
command: format!("{cmd:?}"),
errors: vec![Error::ExitStatus {
expected: 0,
status: output.status,
reason: Spanned::new(
"after rustfix is applied, all errors should be gone, but weren't"
.into(),
diagnostics
.messages
.iter()
.flatten()
.chain(diagnostics.messages_from_unknown_file_or_line.iter())
.find_map(|message| message.span.clone())
.unwrap_or_default(),
),
}],
stderr: diagnostics.rendered,
stdout: output.stdout,
})
}
});
}
Ok(())
}