use clap::Args;
use glob::glob;
use ignore::Walk;
use rayon::prelude::*;
use std::fs;
use std::path::PathBuf;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Instant;
use vize_patina::{format_results, format_summary, HelpLevel, Linter, OutputFormat};
#[derive(Args)]
pub struct LintArgs {
#[arg(default_value = "./**/*.vue")]
pub patterns: Vec<String>,
#[arg(long)]
pub fix: bool,
#[arg(short, long)]
pub config: Option<PathBuf>,
#[arg(short, long, default_value = "text")]
pub format: String,
#[arg(long)]
pub max_warnings: Option<usize>,
#[arg(short, long)]
pub quiet: bool,
#[arg(long, default_value = "full")]
pub help_level: String,
}
pub fn run(args: LintArgs) {
let start = Instant::now();
let files: Vec<PathBuf> = args
.patterns
.iter()
.flat_map(|pattern| {
if pattern.contains('*') || pattern.contains('?') || pattern.contains('[') {
glob(pattern)
.ok()
.into_iter()
.flatten()
.filter_map(|r| r.ok())
.filter(|p| {
p.extension().is_some_and(|ext| ext == "vue")
&& !p.components().any(|c| c.as_os_str() == "node_modules")
})
.collect::<Vec<_>>()
} else {
Walk::new(pattern)
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().is_some_and(|ext| ext == "vue"))
.map(|e| e.path().to_path_buf())
.collect::<Vec<_>>()
}
})
.collect();
if files.is_empty() {
eprintln!("No .vue files found matching patterns: {:?}", args.patterns);
return;
}
let help_level = match args.help_level.as_str() {
"none" => HelpLevel::None,
"short" => HelpLevel::Short,
_ => HelpLevel::Full,
};
let linter = Linter::new().with_help_level(help_level);
let error_count = AtomicUsize::new(0);
let warning_count = AtomicUsize::new(0);
let results: Vec<_> = files
.par_iter()
.filter_map(|path| {
let source = match fs::read_to_string(path) {
Ok(s) => s,
Err(e) => {
eprintln!("Failed to read {}: {}", path.display(), e);
return None;
}
};
let filename = path.to_string_lossy().to_string();
let result = linter.lint_sfc(&source, &filename);
error_count.fetch_add(result.error_count, Ordering::Relaxed);
warning_count.fetch_add(result.warning_count, Ordering::Relaxed);
Some((filename, source, result))
})
.collect();
let total_errors = error_count.load(Ordering::Relaxed);
let total_warnings = warning_count.load(Ordering::Relaxed);
let format = match args.format.as_str() {
"json" => OutputFormat::Json,
_ => OutputFormat::Text,
};
if !args.quiet || total_errors > 0 || total_warnings > 0 {
let lint_results: Vec<_> = results.iter().map(|(_, _, r)| r).cloned().collect();
let sources: Vec<_> = results
.iter()
.map(|(f, s, _)| (f.clone(), s.clone()))
.collect();
let output = format_results(&lint_results, &sources, format);
if !output.trim().is_empty() {
print!("{}", output);
}
}
let elapsed = start.elapsed();
if format == OutputFormat::Text {
println!(
"\n{}",
format_summary(total_errors, total_warnings, files.len())
);
println!("Linted {} files in {:.4?}", files.len(), elapsed);
}
if args.fix {
eprintln!("\nNote: --fix is not yet implemented");
}
if total_errors > 0 {
std::process::exit(1);
}
if let Some(max) = args.max_warnings {
if total_warnings > max {
eprintln!("\nToo many warnings ({} > max {})", total_warnings, max);
std::process::exit(1);
}
}
}