use std::ffi::OsString;
use std::path::PathBuf;
pub const ENV_RULES: &str = "DROIDSAW_SEMGREP_RULES";
#[derive(Debug, Clone, Default, clap::Args)]
pub struct SemgrepArgs {
#[arg(long = "rules", value_name = "PATH", action = clap::ArgAction::Append)]
pub rules: Vec<PathBuf>,
#[arg(long = "no-auto")]
pub no_auto: bool,
}
#[derive(Debug, thiserror::Error)]
pub enum SemgrepError {
#[error(
"--no-auto was passed but no rule sources were provided. \
Add --rules <path> or set DROIDSAW_SEMGREP_RULES, or drop --no-auto."
)]
NoRuleSource,
}
impl SemgrepArgs {
pub fn effective_rules(&self) -> Vec<PathBuf> {
let env_value = std::env::var(ENV_RULES).ok();
self.effective_rules_with_env(env_value.as_deref())
}
pub fn effective_rules_with_env(&self, env_value: Option<&str>) -> Vec<PathBuf> {
let mut out = self.rules.clone();
if let Some(raw) = env_value {
for p in std::env::split_paths(raw) {
if !p.as_os_str().is_empty() {
out.push(p);
}
}
}
out
}
}
fn compose_iter<F, T>(args: &SemgrepArgs, env_value: Option<&str>, path_fn: F) -> Vec<T>
where
F: Fn(PathBuf) -> T,
T: From<&'static str>,
{
let user_rules = args.effective_rules_with_env(env_value);
let mut out: Vec<T> = Vec::new();
if !args.no_auto {
out.push(T::from("--config"));
out.push(T::from("auto"));
}
for p in user_rules {
out.push(T::from("--config"));
out.push(path_fn(p));
}
out
}
pub fn compose_config_args(args: &SemgrepArgs) -> Result<Vec<String>, SemgrepError> {
let env_value = std::env::var(ENV_RULES).ok();
compose_config_args_with_env(args, env_value.as_deref())
}
pub fn compose_config_args_with_env(
args: &SemgrepArgs,
env_value: Option<&str>,
) -> Result<Vec<String>, SemgrepError> {
if args.no_auto && args.effective_rules_with_env(env_value).is_empty() {
return Err(SemgrepError::NoRuleSource);
}
Ok(compose_iter(args, env_value, |p| p.display().to_string()))
}
pub fn compose_argv(args: &SemgrepArgs) -> Result<Vec<OsString>, SemgrepError> {
let env_value = std::env::var(ENV_RULES).ok();
compose_argv_with_env(args, env_value.as_deref())
}
pub fn compose_argv_with_env(
args: &SemgrepArgs,
env_value: Option<&str>,
) -> Result<Vec<OsString>, SemgrepError> {
if args.no_auto && args.effective_rules_with_env(env_value).is_empty() {
return Err(SemgrepError::NoRuleSource);
}
Ok(compose_iter(args, env_value, |p| p.into_os_string()))
}
#[cfg(test)]
mod tests {
use super::*;
fn args_with(rules: Vec<&str>, no_auto: bool) -> SemgrepArgs {
SemgrepArgs {
rules: rules.into_iter().map(PathBuf::from).collect(),
no_auto,
}
}
#[test]
fn default_emits_auto_only() {
let v = compose_config_args_with_env(&SemgrepArgs::default(), None).unwrap();
assert_eq!(v, vec!["--config", "auto"]);
}
#[test]
fn rules_after_auto() {
let v = compose_config_args_with_env(&args_with(vec!["a.yml", "b.yml"], false), None)
.unwrap();
assert_eq!(
v,
vec!["--config", "auto", "--config", "a.yml", "--config", "b.yml"]
);
}
#[test]
fn no_auto_with_rules_omits_auto() {
let v = compose_config_args_with_env(&args_with(vec!["x.yml"], true), None).unwrap();
assert_eq!(v, vec!["--config", "x.yml"]);
}
#[test]
fn no_auto_no_rules_errors() {
let err =
compose_config_args_with_env(&args_with(vec![], true), None).unwrap_err();
assert!(matches!(err, SemgrepError::NoRuleSource));
}
#[test]
fn env_var_appends_after_cli_rules() {
let v = compose_config_args_with_env(
&args_with(vec!["cli.yml"], false),
Some(if cfg!(windows) {
"env1.yml;env2.yml"
} else {
"env1.yml:env2.yml"
}),
)
.unwrap();
assert_eq!(
v,
vec![
"--config", "auto", "--config", "cli.yml", "--config", "env1.yml", "--config",
"env2.yml",
]
);
}
#[test]
fn env_only_no_cli_rules() {
let v = compose_config_args_with_env(&SemgrepArgs::default(), Some("env.yml")).unwrap();
assert_eq!(v, vec!["--config", "auto", "--config", "env.yml"]);
}
#[test]
fn empty_env_segments_ignored() {
let v = compose_config_args_with_env(
&SemgrepArgs::default(),
Some(if cfg!(windows) { ";a.yml;" } else { ":a.yml:" }),
)
.unwrap();
assert_eq!(v, vec!["--config", "auto", "--config", "a.yml"]);
}
#[test]
fn no_auto_satisfied_by_env() {
let v = compose_config_args_with_env(
&args_with(vec![], true),
Some("env.yml"),
)
.unwrap();
assert_eq!(v, vec!["--config", "env.yml"]);
}
}