use std::str::FromStr;
use clap::Parser;
#[derive(Parser)]
#[command(name = "meta-ast", version, about = "Polyglot static analyzer")]
pub enum Cli {
Inspect(InspectArgs),
Graph(GraphArgs),
#[cfg(feature = "metacall-deploy")]
Deploy(DeployArgs),
}
fn parse_format(s: &str) -> Result<crate::output::OutputFormat, String> {
match s.to_lowercase().as_str() {
"json" => Ok(crate::output::OutputFormat::Json),
"yaml" | "yml" => Ok(crate::output::OutputFormat::Yaml),
_ => Err(format!("invalid format '{s}': expected 'json' or 'yaml'")),
}
}
fn parse_language(s: &str) -> Result<crate::language::LangId, String> {
let normalized = s.to_lowercase();
crate::language::LangId::from_str(&normalized).map_err(|_| {
let all = crate::language::LangId::all();
let names: Vec<_> = all.iter().map(|l| l.as_ref()).collect();
format!(
"invalid language '{s}': expected one of {}",
names.join(", ")
)
})
}
#[derive(Parser)]
pub struct InspectArgs {
pub path: std::path::PathBuf,
#[arg(short, long)]
pub output: Option<std::path::PathBuf>,
#[arg(short, long, value_parser = parse_language)]
pub language: Option<crate::language::LangId>,
#[arg(short = 'f', long, default_value = "json", value_parser = parse_format)]
pub format: crate::output::OutputFormat,
#[arg(long, value_enum, default_value_t = crate::interface::report::FailOn::Error)]
pub fail_on: crate::interface::report::FailOn,
}
#[derive(Parser)]
pub struct GraphArgs {
pub path: std::path::PathBuf,
#[arg(short, long)]
pub output: Option<std::path::PathBuf>,
#[arg(short, long, value_parser = parse_language)]
pub language: Option<crate::language::LangId>,
#[arg(short = 'f', long, default_value = "json", value_parser = parse_format)]
pub format: crate::output::OutputFormat,
#[arg(long, value_enum, default_value_t = crate::interface::report::FailOn::Error)]
pub fail_on: crate::interface::report::FailOn,
#[arg(long)]
pub html: bool,
#[arg(long, requires = "html")]
pub open: bool,
#[cfg(feature = "dataflow")]
#[arg(long)]
pub datagraph: bool,
#[cfg(feature = "dataflow")]
#[arg(long)]
pub datagraph_output: Option<std::path::PathBuf>,
#[cfg(feature = "watch")]
#[arg(long)]
pub watch: bool,
#[cfg(feature = "watch")]
#[arg(long, default_value = "200")]
pub watch_debounce: u64,
}
#[cfg(feature = "metacall-deploy")]
#[derive(Parser)]
pub struct DeployArgs {
pub path: std::path::PathBuf,
#[arg(short = 'f', long, default_value = "json", value_parser = parse_format)]
pub format: crate::output::OutputFormat,
#[arg(long)]
pub check: bool,
#[arg(short, long, default_value = ".")]
pub out: std::path::PathBuf,
#[arg(long, default_value_t = crate::deploy::cut::DEFAULT_MAX_POD_SIZE)]
pub max_pod_size: usize,
#[arg(long, value_enum, default_value_t = crate::interface::report::FailOn::Error)]
pub fail_on: crate::interface::report::FailOn,
}
impl From<&InspectArgs> for crate::output::emitter::EmitConfig {
fn from(args: &InspectArgs) -> Self {
Self {
output: args.output.clone(),
format: args.format,
html: false,
open_browser: false,
}
}
}
impl From<&GraphArgs> for crate::output::emitter::EmitConfig {
fn from(args: &GraphArgs) -> Self {
Self {
output: args.output.clone(),
format: args.format,
html: args.html,
open_browser: true,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_language_valid_lowercase() {
assert_eq!(
parse_language("python"),
Ok(crate::language::LangId::Python)
);
assert_eq!(
parse_language("typescript"),
Ok(crate::language::LangId::TypeScript)
);
}
#[test]
fn parse_language_case_insensitive() {
assert_eq!(
parse_language("Python"),
Ok(crate::language::LangId::Python)
);
assert_eq!(
parse_language("TYPESCRIPT"),
Ok(crate::language::LangId::TypeScript)
);
}
#[test]
fn parse_language_invalid_returns_all_names() {
let err = parse_language("pytho").unwrap_err();
for id in crate::language::LangId::all() {
let name: &str = id.as_ref();
assert!(
err.contains(name),
"error {err:?} should list the valid name {name:?}"
);
}
}
#[test]
fn parse_language_empty_returns_err() {
assert!(parse_language("").is_err());
}
#[test]
fn opening_the_dashboard_is_an_opt_in() {
let parsed = Cli::try_parse_from(["meta-ast", "graph", "demo", "--html", "--open"]);
assert!(
parsed.is_ok(),
"graph --html --open parses: {:?}",
parsed.as_ref().err()
);
assert!(
matches!(parsed, Ok(Cli::Graph(ref args)) if args.open),
"the flag reaches the parsed arguments"
);
assert!(
Cli::try_parse_from(["meta-ast", "graph", "demo", "--open"]).is_err(),
"--open without --html is a usage error"
);
}
}