use anyhow::Result;
use std::fs;
use std::path::Path;
fn print_script_warnings(sfc: &velox_sfc::Sfc, path: &Path) {
for block in [sfc.script_setup.as_ref(), sfc.script.as_ref()]
.into_iter()
.flatten()
{
for warning in velox_sfc::lint_script(&block.content) {
println!("⚠️ {} - {}", path.display(), warning);
}
}
}
fn print_style_warnings(sfc: &velox_sfc::Sfc, path: &Path) {
let Some(block) = sfc.style.as_ref() else {
return;
};
let sheet = velox_style::Stylesheet::parse(&block.content);
for (i, rule) in sheet.rules.iter().enumerate() {
let mut props: Vec<&str> = rule.decls.keys().map(String::as_str).collect();
props.sort_unstable();
for prop in props {
if let Some((_, why)) = velox_style::ComputedStyle::PARSED_BUT_UNRENDERED
.iter()
.find(|(name, _)| *name == prop)
{
println!(
"⚠️ {} - <style> rule {}: `{}` is parsed but never rendered ({why})",
path.display(),
i + 1,
prop
);
}
}
}
}
fn lint_file_error(path: &Path) -> bool {
let content = match fs::read_to_string(path) {
Ok(c) => c,
Err(e) => {
println!("❌ {} - Read error: {}", path.display(), e);
return true;
}
};
match velox_sfc::parse_sfc(&content) {
Ok(sfc) => {
print_script_warnings(&sfc, path);
print_style_warnings(&sfc, path);
false
}
Err(e) => {
println!("❌ {} - Parse error: {}", path.display(), e);
true
}
}
}
fn normalize_source(src: &str) -> String {
let mut lines: Vec<&str> = src.split('\n').map(|l| l.trim_end()).collect();
while lines.last().is_some_and(|l| l.is_empty()) {
lines.pop();
}
let mut out = lines.join("\n");
out.push('\n');
out
}
fn fix_file(path: &Path) -> bool {
let original = match fs::read_to_string(path) {
Ok(c) => c,
Err(e) => {
println!("❌ {} - Read error: {}", path.display(), e);
return true;
}
};
let sfc = match velox_sfc::parse_sfc(&original) {
Ok(sfc) => sfc,
Err(e) => {
println!("❌ {} - Parse error: {}", path.display(), e);
return true;
}
};
print_script_warnings(&sfc, path);
print_style_warnings(&sfc, path);
let fixed = normalize_source(&original);
if fixed == original {
println!("✅ {}", path.display());
return false;
}
match fs::write(path, &fixed) {
Ok(()) => {
println!("🔧 {} - fixed", path.display());
false
}
Err(e) => {
println!("❌ {} - Write error: {}", path.display(), e);
true
}
}
}
pub fn lint_directory_fix(dir: &Path, fix: bool) -> Result<()> {
fn walk(dir: &Path, file_count: &mut usize, error_count: &mut usize, fix: bool) -> Result<()> {
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
walk(&path, file_count, error_count, fix)?;
continue;
}
if path.extension().and_then(|s| s.to_str()) == Some("vx") {
*file_count += 1;
let errored = if fix {
fix_file(&path)
} else {
lint_file_error(&path)
};
if errored {
*error_count += 1;
}
}
}
Ok(())
}
let mut file_count = 0usize;
let mut error_count = 0usize;
walk(dir, &mut file_count, &mut error_count, fix)?;
if file_count == 0 {
println!("⚠️ No .vx files found in {}", dir.display());
return Ok(());
}
let label = if fix { "Lint+fix" } else { "Lint" };
println!(
"\n📊 {label} results: {} files, {} errors",
file_count, error_count
);
if error_count > 0 {
anyhow::bail!("Lint failed with {} errors", error_count);
}
Ok(())
}
pub fn lint_file(path: &Path) -> Result<()> {
if lint_file_error(path) {
Err(anyhow::anyhow!("Lint failed"))
} else {
Ok(())
}
}
pub fn fix_file_single(path: &Path) -> Result<()> {
if fix_file(path) {
Err(anyhow::anyhow!("Lint failed"))
} else {
Ok(())
}
}
pub fn lint_directory(dir: &Path) -> Result<()> {
lint_directory_fix(dir, false)
}