use crate::check::Check;
use crate::project::Project;
use crate::runner::RunStatus;
use crate::types::{CheckGroup, CheckLayer, CheckResult, CheckStatus, Confidence};
const GATE_FLAGS: &[&str] = &[
"--no-interactive",
"--non-interactive",
"-p",
"--print",
"--no-input",
"--batch",
"--headless",
"-y",
"--yes",
"--assume-yes",
];
const HELP_ON_BARE_MARKERS: &[&str] = &["Usage:", "USAGE:", "usage:"];
pub struct FlagExistenceCheck;
impl Check for FlagExistenceCheck {
fn id(&self) -> &str {
"p1-flag-existence"
}
fn group(&self) -> CheckGroup {
CheckGroup::P1
}
fn layer(&self) -> CheckLayer {
CheckLayer::Behavioral
}
fn covers(&self) -> &'static [&'static str] {
&["p1-must-no-interactive"]
}
fn applicable(&self, project: &Project) -> bool {
project.runner.is_some()
}
fn run(&self, project: &Project) -> anyhow::Result<CheckResult> {
let runner = project.runner_ref();
let bare = runner.run(&[], &[]);
let bare_output = format!("{}{}", bare.stdout, bare.stderr);
let help_on_bare = HELP_ON_BARE_MARKERS.iter().any(|m| bare_output.contains(m));
let stdin_clean_exit = matches!(bare.status, RunStatus::Ok);
if help_on_bare || stdin_clean_exit {
return Ok(CheckResult {
id: self.id().to_string(),
label: "Non-interactive gate flag advertised in --help".into(),
group: self.group(),
layer: self.layer(),
status: CheckStatus::Skip(
"target satisfies P1 via alternative gate (help-on-bare or stdin-primary)"
.into(),
),
confidence: Confidence::High,
});
}
let status = match project.help_output() {
None => CheckStatus::Skip("could not probe --help".into()),
Some(help) => {
let raw = help.raw();
if raw.trim().is_empty() {
CheckStatus::Skip(
"--help produced no output (likely non-English or unsupported)".into(),
)
} else if GATE_FLAGS.iter().any(|needle| contains_flag(raw, needle)) {
CheckStatus::Pass
} else {
CheckStatus::Warn(format!(
"no non-interactive flag found in --help; expected one of: {}",
GATE_FLAGS.join(", ")
))
}
}
};
Ok(CheckResult {
id: self.id().to_string(),
label: "Non-interactive gate flag advertised in --help".into(),
group: self.group(),
layer: self.layer(),
status,
confidence: Confidence::High,
})
}
}
fn contains_flag(haystack: &str, needle: &str) -> bool {
let mut rest = haystack;
while let Some(pos) = rest.find(needle) {
let before_ok = pos == 0 || !is_flag_name_char(rest.as_bytes()[pos - 1] as char);
let after_idx = pos + needle.len();
let after_ok =
after_idx >= rest.len() || !is_flag_name_char(rest.as_bytes()[after_idx] as char);
if before_ok && after_ok {
return true;
}
rest = &rest[after_idx..];
}
false
}
fn is_flag_name_char(c: char) -> bool {
c.is_ascii_alphanumeric() || c == '-' || c == '_'
}
#[cfg(test)]
fn check_flag_existence(help_raw: &str, bare_stdout: &str, bare_ok: bool) -> CheckStatus {
let help_on_bare = HELP_ON_BARE_MARKERS.iter().any(|m| bare_stdout.contains(m));
if help_on_bare || bare_ok {
return CheckStatus::Skip(
"target satisfies P1 via alternative gate (help-on-bare or stdin-primary)".into(),
);
}
if help_raw.trim().is_empty() {
return CheckStatus::Skip(
"--help produced no output (likely non-English or unsupported)".into(),
);
}
if GATE_FLAGS
.iter()
.any(|needle| contains_flag(help_raw, needle))
{
CheckStatus::Pass
} else {
CheckStatus::Warn(format!(
"no non-interactive flag found in --help; expected one of: {}",
GATE_FLAGS.join(", ")
))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn happy_path_batch_flag_in_help() {
let help = " --batch Run in batch mode.\n";
assert_eq!(check_flag_existence(help, "", false), CheckStatus::Pass);
}
#[test]
fn happy_path_short_print_flag() {
let help = " -p, --print Print output.\n";
assert_eq!(check_flag_existence(help, "", false), CheckStatus::Pass);
}
#[test]
fn skip_when_help_on_bare_invocation() {
let help = " --foo Do a thing.\n";
let result = check_flag_existence(help, "Usage: foo [OPTIONS]\n", false);
assert!(matches!(result, CheckStatus::Skip(_)));
}
#[test]
fn skip_when_stdin_clean_exit() {
let help = " --foo Do a thing.\n";
let result = check_flag_existence(help, "", true);
assert!(matches!(result, CheckStatus::Skip(_)));
}
#[test]
fn warn_when_no_gate_flag_and_no_alt_gate() {
let help = " --color When to color.\n --version Print version.\n";
match check_flag_existence(help, "", false) {
CheckStatus::Warn(msg) => assert!(msg.contains("--no-interactive")),
other => panic!("expected Warn, got {other:?}"),
}
}
#[test]
fn non_english_help_is_skipped() {
let help = "用法: outil\n选项:\n -H, --header 自定义请求头\n";
let result = check_flag_existence(help, "", false);
assert!(matches!(result, CheckStatus::Warn(_)));
let empty = "";
let result = check_flag_existence(empty, "", false);
assert!(matches!(result, CheckStatus::Skip(_)));
}
#[test]
fn word_boundary_rejects_partial_matches() {
let help = " --print-json Print as JSON.\n";
let result = check_flag_existence(help, "", false);
assert!(matches!(result, CheckStatus::Warn(_)));
}
#[test]
fn contains_flag_word_boundary() {
assert!(contains_flag("use --batch mode", "--batch"));
assert!(contains_flag(" --batch\n", "--batch"));
assert!(!contains_flag("--batching", "--batch"));
assert!(contains_flag("-p, --print", "-p"));
assert!(!contains_flag("-pr", "-p"));
}
}