use clap::Parser;
use daml_lint::detector::{self, Severity};
use daml_lint::reporter::{self, OutputFormat};
use daml_lint::{detectors, parser};
use std::io::Write as _;
use std::path::{Path, PathBuf};
#[cfg(feature = "custom-rules")]
mod config;
#[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", value_parser = parse_output_format)]
format: OutputFormat,
#[arg(short, long)]
output: Option<PathBuf>,
#[arg(long, default_value = "high", value_parser = parse_fail_on)]
fail_on: Severity,
#[cfg(feature = "custom-rules")]
#[arg(short, long)]
config: Option<PathBuf>,
#[cfg(feature = "custom-rules")]
#[arg(long)]
rules: Vec<PathBuf>,
}
fn main() {
let cli = Cli::parse();
#[cfg(feature = "custom-rules")]
let detectors = {
let lint_config = config::LintConfig::load(cli.config.as_deref()).unwrap_or_else(|e| {
eprintln!("Error: {e}");
std::process::exit(2);
});
let mut detectors = detectors::create_builtin_detectors();
match lint_config.load_plugin_detectors() {
Ok(plugin_detectors) => detectors.extend(plugin_detectors),
Err(e) => {
eprintln!("Error: {e}");
std::process::exit(2);
}
}
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);
}
}
}
let detectors = lint_config.apply_rule_settings(detectors);
if let Err(e) = lint_config.validate_rule_settings(&detectors) {
eprintln!("Error: {e}");
std::process::exit(2);
}
detectors
};
#[cfg(not(feature = "custom-rules"))]
let detectors = detectors::create_builtin_detectors();
if let Some(duplicate_detector_name) = detector::find_duplicate_detector_name(&detectors) {
eprintln!(
"Error: rule '{duplicate_detector_name}': name collides with a built-in detector or another rule"
);
std::process::exit(2);
}
let (files, mut input_errors) = discover_daml_files(&cli.paths);
if !input_errors.is_empty() {
for error in &input_errors {
eprintln!("{error}");
}
}
if files.is_empty() && input_errors.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!("Error: could not read {}: {e}", file.display());
input_errors.push(InputDiscoveryError::ReadFailure {
path: file.clone(),
error: e,
});
continue;
}
};
let parse_result = parser::parse_daml_with_diagnostics(&source, file);
let module = parse_result.module;
for d in &parse_result.diagnostics {
eprintln!(
"daml-lint: parse [{}]: {}:{}:{}: {}",
d.category.as_str(),
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| {
b.severity
.rank()
.cmp(&a.severity.rank())
.then_with(|| a.file.cmp(&b.file))
.then_with(|| a.line.cmp(&b.line))
});
let mut reportable_parse_errors = parse_errors.clone();
reportable_parse_errors.extend(input_errors.iter().map(input_error_to_parse_error));
let output = reporter::format_findings(&all_findings, &reportable_parse_errors, cli.format);
if let Some(output_path) = &cli.output {
std::fs::write(output_path, &output).unwrap_or_else(|e| {
eprintln!("Error writing to {}: {e}", output_path.display());
std::process::exit(2);
});
eprintln!(
"daml-lint: {} finding(s) written to {}",
all_findings.len(),
output_path.display()
);
} else {
print!("{output}");
let _ = std::io::stdout().flush();
}
let code = if input_errors.is_empty() {
if parse_errors.is_empty() {
reporter::exit_code(&all_findings, cli.fail_on)
} else {
3
}
} else {
2
};
std::process::exit(code);
}
fn input_error_to_parse_error(error: &InputDiscoveryError) -> reporter::ParseError {
reporter::ParseError {
file: match error {
InputDiscoveryError::NotFound(path)
| InputDiscoveryError::NotADirectory(path)
| InputDiscoveryError::ReadDir { path, .. }
| InputDiscoveryError::ReadFailure { path, .. } => path.display().to_string(),
},
line: 1,
column: 1,
end_column: None,
message: error.to_string(),
category: parser::ParseDiagnosticCategory::UnsupportedSyntax,
}
}
fn parse_output_format(value: &str) -> Result<OutputFormat, String> {
value
.parse::<OutputFormat>()
.map_err(|_| format!("Unknown format '{value}'. Use sarif, markdown, or json."))
}
fn parse_fail_on(value: &str) -> Result<Severity, String> {
value.parse::<Severity>().map_err(|_| {
format!("Unknown severity '{value}'. Use critical, high, medium, low, or info.")
})
}
#[derive(Debug)]
enum InputDiscoveryError {
NotFound(PathBuf),
NotADirectory(PathBuf),
ReadDir {
path: PathBuf,
error: std::io::Error,
},
ReadFailure {
path: PathBuf,
error: std::io::Error,
},
}
impl std::fmt::Display for InputDiscoveryError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NotFound(path) => {
write!(f, "Error: scan path {} does not exist", path.display())
}
Self::NotADirectory(path) => {
write!(
f,
"Error: scan path {} is not a file or directory",
path.display()
)
}
Self::ReadDir { path, error } => {
write!(
f,
"Error: could not read directory {}: {error}",
path.display()
)
}
Self::ReadFailure { path, error } => {
write!(f, "Error: could not read {}: {error}", path.display())
}
}
}
}
fn discover_daml_files(paths: &[PathBuf]) -> (Vec<PathBuf>, Vec<InputDiscoveryError>) {
let mut daml_files = Vec::new();
let mut errors = Vec::new();
for path in paths {
collect_input_path(path, &mut daml_files, &mut errors);
}
daml_files.sort();
(daml_files, errors)
}
fn collect_input_path(
path: &Path,
daml_files: &mut Vec<PathBuf>,
errors: &mut Vec<InputDiscoveryError>,
) {
match path.metadata() {
Ok(_md) if _md.is_file() => {
if path.extension().is_some_and(|e| e == "daml") {
daml_files.push(path.to_path_buf());
}
}
Ok(md) if md.is_dir() => {
let entries = match std::fs::read_dir(path) {
Ok(entries) => entries,
Err(error) => {
errors.push(InputDiscoveryError::ReadDir {
path: path.to_path_buf(),
error,
});
return;
}
};
for entry in entries {
match entry {
Ok(entry) => collect_input_path(&entry.path(), daml_files, errors),
Err(error) => errors.push(InputDiscoveryError::ReadDir {
path: path.to_path_buf(),
error,
}),
}
}
}
Ok(_) => {
errors.push(InputDiscoveryError::NotADirectory(path.to_path_buf()));
}
Err(error) => {
if error.kind() == std::io::ErrorKind::NotFound {
errors.push(InputDiscoveryError::NotFound(path.to_path_buf()));
} else {
errors.push(InputDiscoveryError::ReadDir {
path: path.to_path_buf(),
error,
});
}
}
};
}