use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use std::sync::OnceLock;
use clap::Parser;
use fallow_engine::validate;
use fallow_types::ANALYSIS_DIAGNOSTIC_LOG_TARGET;
use tracing_subscriber::filter::{LevelFilter, Targets};
use crate::cli_format::{Format, FormatConfig, format_from_env, parse_format_arg, resolve_format};
use crate::cli_telemetry::{TelemetryRun, record_run_epilogue};
use crate::{
Cli, Command, emit_known_failure, emit_known_failure_with_style, rayon_pool, regression,
report, runtime_support, security_help_target, similar_code_help_target, telemetry,
};
pub fn build_tracing_filter(rust_log: Option<&str>, quiet: bool) -> Targets {
match rust_log.map(str::trim) {
Some(value) => parse_targets_lossy(value),
None if quiet => Targets::new()
.with_default(LevelFilter::WARN)
.with_target(ANALYSIS_DIAGNOSTIC_LOG_TARGET, LevelFilter::OFF),
None => Targets::new().with_default(LevelFilter::WARN),
}
}
pub fn parse_targets_lossy(value: &str) -> Targets {
let readable: Vec<&str> = value
.split(',')
.map(str::trim)
.filter(|directive| !directive.is_empty() && directive.parse::<Targets>().is_ok())
.collect();
if readable.is_empty() {
return Targets::new();
}
readable.join(",").parse().unwrap_or_default()
}
#[must_use]
pub fn tracing_uses_ansi(
stderr_is_terminal: bool,
no_color: Option<&OsStr>,
clicolor_force: Option<&OsStr>,
) -> bool {
if clicolor_force.is_some_and(|value| value != "0") {
return true;
}
stderr_is_terminal && no_color.is_none_or(OsStr::is_empty)
}
pub fn setup_tracing(quiet: bool) {
use std::io::IsTerminal as _;
use tracing_subscriber::layer::SubscriberExt as _;
use tracing_subscriber::util::SubscriberInitExt as _;
let rust_log = std::env::var("RUST_LOG").ok();
let ansi = tracing_uses_ansi(
std::io::stderr().is_terminal(),
std::env::var_os("NO_COLOR").as_deref(),
std::env::var_os("CLICOLOR_FORCE").as_deref(),
);
tracing_subscriber::fmt()
.with_max_level(tracing_subscriber::filter::LevelFilter::TRACE)
.with_writer(std::io::stderr)
.with_ansi(ansi)
.with_target(false)
.with_timer(tracing_subscriber::fmt::time::uptime())
.finish()
.with(build_tracing_filter(rust_log.as_deref(), quiet))
.init();
}
pub fn validate_inputs(
cli: &Cli,
output: fallow_config::OutputFormat,
json_style: crate::json_style::JsonStyle,
) -> Result<(PathBuf, usize), ExitCode> {
validate_input_flags(cli, output, json_style)?;
let root = resolve_validated_root(cli, output, json_style)?;
validate_input_git_refs(cli, output, json_style)?;
let threads = cli
.threads
.unwrap_or_else(|| std::thread::available_parallelism().map_or(4, std::num::NonZero::get));
crate::process_clock::time(crate::process_clock::ProcessSpan::ThreadPool, || {
rayon_pool::configure_global_pool(threads);
});
Ok((root, threads))
}
fn validate_input_flags(
cli: &Cli,
output: fallow_config::OutputFormat,
json_style: crate::json_style::JsonStyle,
) -> Result<(), ExitCode> {
let validation_failure = |message: &str| {
emit_known_failure_with_style(
message,
2,
output,
json_style,
telemetry::FailureReason::Validation,
)
};
if matches!(
&cli.command,
Some(Command::AuditCache {
subcommand: crate::AuditCacheCli::Remove { .. }
})
) && cli.root.is_none()
{
return Err(validation_failure(
"`fallow audit-cache remove` requires an explicit `--root <path>`.",
));
}
if matches!(&cli.command, Some(Command::Security { .. }))
&& let Some(flag) = crate::unsupported_security_global(cli)
{
return Err(validation_failure(&format!(
"{flag} is not valid with `fallow security`."
)));
}
if cli.fail_on_parse_error
&& let Some(command) = match &cli.command {
Some(Command::Dupes { .. }) => Some("dupes"),
Some(Command::Fix { .. }) => Some("fix"),
_ => None,
}
{
return Err(validation_failure(&format!(
"--fail-on-parse-error is not valid with `fallow {command}`. Use it with dead-code, architecture, health, audit or the bare run."
)));
}
if let Some(patterns) = cli.group.as_deref() {
if !matches!(&cli.command, Some(Command::Health { .. })) {
return Err(validation_failure(
"--group is valid with `fallow health --group-by` only.",
));
}
for pattern in patterns {
if let Err(e) = validate::validate_no_control_chars(pattern, "--group") {
return Err(validation_failure(&e));
}
}
if let Err(e) = fallow_engine::health::validate_group_filter(patterns) {
return Err(validation_failure(&e));
}
}
if matches!(&cli.command, Some(Command::SimilarCode { .. }))
&& let Some(flag) = crate::similar_code_help::unsupported_similar_code_option(cli)
{
return Err(validation_failure(&format!(
"{flag} is not valid with this `fallow similar-code` command."
)));
}
if let Some(ref config_path) = cli.config
&& let Some(s) = config_path.to_str()
&& let Err(e) = validate::validate_no_control_chars(s, "--config")
{
return Err(validation_failure(&e));
}
if let Some(ref ws_patterns) = cli.workspace {
for ws in ws_patterns {
if let Err(e) = validate::validate_no_control_chars(ws, "--workspace") {
return Err(validation_failure(&e));
}
}
}
if let Some(ref git_ref) = cli.changed_since
&& let Err(e) = validate::validate_no_control_chars(git_ref, "--changed-since")
{
return Err(validation_failure(&e));
}
if let Some(ref git_ref) = cli.changed_workspaces
&& let Err(e) = validate::validate_no_control_chars(git_ref, "--changed-workspaces")
{
return Err(validation_failure(&e));
}
if cli.workspace.is_some() && cli.changed_workspaces.is_some() {
return Err(validation_failure(
"--workspace and --changed-workspaces are mutually exclusive. \
Pick one: --workspace for explicit package names/globs, \
--changed-workspaces for git-derived monorepo CI scoping.",
));
}
Ok(())
}
fn resolve_validated_root(
cli: &Cli,
output: fallow_config::OutputFormat,
json_style: crate::json_style::JsonStyle,
) -> Result<PathBuf, ExitCode> {
let raw_root = if let Some(root) = cli.root.clone() {
root
} else {
std::env::current_dir().map_err(|err| {
emit_known_failure_with_style(
&format!("Failed to get current directory: {err}"),
2,
output,
json_style,
telemetry::FailureReason::Config,
)
})?
};
validate::validate_root(&raw_root).map_err(|e| {
emit_known_failure_with_style(&e, 2, output, json_style, telemetry::FailureReason::Config)
})
}
fn validate_input_git_refs(
cli: &Cli,
output: fallow_config::OutputFormat,
json_style: crate::json_style::JsonStyle,
) -> Result<(), ExitCode> {
let validation_failure = |message: &str| {
emit_known_failure_with_style(
message,
2,
output,
json_style,
telemetry::FailureReason::Validation,
)
};
if let Some(ref git_ref) = cli.changed_since
&& let Err(e) = validate::validate_git_ref(git_ref)
{
return Err(validation_failure(&format!("invalid --changed-since: {e}")));
}
if let Some(ref git_ref) = cli.changed_workspaces
&& let Err(e) = validate::validate_git_ref(git_ref)
{
return Err(validation_failure(&format!(
"invalid --changed-workspaces: {e}"
)));
}
Ok(())
}
fn first_subcommand_token<I>(args: I) -> Option<String>
where
I: IntoIterator<Item = String>,
{
let mut skip_next = false;
for arg in args.into_iter().skip(1) {
if skip_next {
skip_next = false;
continue;
}
if arg == "--" {
break;
}
if arg.starts_with('-') {
skip_next = global_flag_consumes_next(&arg);
continue;
}
return Some(arg);
}
None
}
fn global_flag_consumes_next(arg: &str) -> bool {
!arg.contains('=') && global_value_options().iter().any(|name| name == arg)
}
fn global_value_options() -> &'static [String] {
static OPTIONS: OnceLock<Vec<String>> = OnceLock::new();
OPTIONS.get_or_init(|| {
<Cli as clap::CommandFactory>::command()
.get_arguments()
.filter(|arg| !arg.is_positional() && arg.get_action().takes_values())
.flat_map(option_spellings)
.collect()
})
}
fn option_spellings(arg: &clap::Arg) -> Vec<String> {
let longs = arg
.get_long()
.into_iter()
.chain(arg.get_all_aliases().unwrap_or_default())
.map(|long| format!("--{long}"));
let shorts = arg
.get_short()
.into_iter()
.chain(arg.get_all_short_aliases().unwrap_or_default())
.map(|short| format!("-{short}"));
longs.chain(shorts).collect()
}
fn args_use_legacy_check_alias<I>(args: I) -> bool
where
I: IntoIterator<Item = String>,
{
first_subcommand_token(args).as_deref() == Some("check")
}
fn args_invoked_review_alias<I>(args: I) -> bool
where
I: IntoIterator<Item = String>,
{
first_subcommand_token(args).as_deref() == Some("review")
}
fn raw_args_use_legacy_check_alias() -> bool {
args_use_legacy_check_alias(std::env::args_os().map(|arg| arg.to_string_lossy().into_owned()))
}
fn raw_args_invoked_review_alias() -> bool {
args_invoked_review_alias(std::env::args_os().map(|arg| arg.to_string_lossy().into_owned()))
}
fn warn_legacy_check_alias_if_needed(used_legacy_check_alias: bool, quiet: bool) {
if used_legacy_check_alias && !quiet {
eprintln!("fallow: `check` is deprecated; use `dead-code` instead.");
}
}
pub fn parse_cli_args() -> Result<(Cli, FormatConfig), ExitCode> {
let used_legacy_check_alias = raw_args_use_legacy_check_alias();
let mut cli = Cli::try_parse().map_err(|err| handle_cli_parse_error(&err))?;
warn_legacy_check_alias_if_needed(used_legacy_check_alias, cli.quiet);
if raw_args_invoked_review_alias()
&& let Some(Command::Audit { brief, .. }) = cli.command.as_mut()
{
*brief = true;
}
runtime_support::set_max_file_size_override(cli.max_file_size);
if let Some(workspaces) = cli.workspace.as_ref()
&& !workspaces.is_empty()
{
report::ci::pr_comment::set_workspace_marker_from_list(workspaces);
}
let fmt = resolve_format(&cli);
Ok((cli, fmt))
}
pub fn run_pre_dispatch_checks(
cli: &Cli,
root: &Path,
output: fallow_config::OutputFormat,
json_style: crate::json_style::JsonStyle,
quiet: bool,
telemetry_run: TelemetryRun,
) -> Result<regression::Tolerance, ExitCode> {
let fail = |code: ExitCode, reason: telemetry::FailureReason| {
record_run_epilogue(
telemetry_run,
code,
Some(reason),
cli.parent_run.as_deref(),
None,
)
};
if let Err(code) = init_cli_diff_filter(cli, root, output, quiet) {
return Err(fail(code, telemetry::FailureReason::Diff));
}
if (cli.ci || cli.fail_on_issues || cli.sarif_file.is_some() || cli.output_file.is_some())
&& command_rejects_output_gate(cli.command.as_ref())
{
let code = emit_known_failure_with_style(
"--ci, --fail-on-issues, --sarif-file, and --output-file are only valid with dead-code, architecture, dupes, health, security, or bare invocation",
2,
output,
json_style,
telemetry::FailureReason::Validation,
);
return Err(fail(code, telemetry::FailureReason::Validation));
}
if let Some(message) = sarif_file_without_sarif_error(cli) {
let code = emit_known_failure_with_style(
&message,
2,
output,
json_style,
telemetry::FailureReason::Validation,
);
return Err(fail(code, telemetry::FailureReason::Validation));
}
if let Some(message) = crate::write_scope::write_path_error(cli, root) {
let code = emit_known_failure_with_style(
&message,
2,
output,
json_style,
telemetry::FailureReason::Validation,
);
return Err(fail(code, telemetry::FailureReason::Validation));
}
if let Some(message) = global_filter_error(cli) {
let code = emit_known_failure_with_style(
message,
2,
output,
json_style,
telemetry::FailureReason::Validation,
);
return Err(fail(code, telemetry::FailureReason::Validation));
}
if cli.report_path_prefix.is_some()
&& !matches!(
output,
fallow_config::OutputFormat::GithubAnnotations
| fallow_config::OutputFormat::GithubSummary
| fallow_config::OutputFormat::CodeClimate
| fallow_config::OutputFormat::PrCommentGithub
| fallow_config::OutputFormat::PrCommentGitlab
| fallow_config::OutputFormat::ReviewGithub
| fallow_config::OutputFormat::ReviewGitlab
)
{
let code = emit_known_failure_with_style(
"--report-path-prefix is only valid with --format github-annotations, \
github-summary, codeclimate, pr-comment-github, pr-comment-gitlab, \
review-github, or review-gitlab",
2,
output,
json_style,
telemetry::FailureReason::Validation,
);
return Err(fail(code, telemetry::FailureReason::Validation));
}
report::github::set_report_path_prefix(cli.report_path_prefix.clone());
if matches!(
output,
fallow_config::OutputFormat::CodeClimate
| fallow_config::OutputFormat::PrCommentGithub
| fallow_config::OutputFormat::PrCommentGitlab
| fallow_config::OutputFormat::ReviewGithub
| fallow_config::OutputFormat::ReviewGitlab
) {
report::github::init_report_prefix(root);
}
if defers_tolerance_parse(cli.command.as_ref()) {
return Ok(regression::Tolerance::Absolute(0));
}
parse_cli_tolerance(cli, output)
.map_err(|code| fail(code, telemetry::FailureReason::Validation))
}
fn defers_tolerance_parse(command: Option<&Command>) -> bool {
matches!(command, Some(Command::List { .. } | Command::Workspaces))
}
fn handle_cli_parse_error(err: &clap::Error) -> ExitCode {
let exit_code = u8::try_from(err.exit_code()).unwrap_or(2);
if err.kind() == clap::error::ErrorKind::DisplayHelp
&& let Some(target) = similar_code_help_target(std::env::args_os().skip(1))
{
print!("{}", crate::render_similar_code_help(target));
return ExitCode::SUCCESS;
}
if err.kind() == clap::error::ErrorKind::DisplayHelp
&& let Some(target) = security_help_target(std::env::args_os().skip(1))
{
print!("{}", crate::render_security_help(target));
return ExitCode::SUCCESS;
}
if matches!(
parse_error_output_format(std::env::args_os().skip(1)),
fallow_config::OutputFormat::Json
) {
return crate::error::emit_error_with_style(
err.to_string().trim(),
exit_code,
fallow_config::OutputFormat::Json,
parse_error_json_style(std::env::args_os().skip(1)),
);
}
let _ = err.print();
ExitCode::from(exit_code)
}
fn parse_error_json_style<I, S>(args: I) -> crate::json_style::JsonStyle
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
if args
.into_iter()
.any(|arg| arg.as_ref() == OsStr::new("--pretty"))
{
crate::json_style::JsonStyle::Pretty
} else {
crate::json_style::JsonStyle::Compact
}
}
fn parse_error_output_format<I, S>(args: I) -> fallow_config::OutputFormat
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
let args: Vec<String> = args
.into_iter()
.map(|arg| arg.as_ref().to_string_lossy().into_owned())
.collect();
let mut iter = args.iter();
while let Some(arg) = iter.next() {
if matches!(arg.as_str(), "--format" | "--output" | "-f") {
return iter
.next()
.and_then(|value| parse_format_arg(value))
.unwrap_or(Format::Human)
.into();
}
let short_format_value = if arg.starts_with("--") {
None
} else {
arg.strip_prefix("-f")
};
if let Some(value) = arg
.strip_prefix("--format=")
.or_else(|| arg.strip_prefix("--output="))
.or(short_format_value)
.filter(|value| !value.is_empty())
{
return parse_format_arg(value).unwrap_or(Format::Human).into();
}
}
format_from_env().unwrap_or(Format::Human).into()
}
pub fn cli_has_bare_coverage_input(cli: &Cli) -> bool {
cli.coverage.is_some() || cli.coverage_root.is_some()
}
pub fn bare_coverage_subcommand_error_message() -> &'static str {
"`--coverage` and `--coverage-root` are bare combined-mode flags. Use `fallow health --coverage <coverage-final.json>` for standalone health analysis, or omit the subcommand to run combined mode."
}
pub fn cli_bare_combined_baseline_flag(cli: &Cli) -> Option<&'static str> {
if cli.dupes_baseline.is_some() {
return Some("--dupes-baseline");
}
if cli.health_baseline.is_some() {
return Some("--health-baseline");
}
None
}
pub fn bare_combined_baseline_subcommand_error_message(flag: &str) -> String {
format!(
"`{flag}` is a bare combined-mode flag and has no effect before a subcommand. Use `fallow audit {flag} <file>`, `fallow dupes --baseline <file>` or `fallow health --baseline <file>`, or omit the subcommand to run combined mode."
)
}
fn sarif_file_without_sarif_error(cli: &Cli) -> Option<String> {
cli.sarif_file.as_ref()?;
let command = match cli.command.as_ref() {
None => {
let (run_check, _, _) = crate::combined::resolve_analyses(&cli.only, &cli.skip);
return (!run_check).then(|| {
"This run does not include the dead-code analysis, which writes the SARIF file, so `--sarif-file` has no effect. Use `--format sarif` with `--output-file`, or include dead-code in the run.".to_string()
});
}
Some(Command::Dupes { .. }) => "dupes",
Some(Command::Health { .. }) => "health",
Some(_) => return None,
};
Some(format!(
"`fallow {command}` does not write a SARIF file, so `--sarif-file` has no effect. Use `--format sarif` with `--output-file`, or use `--sarif-file` with bare `fallow`, `fallow dead-code`, `fallow architecture` or `fallow security`."
))
}
pub fn cli_global_baseline_flag(cli: &Cli) -> Option<&'static str> {
if cli.baseline.is_some() {
Some("--baseline")
} else if cli.save_baseline.is_some() {
Some("--save-baseline")
} else if cli.fail_on_baseline_growth {
Some("--fail-on-baseline-growth")
} else if cli.baseline_base.is_some() {
Some("--baseline-base")
} else {
None
}
}
pub fn command_without_global_baseline(command: &Command) -> Option<&'static str> {
match command {
Command::Check { .. }
| Command::Architecture { .. }
| Command::Dupes { .. }
| Command::Health { .. }
| Command::Audit { .. }
| Command::Security { .. }
| Command::Doctor
| Command::Baselines { .. }
| Command::SimilarCode { .. } => None,
Command::Watch { .. } => Some("watch"),
Command::TypeAware { .. } => Some("type-aware"),
Command::Inspect { .. } => Some("inspect"),
Command::Trace { .. } => Some("trace"),
Command::TraceError { .. } => Some("trace-error"),
Command::Fix { .. } => Some("fix"),
Command::Init { .. } => Some("init"),
Command::Hooks { .. } => Some("hooks"),
Command::Agent { .. } => Some("agent"),
Command::Ci { .. } => Some("ci"),
Command::ConfigSchema => Some("config-schema"),
Command::PluginSchema => Some("plugin-schema"),
Command::PluginCheck => Some("plugin-check"),
Command::RulePackSchema => Some("rule-pack-schema"),
Command::RulePack { .. } => Some("rule-pack"),
Command::Guard { .. } => Some("guard"),
Command::Config { .. } => Some("config"),
Command::Recommend => Some("recommend"),
Command::List { .. } => Some("list"),
Command::Workspaces => Some("workspaces"),
Command::Flags { .. } => Some("flags"),
Command::Suppressions { .. } => Some("suppressions"),
Command::Explain { .. } => Some("explain"),
Command::AuditCache { .. } => Some("audit-cache"),
Command::DecisionSurface { .. } => Some("decision-surface"),
Command::Impact { .. } => Some("impact"),
Command::Report { .. } => Some("report"),
Command::Schema => Some("schema"),
Command::CiTemplate { .. } => Some("ci-template"),
Command::Migrate { .. } => Some("migrate"),
Command::License { .. } => Some("license"),
Command::Telemetry { .. } => Some("telemetry"),
Command::Coverage { .. } => Some("coverage"),
Command::SetupHooks { .. } => Some("setup-hooks"),
Command::Viz { .. } => Some("viz"),
Command::Mutation { .. } => Some("mutation"),
}
}
pub fn global_baseline_subcommand_error_message(command: &str, flag: &str) -> String {
format!(
"`fallow {command}` does not load or save a baseline, so `{flag}` has no effect. Use `{flag}` with bare `fallow`, `fallow dead-code`, `fallow architecture`, `fallow dupes` or `fallow health`, or use `fallow audit --dead-code-baseline`, `--health-baseline` or `--dupes-baseline`."
)
}
pub fn reject_global_baseline_flags(cli: &Cli, format: &FormatConfig) -> Option<ExitCode> {
let name = cli
.command
.as_ref()
.and_then(command_without_global_baseline)?;
let flag = cli_global_baseline_flag(cli)?;
Some(crate::error::emit_error_with_style(
&global_baseline_subcommand_error_message(name, flag),
2,
format.output,
format.json_style,
))
}
fn command_rejects_output_gate(command: Option<&Command>) -> bool {
matches!(
command,
Some(
Command::Init { .. }
| Command::ConfigSchema
| Command::PluginSchema
| Command::PluginCheck
| Command::RulePackSchema
| Command::Schema
| Command::Explain { .. }
| Command::CiTemplate { .. }
| Command::Config { .. }
| Command::Ci { .. }
| Command::List { .. }
| Command::Inspect { .. }
| Command::Trace { .. }
| Command::Flags { .. }
| Command::Migrate { .. }
| Command::License { .. }
| Command::Coverage { .. }
| Command::Hooks { .. }
| Command::SetupHooks { .. }
| Command::AuditCache { .. }
)
)
}
fn global_filter_error(cli: &Cli) -> Option<&'static str> {
if (!cli.only.is_empty() || !cli.skip.is_empty()) && cli.command.is_some() {
return Some(
"--only and --skip can only be used without a subcommand. For audit, pass them after `audit`",
);
}
if (cli.production_dead_code || cli.production_health || cli.production_dupes)
&& cli.command.is_some()
{
return Some(
"--production-dead-code, --production-health, and --production-dupes can only be used without a subcommand. For audit, pass them after `audit`",
);
}
if !cli.only.is_empty() && !cli.skip.is_empty() {
return Some("--only and --skip are mutually exclusive");
}
None
}
fn parse_cli_tolerance(
cli: &Cli,
output: fallow_config::OutputFormat,
) -> Result<regression::Tolerance, ExitCode> {
regression::Tolerance::parse(&cli.tolerance).map_err(|e| {
emit_known_failure(
&format!("invalid --tolerance: {e}"),
2,
output,
telemetry::FailureReason::Validation,
)
})
}
fn init_cli_diff_filter(
cli: &Cli,
root: &Path,
output: fallow_config::OutputFormat,
quiet: bool,
) -> Result<(), ExitCode> {
let diff_source = report::ci::diff_filter::resolve_diff_source(
cli.diff_file.as_deref(),
cli.diff_stdin,
root,
)
.map_err(|msg| emit_known_failure(&msg, 2, output, telemetry::FailureReason::Diff))?;
if diff_source.is_some() && cli.changed_since.is_some() && !quiet {
eprintln!(
"fallow: --diff-file precedes --changed-since for line-level \
filtering; --changed-since still scopes file discovery. Drop \
one of them to disable this combination."
);
}
let suppress_warnings = quiet
&& matches!(
diff_source,
Some(report::ci::diff_filter::DiffSource::EnvVar(_)) | None
);
let candidate_bases = diff_source
.as_ref()
.map(|_| fallow_engine::diff_source::diff_base_candidates(root))
.unwrap_or_default();
let _ = report::ci::diff_filter::init_shared_diff(
diff_source.as_ref(),
root,
&candidate_bases,
suppress_warnings,
);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tracing_filter_defaults_to_warn_without_env() {
let filter = build_tracing_filter(None, false);
assert!(filter.would_enable("fallow_core", &tracing::Level::WARN));
assert!(!filter.would_enable("fallow_core", &tracing::Level::INFO));
assert!(filter.would_enable(ANALYSIS_DIAGNOSTIC_LOG_TARGET, &tracing::Level::WARN));
}
#[test]
fn a_quiet_run_hides_only_analysis_diagnostic_warnings() {
let filter = build_tracing_filter(None, true);
assert!(!filter.would_enable(ANALYSIS_DIAGNOSTIC_LOG_TARGET, &tracing::Level::WARN));
assert!(!filter.would_enable(ANALYSIS_DIAGNOSTIC_LOG_TARGET, &tracing::Level::ERROR));
assert!(filter.would_enable("fallow_config", &tracing::Level::WARN));
assert!(!filter.would_enable("fallow_core", &tracing::Level::INFO));
}
#[test]
fn an_explicit_env_filter_wins_over_quiet() {
let filter = build_tracing_filter(Some("warn"), true);
assert!(filter.would_enable(ANALYSIS_DIAGNOSTIC_LOG_TARGET, &tracing::Level::WARN));
}
#[test]
fn tracing_filter_respects_explicit_env_directives() {
let filter = build_tracing_filter(Some("info"), false);
assert!(filter.would_enable("fallow_core", &tracing::Level::INFO));
assert!(!filter.would_enable("fallow_core", &tracing::Level::DEBUG));
let filter = build_tracing_filter(Some("fallow=debug"), false);
assert!(filter.would_enable("fallow_core::lib", &tracing::Level::DEBUG));
assert!(!filter.would_enable("oxc_resolver", &tracing::Level::ERROR));
}
#[test]
fn tracing_filter_skips_a_directive_that_it_cannot_read() {
let directives = "fallow[span{id=1}]=trace,fallow_graph=debug,warn";
let filter = build_tracing_filter(Some(directives), false);
assert!(filter.would_enable("fallow_graph", &tracing::Level::DEBUG));
assert!(filter.would_enable("oxc_resolver", &tracing::Level::WARN));
assert!(!filter.would_enable("fallow_core", &tracing::Level::TRACE));
}
#[test]
fn tracing_filter_treats_empty_env_as_off() {
for value in ["", " "] {
let filter = build_tracing_filter(Some(value), false);
assert!(!filter.would_enable("fallow_core", &tracing::Level::ERROR));
}
}
#[test]
fn tracing_color_needs_a_terminal_and_no_no_color() {
assert!(tracing_uses_ansi(true, None, None));
assert!(tracing_uses_ansi(true, Some(OsStr::new("")), None));
assert!(!tracing_uses_ansi(false, None, None));
assert!(!tracing_uses_ansi(true, Some(OsStr::new("1")), None));
assert!(tracing_uses_ansi(false, None, Some(OsStr::new("1"))));
assert!(tracing_uses_ansi(
true,
Some(OsStr::new("1")),
Some(OsStr::new("1"))
));
assert!(!tracing_uses_ansi(false, None, Some(OsStr::new("0"))));
}
#[test]
fn global_baseline_hint_names_every_baseline_command() {
let message = global_baseline_subcommand_error_message("inspect", "--baseline");
for command in [
"`fallow dead-code`",
"`fallow architecture`",
"`fallow dupes`",
"`fallow health`",
] {
assert!(message.contains(command), "{command}: {message}");
}
}
#[test]
fn legacy_check_alias_detection_ignores_option_values() {
assert!(args_use_legacy_check_alias(vec![
"fallow".to_string(),
"check".to_string(),
"--summary".to_string(),
]));
assert!(!args_use_legacy_check_alias(vec![
"fallow".to_string(),
"--root".to_string(),
"check".to_string(),
"dead-code".to_string(),
]));
assert!(!args_use_legacy_check_alias(vec![
"fallow".to_string(),
"dead-code".to_string(),
"--file".to_string(),
"check".to_string(),
]));
}
fn argv(args: &[&str]) -> Vec<String> {
std::iter::once("fallow")
.chain(args.iter().copied())
.map(str::to_string)
.collect()
}
#[test]
fn boolean_global_flags_do_not_hide_the_subcommand() {
for flag in [
"--dupes-skip-local",
"--dupes-cross-language",
"--dupes-ignore-imports",
] {
assert!(
args_use_legacy_check_alias(argv(&[flag, "check"])),
"{flag} check"
);
assert!(
args_invoked_review_alias(argv(&[flag, "review"])),
"{flag} review"
);
}
}
#[test]
fn subcommand_scan_matches_every_top_level_clap_option() {
let command = <Cli as clap::CommandFactory>::command();
for arg in command.get_arguments().filter(|arg| !arg.is_positional()) {
for flag in option_spellings(arg) {
if arg.get_action().takes_values() {
assert!(
!args_use_legacy_check_alias(argv(&[&flag, "check", "dead-code"])),
"{flag} takes a value, so `check` after it is that value"
);
assert!(
args_use_legacy_check_alias(argv(&[&flag, "value", "check"])),
"{flag} value check"
);
} else {
assert!(
args_use_legacy_check_alias(argv(&[&flag, "check"])),
"{flag} takes no value, so `check` after it is the subcommand"
);
}
}
}
}
}