mod ast;
mod cli;
pub mod languages;
pub mod processor;
mod rules;
use anyhow::{Context, Result};
use clap::Parser;
use glob::glob;
use processor::OutputWriter;
use rayon::prelude::*;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
fn main() -> Result<()> {
let cli = cli::Cli::parse();
let options = cli.processing_options();
if cli.paths.is_empty() {
eprintln!("Error: No input paths specified. Use 'uncomment --help' for usage information.");
std::process::exit(1);
}
let files = collect_files(&cli.paths, &options)?;
if files.is_empty() {
eprintln!("No supported files found to process in the specified paths.");
eprintln!("Supported extensions: .rs, .py, .js, .jsx, .mjs, .cjs, .ts, .tsx, .mts, .cts, .d.ts, .java, .go, .c, .cpp, .rb, and more.");
if options.respect_gitignore {
eprintln!("Tip: Use --no-gitignore to process files ignored by git.");
}
return Ok(());
}
let num_threads = if cli.threads == 0 {
num_cpus::get()
} else {
cli.threads
};
if cli.verbose && num_threads > 1 {
println!("🔧 Using {} parallel threads", num_threads);
}
rayon::ThreadPoolBuilder::new()
.num_threads(num_threads)
.build_global()
.unwrap();
let output_writer = Arc::new(OutputWriter::new(options.dry_run, cli.verbose));
let total_files = files.len();
let results = Arc::new(Mutex::new(Vec::new()));
let modified_count = Arc::new(Mutex::new(0usize));
if num_threads == 1 {
let mut processor = processor::Processor::new();
for file_path in files {
match processor.process_file(&file_path, &options) {
Ok(mut processed_file) => {
processed_file.modified =
processed_file.original_content != processed_file.processed_content;
if processed_file.modified {
*modified_count.lock().unwrap() += 1;
}
output_writer.write_file(&processed_file)?;
}
Err(e) => {
eprintln!("Error processing {}: {}", file_path.display(), e);
if cli.verbose {
eprintln!(" Full error: {:?}", e);
}
}
}
}
} else {
files.par_iter().for_each(|file_path| {
let mut processor = processor::Processor::new();
match processor.process_file(file_path, &options) {
Ok(mut processed_file) => {
processed_file.modified =
processed_file.original_content != processed_file.processed_content;
if processed_file.modified {
*modified_count.lock().unwrap() += 1;
}
results.lock().unwrap().push(processed_file);
}
Err(e) => {
eprintln!("Error processing {}: {}", file_path.display(), e);
if cli.verbose {
eprintln!(" Full error: {:?}", e);
}
}
}
});
let results = Arc::try_unwrap(results).unwrap().into_inner().unwrap();
for processed_file in results {
output_writer.write_file(&processed_file)?;
}
}
let modified_files = *modified_count.lock().unwrap();
output_writer.print_summary(total_files, modified_files);
Ok(())
}
fn collect_files(paths: &[String], options: &processor::ProcessingOptions) -> Result<Vec<PathBuf>> {
let mut files = Vec::new();
for path_pattern in paths {
let path = Path::new(path_pattern);
if path.is_file() {
files.push(path.to_path_buf());
} else if path.is_dir() {
let pattern = format!("{}/**/*", path.display());
collect_from_pattern(&pattern, &mut files, options)?
} else {
collect_from_pattern(path_pattern, &mut files, options)?
}
}
files.sort();
files.dedup();
Ok(files)
}
fn collect_from_pattern(
pattern: &str,
files: &mut Vec<PathBuf>,
options: &processor::ProcessingOptions,
) -> Result<()> {
if options.respect_gitignore {
use ignore::WalkBuilder;
let pattern_path = if pattern.contains("/**/*") {
pattern.strip_suffix("/**/*").unwrap_or(".")
} else {
"."
};
let walker = WalkBuilder::new(pattern_path)
.hidden(false) .git_ignore(true)
.git_global(true)
.git_exclude(true)
.require_git(false) .build();
for entry in walker {
match entry {
Ok(entry) => {
let path = entry.path();
if path.is_file() && has_supported_extension(path) {
files.push(path.to_path_buf());
}
}
Err(e) => eprintln!("Error reading path: {}", e),
}
}
} else {
for entry in glob(pattern).context("Failed to parse glob pattern")? {
match entry {
Ok(path) => {
if path.is_file() && has_supported_extension(&path) {
files.push(path);
}
}
Err(e) => eprintln!("Error reading path: {}", e),
}
}
}
Ok(())
}
fn has_supported_extension(path: &Path) -> bool {
if let Some(ext) = path.extension() {
let ext_str = ext.to_string_lossy().to_lowercase();
let supported_extensions = [
"py", "pyw", "pyi", "pyx", "pxd", "js", "jsx", "mjs", "cjs", "ts", "tsx", "mts", "cts", "rs", "go", "java", "c", "h", "cpp", "cc", "cxx", "hpp", "hxx", "hh", "rb", "yml", "yaml", "hcl", "tf", "tfvars", ];
if path.to_string_lossy().ends_with(".d.ts") {
return true;
}
if let Some(filename) = path.file_name() {
let filename_str = filename.to_string_lossy().to_lowercase();
if filename_str == "makefile" || filename_str.ends_with(".mk") {
return true;
}
}
supported_extensions.iter().any(|&e| e == ext_str)
} else {
if let Some(filename) = path.file_name() {
let filename_str = filename.to_string_lossy().to_lowercase();
filename_str == "makefile"
} else {
false
}
}
}