use std::{
env::current_dir,
fs,
path::{Path, PathBuf},
process::{Command, Stdio, exit},
thread::{self, available_parallelism},
};
use compio::runtime::Runtime;
use syn::{spanned::Spanned, visit::Visit};
use crate::{
cache::{self, FileMeta, find_project_root, get_cache_file_path, load_cache, save_cache},
error::Result,
hash::HashSet,
i18n, manifest,
options::Options,
parser::{ModuleContext, PathCollector, collect_file_scope},
rewriter::{RewriteResult, compute_line_offsets, rewrite_modules_with_offsets},
walker::scan_rs_files,
};
#[derive(Debug, Default)]
pub struct ProcessSummary {
pub changed_files: usize,
pub total_files: usize,
pub total_replacements: usize,
}
pub fn format_file(file: &Path) {
if let Ok(content) = fs::read(file)
&& let Ok(mut child) = Command::new("rustfmt")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
{
if let Some(mut stdin) = child.stdin.take() {
use std::io::Write;
let _ = stdin.write_all(&content);
}
if let Ok(output) = child.wait_with_output()
&& output.status.success()
&& !output.stdout.is_empty()
&& output.stdout != content
{
let _ = fs::write(file, output.stdout);
}
}
}
pub fn process_file_with_crates(
path: &Path,
options: &Options,
known_crates: &HashSet<String>,
) -> Result<Option<RewriteResult>> {
let content = fs::read_to_string(path)?;
let syntax = match syn::parse_file(&content) {
Ok(s) => s,
Err(e) => {
eprintln!(
"[warn] syntax parse error in {}: {e}, skipping",
path.display()
);
return Ok(None);
}
};
if syntax
.attrs
.iter()
.any(|a| a.path().is_ident("no_implicit_prelude"))
{
return Ok(None);
}
let scope = collect_file_scope(&syntax);
let line_offsets = compute_line_offsets(&content);
let insert_pos = find_top_level_insert_pos_with_offsets(&syntax, &content, &line_offsets);
let has_existing_use = !scope.existing_use_paths.is_empty();
let root_module = ModuleContext {
insert_pos,
indent: String::new(),
has_existing_use,
scope,
paths: Vec::new(),
};
let mut collector = PathCollector::new(
options,
known_crates,
root_module,
&content,
&line_offsets,
);
collector.visit_file(&syntax);
let modules = collector.finish();
let has_paths = modules.iter().any(|m| !m.paths.is_empty());
if !has_paths {
return Ok(None);
}
let replaced = rewrite_modules_with_offsets(
&content,
&line_offsets,
&modules,
options,
);
Ok(replaced)
}
pub fn find_top_level_insert_pos(syntax: &syn::File, source: &str) -> usize {
let line_offsets = compute_line_offsets(source);
find_top_level_insert_pos_with_offsets(syntax, source, &line_offsets)
}
pub fn find_top_level_insert_pos_with_offsets(
syntax: &syn::File,
source: &str,
line_offsets: &[usize],
) -> usize {
let mut last_top_use_end_line = None;
let mut first_item_start_line = None;
let mut max_inner_attr_end_line = 0;
for attr in &syntax.attrs {
if matches!(attr.style, syn::AttrStyle::Inner(_)) {
max_inner_attr_end_line = max_inner_attr_end_line.max(attr.span().end().line);
}
}
for item in &syntax.items {
match item {
syn::Item::Use(u) => {
let end_line = u.span().end().line;
last_top_use_end_line = Some(end_line);
}
_ => {
first_item_start_line = Some(item.span().start().line);
break;
}
}
}
if let Some(line) = last_top_use_end_line {
if line < line_offsets.len() {
return line_offsets[line];
}
return source.len();
}
if let Some(line) = first_item_start_line
&& line > 0
&& line - 1 < line_offsets.len()
{
let pos = line_offsets[line - 1];
if max_inner_attr_end_line > 0 && max_inner_attr_end_line < line_offsets.len() {
return pos.max(line_offsets[max_inner_attr_end_line]);
}
return pos;
}
if max_inner_attr_end_line > 0 && max_inner_attr_end_line < line_offsets.len() {
return line_offsets[max_inner_attr_end_line];
}
0
}
type FileCheckResult = (PathBuf, String, Option<FileMeta>, Option<RewriteResult>);
pub fn run(options: &Options) -> Result<ProcessSummary> {
let mut opts = options.clone();
let target_path = opts.path.clone().unwrap_or_else(|| {
let cwd = current_dir().unwrap_or_else(|_| PathBuf::from("."));
find_project_root(&cwd)
});
let project_root = find_project_root(&target_path);
opts.load_clippy_toml_for(&project_root);
let cache_path = get_cache_file_path(&project_root);
let expected_hash = opts.config_hash();
let mut cache = if !opts.no_cache
&& let Some(ref cp) = cache_path
{
load_cache(cp, expected_hash)
} else {
cache::ProjectCache {
config_hash: expected_hash,
clean_files: Default::default(),
}
};
let files = scan_rs_files(&target_path);
if files.is_empty() {
return Ok(ProcessSummary::default());
}
let known_crates = manifest::load_project_crates(&project_root, &opts.extra_crates);
let num_threads = available_parallelism().map(|n| n.get()).unwrap_or(1);
let chunk_size = files.len().div_ceil(num_threads);
let mut all_results = Vec::new();
thread::scope(|s| {
let mut handles = Vec::with_capacity(num_threads);
for chunk in files.chunks(chunk_size) {
let opts = &opts;
let root = &project_root;
let clean_cache = &cache.clean_files;
let crates = &known_crates;
let handle = s.spawn(move || -> Vec<FileCheckResult> {
let rt = match Runtime::new() {
Ok(rt) => rt,
Err(e) => {
eprintln!("[warn] {}: {e}", i18n::msg().failed_init_runtime);
return Vec::new();
}
};
rt.block_on(async move {
let mut thread_results = Vec::with_capacity(chunk.len());
for file in chunk {
let rel_path_cow = file
.strip_prefix(root.as_path())
.unwrap_or(file.as_path())
.to_string_lossy();
let meta = FileMeta::from_path(file);
if !opts.no_cache
&& let Some(ref m) = meta
&& clean_cache.get(rel_path_cow.as_ref()) == Some(m)
{
continue;
}
let rel_path = rel_path_cow.into_owned();
match process_file_with_crates(file, opts, crates) {
Ok(Some(res)) => {
thread_results.push((file.clone(), rel_path, meta, Some(res)));
}
Ok(None) => {
thread_results.push((file.clone(), rel_path, meta, None));
}
Err(e) => {
let f_str = file.display().to_string();
let e_str = e.to_string();
eprintln!("{}", (i18n::msg().error_processing_file)(&f_str, &e_str));
}
}
}
thread_results
})
});
handles.push(handle);
}
for h in handles {
if let Ok(res) = h.join() {
all_results.extend(res);
}
}
});
let mut changed_count = 0;
let mut total_replacements = 0;
for (file, rel_path, meta, rewrite_res) in all_results {
if let Some(res) = rewrite_res {
changed_count += 1;
total_replacements += res.replacements.len();
if opts.should_show_details() {
println!("{rel_path}");
for (orig, repl) in &res.replacements {
println!(" {orig} -> {repl}");
}
}
if opts.should_write() {
fs::write(&file, &res.content)?;
format_file(&file);
if !opts.no_cache
&& let Some(updated_meta) = FileMeta::from_path(&file)
{
cache.clean_files.insert(rel_path, updated_meta);
}
}
} else if !opts.no_cache
&& let Some(m) = meta
{
cache.clean_files.insert(rel_path, m);
}
}
if ((!opts.no_cache && opts.should_write()) || changed_count == 0)
&& let Some(ref cp) = cache_path
{
let _ = save_cache(cp, &cache);
}
Ok(ProcessSummary {
changed_files: changed_count,
total_files: files.len(),
total_replacements,
})
}
pub fn finish_run(summary: &ProcessSummary, options: &Options) {
let m = i18n::msg();
if options.check && summary.changed_files > 0 {
if options.should_show_details() {
eprintln!("{}", (m.found_exceeding_limit)(summary.changed_files));
}
exit(1);
}
if options.should_show_details() && !options.should_write() && summary.changed_files > 0 {
println!("{}", (m.can_be_simplified)(summary.changed_files));
}
}