use regex::Regex;
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FlagClass {
ModeledInKey,
ParserHandled,
CapturedByProbe,
PreprocessorCaptured,
NoObjectEffect,
}
#[derive(Debug, Clone, Copy)]
pub enum Matcher {
Exact(&'static str),
Prefix(&'static str),
Regex(&'static str),
}
#[derive(Debug, Clone, Copy)]
pub struct FlagSpec {
pub matcher: Matcher,
pub class: FlagClass,
pub source: &'static str,
}
pub fn build_regex_cache(table: &'static [FlagSpec]) -> HashMap<&'static str, Regex> {
let mut map = HashMap::with_capacity(table.len());
for spec in table {
if let Matcher::Regex(pat) = spec.matcher {
let anchored = format!("^(?:{pat})$");
let re = Regex::new(&anchored).unwrap_or_else(|e| {
panic!(
"compiler/flags: invalid regex `{pat}` from {}: {e}",
spec.source
)
});
map.insert(pat, re);
}
}
map
}
pub fn classify_against(
arg: &str,
table: &'static [FlagSpec],
regex_cache: &HashMap<&'static str, Regex>,
) -> Option<FlagClass> {
if !arg.starts_with('-') {
return Some(FlagClass::NoObjectEffect);
}
for spec in table {
let matched = match &spec.matcher {
Matcher::Exact(s) => arg == *s,
Matcher::Prefix(s) => arg.starts_with(*s),
Matcher::Regex(pat) => regex_cache
.get(pat)
.map(|re| re.is_match(arg))
.unwrap_or_else(|| {
panic!(
"compiler/flags: regex `{pat}` ({}) not in cache",
spec.source
)
}),
};
if matched {
return Some(spec.class);
}
}
None
}
#[cfg(test)]
#[doc(hidden)]
pub fn assert_table_regexes_compile(table: &'static [FlagSpec]) {
for spec in table {
if let Matcher::Regex(pat) = spec.matcher {
let anchored = format!("^(?:{pat})$");
Regex::new(&anchored).unwrap_or_else(|e| {
panic!(
"compiler/flags: invalid regex `{pat}` from {}: {e}",
spec.source
)
});
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::OnceLock;
static TEST_TABLE: &[FlagSpec] = &[
FlagSpec {
matcher: Matcher::Exact("-fPIC"),
class: FlagClass::ModeledInKey,
source: "tests",
},
FlagSpec {
matcher: Matcher::Prefix("-D"),
class: FlagClass::PreprocessorCaptured,
source: "tests",
},
FlagSpec {
matcher: Matcher::Exact("-E"),
class: FlagClass::ParserHandled,
source: "tests",
},
FlagSpec {
matcher: Matcher::Regex(r"-W[^,]*"),
class: FlagClass::NoObjectEffect,
source: "tests — warnings; excludes `-Wl,*`/`-Wa,*`/`-Wp,*` passthrough forms",
},
];
fn cache() -> &'static HashMap<&'static str, Regex> {
static CACHE: OnceLock<HashMap<&'static str, Regex>> = OnceLock::new();
CACHE.get_or_init(|| build_regex_cache(TEST_TABLE))
}
#[test]
fn positional_arg_classifies_as_no_effect() {
assert_eq!(
classify_against("foo.c", TEST_TABLE, cache()),
Some(FlagClass::NoObjectEffect)
);
assert_eq!(
classify_against("include", TEST_TABLE, cache()),
Some(FlagClass::NoObjectEffect)
);
}
#[test]
fn exact_matcher_matches_only_the_literal() {
assert_eq!(
classify_against("-fPIC", TEST_TABLE, cache()),
Some(FlagClass::ModeledInKey)
);
assert_eq!(classify_against("-fPIC=1", TEST_TABLE, cache()), None);
assert_eq!(classify_against("-fPI", TEST_TABLE, cache()), None);
}
#[test]
fn exact_matcher_can_return_parser_handled() {
assert_eq!(
classify_against("-E", TEST_TABLE, cache()),
Some(FlagClass::ParserHandled)
);
}
#[test]
fn prefix_matcher_matches_the_family() {
assert_eq!(
classify_against("-DFOO=1", TEST_TABLE, cache()),
Some(FlagClass::PreprocessorCaptured)
);
assert_eq!(
classify_against("-D", TEST_TABLE, cache()),
Some(FlagClass::PreprocessorCaptured)
);
assert_eq!(classify_against("-d", TEST_TABLE, cache()), None);
}
#[test]
fn regex_matcher_is_anchored_and_excludes_by_pattern() {
assert_eq!(
classify_against("-Wall", TEST_TABLE, cache()),
Some(FlagClass::NoObjectEffect)
);
assert_eq!(
classify_against("-Wno-unused", TEST_TABLE, cache()),
Some(FlagClass::NoObjectEffect)
);
assert_eq!(classify_against("-Wl,-no_pie", TEST_TABLE, cache()), None);
assert_eq!(classify_against("-Wa,--64", TEST_TABLE, cache()), None);
assert_eq!(classify_against("-Wp,-MD,foo.d", TEST_TABLE, cache()), None);
}
#[test]
fn unknown_flag_classifies_as_none() {
assert_eq!(classify_against("-fmadeup", TEST_TABLE, cache()), None);
assert_eq!(classify_against("--unknown", TEST_TABLE, cache()), None);
}
#[test]
fn ci_validator_accepts_valid_table() {
assert_table_regexes_compile(TEST_TABLE);
}
#[test]
fn anchoring_survives_top_level_alternation_in_pattern() {
static ALT_TABLE: &[FlagSpec] = &[FlagSpec {
matcher: Matcher::Regex(r"-foo|-bar"),
class: FlagClass::NoObjectEffect,
source: "tests — alternation anchoring",
}];
let cache = build_regex_cache(ALT_TABLE);
assert_eq!(
classify_against("-foo", ALT_TABLE, &cache),
Some(FlagClass::NoObjectEffect)
);
assert_eq!(
classify_against("-bar", ALT_TABLE, &cache),
Some(FlagClass::NoObjectEffect)
);
assert_eq!(classify_against("-foobar", ALT_TABLE, &cache), None);
assert_eq!(classify_against("-foozilla", ALT_TABLE, &cache), None);
assert_eq!(classify_against("-x-bar", ALT_TABLE, &cache), None);
assert_eq!(classify_against("--bar", ALT_TABLE, &cache), None);
}
}