use clap::Parser;
use daml_lint::detector::{self, parse_severity};
use daml_lint::reporter::{self, OutputFormat};
use daml_lint::{detectors, parser};
use std::path::PathBuf;
#[derive(Parser)]
#[command(name = "daml-lint")]
#[command(about = "Static analysis scanner for DAML smart contracts")]
#[command(version)]
struct Cli {
#[arg(required = true)]
paths: Vec<PathBuf>,
#[arg(short, long, default_value = "markdown")]
format: String,
#[arg(short, long)]
output: Option<PathBuf>,
#[arg(long, default_value = "high")]
fail_on: String,
#[arg(long)]
rules: Vec<PathBuf>,
}
fn main() {
let cli = Cli::parse();
let format = cli.format.parse::<OutputFormat>().unwrap_or_else(|()| {
eprintln!(
"Unknown format '{}'. Use sarif, markdown, or json.",
cli.format
);
std::process::exit(2);
});
let fail_on = parse_severity(&cli.fail_on).unwrap_or_else(|| {
eprintln!(
"Unknown severity '{}'. Use critical, high, medium, low, or info.",
cli.fail_on
);
std::process::exit(2);
});
let mut detectors = detector::all_detectors();
for rules_path in &cli.rules {
match detectors::script::load_script(rules_path) {
Ok(rule) => detectors.push(rule),
Err(e) => {
eprintln!("Error: {}", e);
std::process::exit(2);
}
}
}
if let Some(duplicate_detector_name) = detector::find_duplicate_name(&detectors) {
eprintln!(
"Error: rule '{}': name collides with a built-in detector or another rule",
duplicate_detector_name
);
std::process::exit(2);
}
let files = discover_daml_files(&cli.paths);
if files.is_empty() {
eprintln!("No .daml files found.");
std::process::exit(2);
}
eprintln!("daml-lint: scanning {} file(s)...", files.len());
let mut all_findings = Vec::new();
let mut parse_errors = Vec::new();
for file in &files {
let source = match std::fs::read_to_string(file) {
Ok(s) => s,
Err(e) => {
eprintln!("Warning: could not read {}: {}", file.display(), e);
continue;
}
};
let (module, diagnostics) = parser::parse_daml_with_diagnostics(&source, file);
for d in &diagnostics {
eprintln!(
"daml-lint: parse [{}]: {}:{}:{}: {}",
d.category,
file.display(),
d.line,
d.column,
d.message
);
parse_errors.push(reporter::ParseError {
file: file.display().to_string(),
line: d.line,
column: d.column,
end_column: d.end_column,
message: d.message.clone(),
category: d.category,
});
}
for det in &detectors {
match det.try_detect(&module) {
Ok(findings) => all_findings.extend(findings),
Err(e) => {
eprintln!("Error: {}", e);
std::process::exit(2);
}
}
}
}
all_findings.sort_by(|a, b| {
a.severity
.cmp(&b.severity)
.then_with(|| a.file.cmp(&b.file))
.then_with(|| a.line.cmp(&b.line))
});
let output = reporter::format_findings(&all_findings, &parse_errors, format);
if let Some(output_path) = &cli.output {
std::fs::write(output_path, &output).unwrap_or_else(|e| {
eprintln!("Error writing to {}: {}", output_path.display(), e);
std::process::exit(2);
});
eprintln!(
"daml-lint: {} finding(s) written to {}",
all_findings.len(),
output_path.display()
);
} else {
println!("{}", output);
}
let code = if parse_errors.is_empty() {
reporter::exit_code(&all_findings, fail_on)
} else {
3
};
std::process::exit(code);
}
fn discover_daml_files(paths: &[PathBuf]) -> Vec<PathBuf> {
let mut daml_files = Vec::new();
for path in paths {
if path.is_file() {
if path.extension().is_some_and(|e| e == "daml") {
daml_files.push(path.clone());
}
} else if path.is_dir() {
collect_daml_files(path, &mut daml_files);
} else {
eprintln!("Warning: scan path {} does not exist.", path.display());
}
}
daml_files.sort();
daml_files
}
fn collect_daml_files(dir: &PathBuf, daml_files: &mut Vec<PathBuf>) {
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
collect_daml_files(&path, daml_files);
} else if path.extension().is_some_and(|e| e == "daml") {
daml_files.push(path);
}
}
}
}