mod ast;
mod canon;
mod compare;
mod lexer;
mod normalize;
mod parser;
mod printer;
mod symbols;
mod vclshow;
mod vmod;
use clap::{Args, Parser, Subcommand};
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use vmod::VmodSpec;
#[derive(Parser)]
#[command(name = "vcl-normalizer", version)]
struct Cli {
#[command(subcommand)]
cmd: Cmd,
}
#[derive(Args)]
struct Common {
#[arg(short = 'I', long = "vcl-path", value_name = "DIR")]
include: Vec<PathBuf>,
#[arg(long = "vmod-path", value_name = "DIR")]
vmod_path: Vec<PathBuf>,
#[arg(long)]
no_vmod: bool,
#[arg(long = "from-vcl-show")]
from_vcl_show: bool,
}
#[derive(Subcommand)]
enum Cmd {
Dump {
#[command(flatten)]
common: Common,
file: PathBuf,
},
Print {
#[command(flatten)]
common: Common,
#[arg(long)]
rename: bool,
#[arg(long)]
no_comments: bool,
file: PathBuf,
},
Compare {
#[command(flatten)]
common: Common,
#[arg(long)]
diff: bool,
#[arg(long, default_value_t = 20)]
max_reports: usize,
#[arg(long)]
names: bool,
file_a: PathBuf,
file_b: PathBuf,
},
}
enum PipelineError {
Io(String),
Parse {
path: PathBuf,
err: ast::VclError,
from_vcl_show: bool,
},
Semantic {
sm: ast::SourceMap,
err: ast::VclError,
},
}
fn build(
path: &Path,
common: &Common,
rename: bool,
) -> Result<(ast::Program, ast::SourceMap, ast::NameMap), PipelineError> {
if !path.exists() {
return Err(PipelineError::Io(format!(
"error: no such file: {}",
path.display()
)));
}
if common.from_vcl_show && !common.include.is_empty() {
eprintln!("warning: -I/--vcl-path is ignored with --from-vcl-show");
}
let (mut program, sm) = if common.from_vcl_show {
parser::parse_vcl_show_file(path).map_err(|err| PipelineError::Parse {
path: path.to_path_buf(),
err,
from_vcl_show: true,
})?
} else {
parser::parse_file(path, &common.include).map_err(|err| PipelineError::Parse {
path: path.to_path_buf(),
err,
from_vcl_show: false,
})?
};
let vmod_paths = if !common.vmod_path.is_empty() {
common.vmod_path.clone()
} else {
vmod::default_vmod_paths()
};
let mut specs: BTreeMap<String, VmodSpec> = BTreeMap::new();
if !common.no_vmod {
for decl in &program.decls {
if let ast::Decl::Import { name, from, .. } = decl {
if let Some(spec) = vmod::load_vmod(name, from.as_deref(), &vmod_paths) {
specs.insert(name.clone(), spec);
}
}
}
}
if let Err(err) = symbols::validate(&program, &specs) {
return Err(PipelineError::Semantic { sm, err });
}
let names = match normalize::normalize(&mut program, &specs, rename) {
Ok(names) => names,
Err(err) => return Err(PipelineError::Semantic { sm, err }),
};
Ok((program, sm, names))
}
fn render_parse_error(path: &Path, err: &ast::VclError, from_vcl_show: bool) -> String {
if !from_vcl_show {
if let Some(span) = err.span {
if span.file == 0 {
if let Ok(content) = std::fs::read_to_string(path) {
if span.lo as usize <= content.len() && span.hi as usize <= content.len() {
let mut sm = ast::SourceMap::default();
sm.add(path.to_path_buf(), content);
return err.render(&sm);
}
}
}
}
}
err.msg.clone()
}
fn report_pipeline_error(e: PipelineError) -> ExitCode {
match e {
PipelineError::Io(msg) => {
eprintln!("{msg}");
ExitCode::from(3)
}
PipelineError::Parse {
path,
err,
from_vcl_show,
} => {
eprintln!("{}", render_parse_error(&path, &err, from_vcl_show));
ExitCode::from(2)
}
PipelineError::Semantic { sm, err } => {
eprintln!("{}", err.render(&sm));
ExitCode::from(2)
}
}
}
fn print_names(a: &ast::NameMap, b: &ast::NameMap) {
println!("names (A: canonical <- original):");
for (kind, canonical, original) in &a.entries {
println!(" {kind:<10} {canonical:<24} <- {original}");
}
println!("names (B: canonical <- original):");
for (kind, canonical, original) in &b.entries {
println!(" {kind:<10} {canonical:<24} <- {original}");
}
}
fn main() -> ExitCode {
let cli = match Cli::try_parse() {
Ok(cli) => cli,
Err(e) => {
if e.use_stderr() {
eprint!("{e}");
return ExitCode::from(3);
}
print!("{e}");
return ExitCode::SUCCESS;
}
};
match cli.cmd {
Cmd::Dump { common, file } => match build(&file, &common, true) {
Ok((program, _sm, _names)) => {
println!("{}", canon::to_string(&program));
ExitCode::SUCCESS
}
Err(e) => report_pipeline_error(e),
},
Cmd::Print {
common,
rename,
no_comments,
file,
} => match build(&file, &common, rename) {
Ok((mut program, _sm, _names)) => {
if no_comments {
program.comments = ast::CommentMap::default();
program.trailing_comments = Vec::new();
}
println!("{}", printer::print(&program));
ExitCode::SUCCESS
}
Err(e) => report_pipeline_error(e),
},
Cmd::Compare {
common,
diff,
max_reports,
names,
file_a,
file_b,
} => {
let (a, sm_a, names_a) = match build(&file_a, &common, true) {
Ok(v) => v,
Err(e) => return report_pipeline_error(e),
};
let (b, sm_b, names_b) = match build(&file_b, &common, true) {
Ok(v) => v,
Err(e) => return report_pipeline_error(e),
};
if canon::canon_eq(&a, &b) {
println!("equivalent");
return ExitCode::SUCCESS;
}
let divs = compare::compare(&a, &b, max_reports);
let capped = divs.len() >= max_reports;
print!("{}", compare::render_report(&divs, &sm_a, &sm_b, capped));
if diff {
let print_a = printer::print(&a);
let print_b = printer::print(&b);
let text_diff = similar::TextDiff::from_lines(print_a.as_str(), print_b.as_str());
println!("{}", text_diff.unified_diff().context_radius(3));
}
if names {
print_names(&names_a, &names_b);
}
ExitCode::from(1)
}
}
}