use serde::{Deserialize, Serialize};
macro_rules! for_each_flag_config_field {
($callback:ident) => {
$callback!(
summary_only,
diagnostics_only,
dedupe,
verbose,
pedantic,
errors_only,
packages_only,
fail_fast,
no_prune_implied,
prune_implied,
show_pruned,
aggregate_targets,
no_targets,
install_missing_targets,
only_packages_with_lib_target,
);
};
}
macro_rules! for_each_simple_resolved_flag_field {
($callback:ident) => {
$callback!(
summary_only,
verbose,
pedantic,
errors_only,
packages_only,
fail_fast,
show_pruned,
aggregate_targets,
no_targets,
install_missing_targets,
only_packages_with_lib_target,
);
};
}
macro_rules! define_flag_keys {
($($field:ident),+ $(,)?) => {
pub(crate) const FLAG_KEYS: &[&str] = &[$(stringify!($field),)+ "dedup"];
};
}
for_each_flag_config_field!(define_flag_keys);
#[derive(Serialize, Deserialize, Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct FlagConfig {
#[serde(default)]
pub summary_only: Option<bool>,
#[serde(default)]
pub diagnostics_only: Option<bool>,
#[serde(default, alias = "dedup")]
pub dedupe: Option<bool>,
#[serde(default)]
pub verbose: Option<bool>,
#[serde(default)]
pub pedantic: Option<bool>,
#[serde(default)]
pub errors_only: Option<bool>,
#[serde(default)]
pub packages_only: Option<bool>,
#[serde(default)]
pub fail_fast: Option<bool>,
#[serde(default)]
pub no_prune_implied: Option<bool>,
#[serde(default)]
pub prune_implied: Option<bool>,
#[serde(default)]
pub show_pruned: Option<bool>,
#[serde(default)]
pub aggregate_targets: Option<bool>,
#[serde(default)]
pub no_targets: Option<bool>,
#[serde(default)]
pub install_missing_targets: Option<bool>,
#[serde(default)]
pub only_packages_with_lib_target: Option<bool>,
}
impl FlagConfig {
pub(crate) fn validate(self) -> color_eyre::eyre::Result<()> {
if self.no_prune_implied.is_some() && self.prune_implied.is_some() {
color_eyre::eyre::bail!(
"`no_prune_implied` and `prune_implied` are contradictory; configure only one spelling in the same scope"
);
}
if self.dedupe == Some(true) && self.diagnostics_only == Some(false) {
color_eyre::eyre::bail!(
"`dedupe = true` requires diagnostics-only output; do not set `diagnostics_only = false` in the same scope"
);
}
Ok(())
}
pub fn overlay(&mut self, other: Self) {
macro_rules! overlay_fields {
($($field:ident),+ $(,)?) => {
$(overlay_bool(&mut self.$field, other.$field);)+
};
}
for_each_flag_config_field!(overlay_fields);
if other.diagnostics_only == Some(false) && other.dedupe != Some(true) {
self.dedupe = Some(false);
}
if other.dedupe == Some(true)
&& other.diagnostics_only.is_none()
&& self.diagnostics_only == Some(false)
{
self.diagnostics_only = None;
}
if let Some(value) = other.no_prune_implied {
self.no_prune_implied = Some(value);
self.prune_implied = None;
}
if let Some(value) = other.prune_implied {
self.prune_implied = Some(value);
self.no_prune_implied = None;
}
}
#[must_use]
pub fn requests_diagnostics(self) -> bool {
self.diagnostics_only == Some(true) || self.dedupe == Some(true)
}
#[must_use]
pub fn mentions_diagnostics(self) -> bool {
self.diagnostics_only.is_some() || self.dedupe.is_some()
}
}
fn overlay_bool(target: &mut Option<bool>, source: Option<bool>) {
if source.is_some() {
*target = source;
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct ResolvedFlags {
pub summary_only: bool,
pub diagnostics_only: bool,
pub dedupe: bool,
pub verbose: bool,
pub pedantic: bool,
pub errors_only: bool,
pub packages_only: bool,
pub fail_fast: bool,
pub no_prune_implied: bool,
pub show_pruned: bool,
pub aggregate_targets: bool,
pub no_targets: bool,
pub install_missing_targets: bool,
pub only_packages_with_lib_target: bool,
}
impl ResolvedFlags {
#[must_use]
pub fn from_config(config: FlagConfig) -> Self {
let mut out = Self::default();
macro_rules! set_defaulted_fields {
($($field:ident),+ $(,)?) => {
$(out.$field = config.$field.unwrap_or(false);)+
};
}
for_each_simple_resolved_flag_field!(set_defaulted_fields);
let no_prune_implied = config
.no_prune_implied
.or_else(|| config.prune_implied.map(|enabled| !enabled))
.unwrap_or(false);
let dedupe = config.dedupe.unwrap_or(false);
out.diagnostics_only = config.diagnostics_only.unwrap_or(false) || dedupe;
out.dedupe = dedupe;
out.no_prune_implied = no_prune_implied;
out
}
pub(crate) fn try_from_config(config: FlagConfig) -> color_eyre::eyre::Result<Self> {
if config.dedupe == Some(true) && config.diagnostics_only == Some(false) {
color_eyre::eyre::bail!(
"`dedupe = true` requires `diagnostics_only = true`; remove `diagnostics_only = false` or set `dedupe = false`"
);
}
Ok(Self::from_config(config))
}
}
pub(crate) fn gated_plain_diagnostics(
mut flags: FlagConfig,
ignored_diagnostics_config: &mut bool,
) -> FlagConfig {
if flags.requests_diagnostics() {
*ignored_diagnostics_config = true;
} else if flags.mentions_diagnostics() {
*ignored_diagnostics_config = false;
}
if flags.diagnostics_only == Some(true) {
flags.diagnostics_only = None;
}
if flags.dedupe == Some(true) {
flags.dedupe = None;
}
flags
}
pub(crate) fn combine_flag_configs<'a>(
field_prefix: Option<&str>,
source_kind: &str,
entries: impl IntoIterator<Item = (&'a str, FlagConfig)>,
) -> color_eyre::eyre::Result<FlagConfig> {
let entries: Vec<_> = entries.into_iter().collect();
let mut out = FlagConfig::default();
macro_rules! combine {
($($field:ident),+ $(,)?) => {
$(
let field_name = if let Some(prefix) = field_prefix {
format!("{prefix}.{}", stringify!($field))
} else {
stringify!($field).to_string()
};
if let Some(value) = combine_bool(
&field_name,
source_kind,
&entries,
|flags| flags.$field,
)? {
out.$field = Some(value);
}
)+
};
}
for_each_flag_config_field!(combine);
out.validate()?;
Ok(out)
}
fn combine_scalar<'a, T, V: PartialEq>(
name: &str,
source_kind: &str,
entries: &'a [(&'a str, T)],
get: impl Fn(&'a T) -> Option<V>,
) -> color_eyre::eyre::Result<Option<V>> {
let mut out: Option<V> = None;
for (expr, item) in entries {
if let Some(value) = get(item) {
match &out {
None => out = Some(value),
Some(existing) if *existing == value => {}
Some(_) => {
color_eyre::eyre::bail!(
"conflicting values for `{name}` in {source_kind} `{expr}`"
);
}
}
}
}
Ok(out)
}
pub(crate) fn combine_bool<T>(
name: &str,
source_kind: &str,
entries: &[(&str, T)],
get: impl Fn(&T) -> Option<bool>,
) -> color_eyre::eyre::Result<Option<bool>> {
combine_scalar(name, source_kind, entries, get)
}
pub(crate) fn combine_u128<T>(
name: &str,
source_kind: &str,
entries: &[(&str, T)],
get: impl Fn(&T) -> Option<u128>,
) -> color_eyre::eyre::Result<Option<u128>> {
combine_scalar(name, source_kind, entries, get)
}
pub(crate) fn combine_driver<T>(
name: &str,
source_kind: &str,
entries: &[(&str, T)],
get: impl Fn(&T) -> Option<&str>,
) -> color_eyre::eyre::Result<Option<String>> {
for (expr, item) in entries {
if get(item).is_some_and(|value| value.trim().is_empty()) {
color_eyre::eyre::bail!("`{name}` must not be empty in {source_kind} `{expr}`");
}
}
let combined = combine_scalar(name, source_kind, entries, |item| get(item).map(str::trim))?;
Ok(combined.map(ToString::to_string))
}
#[cfg(test)]
mod tests {
use super::{FlagConfig, ResolvedFlags};
use std::collections::BTreeSet;
#[test]
fn flag_subset_macros_match_documented_special_cases() {
macro_rules! define_master_fields {
($($field:ident),+ $(,)?) => {
fn master_fields() -> BTreeSet<&'static str> {
[$(stringify!($field),)+].into_iter().collect()
}
};
}
macro_rules! define_simple_resolved_fields {
($($field:ident),+ $(,)?) => {
fn simple_resolved_fields() -> BTreeSet<&'static str> {
[$(stringify!($field),)+].into_iter().collect()
}
};
}
for_each_flag_config_field!(define_master_fields);
for_each_simple_resolved_flag_field!(define_simple_resolved_fields);
let master = master_fields();
let simple_resolved = simple_resolved_fields();
let mut expected_simple_resolved = master.clone();
expected_simple_resolved.remove("prune_implied");
expected_simple_resolved.remove("diagnostics_only");
expected_simple_resolved.remove("dedupe");
expected_simple_resolved.remove("no_prune_implied");
assert_eq!(simple_resolved, expected_simple_resolved);
}
#[test]
fn flag_overlay_preserves_diagnostics_couplings() -> color_eyre::eyre::Result<()> {
let mut flags = FlagConfig {
diagnostics_only: Some(false),
..FlagConfig::default()
};
flags.overlay(FlagConfig {
dedupe: Some(true),
..FlagConfig::default()
});
let resolved = ResolvedFlags::try_from_config(flags)?;
assert!(resolved.diagnostics_only);
assert!(resolved.dedupe);
let mut flags = FlagConfig {
dedupe: Some(true),
..FlagConfig::default()
};
flags.overlay(FlagConfig {
diagnostics_only: Some(false),
..FlagConfig::default()
});
let resolved = ResolvedFlags::try_from_config(flags)?;
assert!(!resolved.diagnostics_only);
assert!(!resolved.dedupe);
Ok(())
}
#[test]
fn contradictory_prune_spellings_in_one_scope_error() {
let err = FlagConfig {
no_prune_implied: Some(true),
prune_implied: Some(true),
..FlagConfig::default()
}
.validate()
.expect_err("contradictory prune spelling should fail");
assert!(err.to_string().contains("no_prune_implied"));
}
#[test]
fn combine_driver_trims_whitespace_and_conflicts() -> color_eyre::eyre::Result<()> {
let entries = [("cfg(a)", Some("cross")), ("cfg(b)", Some("cross "))];
let out = super::combine_driver("driver", "target override", &entries, |value| *value)?;
assert_eq!(out.as_deref(), Some("cross"));
let entries = [
("cfg(a)", Some("cross")),
("cfg(b)", Some("cargo-zigbuild")),
];
assert!(
super::combine_driver("driver", "target override", &entries, |value| *value).is_err()
);
Ok(())
}
}