use crate::error::{EachError, PlaceholderMisuseError, WorkspaceRustVersionError};
use crate::plan::Mode;
const PER_PACKAGE_TOKENS: [&str; 4] = ["{name}", "{spec}", "{version}", "{manifest}"];
const TARGET_TOKEN: &str = "{target}";
const PACKAGES_TOKEN: &str = "{packages}";
const WORKSPACE_RUST_VERSION_TOKEN: &str = "{workspace-rust-version}";
#[derive(Debug, Clone)]
pub(crate) enum Placeholders {
Package {
name: String,
spec: String,
version: String,
manifest: String,
workspace_rust_version: Option<String>,
},
Target {
name: String,
spec: String,
version: String,
manifest: String,
target: String,
workspace_rust_version: Option<String>,
},
Once {
packages: Vec<String>,
workspace_rust_version: Option<String>,
},
}
impl Placeholders {
fn workspace_rust_version(&self) -> Option<&str> {
match self {
Self::Package {
workspace_rust_version, ..
}
| Self::Target {
workspace_rust_version, ..
}
| Self::Once {
workspace_rust_version, ..
} => workspace_rust_version.as_deref(),
}
}
}
fn replace_arg<'a>(arg: &str, placeholders: &'a Placeholders, mut replacements: Vec<(&'static str, &'a str)>) -> Result<String, EachError> {
if arg.contains(WORKSPACE_RUST_VERSION_TOKEN) {
let version = placeholders.workspace_rust_version().ok_or_else(|| {
WorkspaceRustVersionError::new("the command uses the placeholder but its root value was not resolved".to_owned())
})?;
replacements.push((WORKSPACE_RUST_VERSION_TOKEN, version));
}
let mut rest = arg;
let mut replaced = String::with_capacity(arg.len());
while let Some((offset, token, value)) = replacements
.iter()
.filter_map(|&(token, value)| rest.find(token).map(|offset| (offset, token, value)))
.min_by_key(|&(offset, _, _)| offset)
{
let (literal, token_and_rest) = rest.split_at(offset);
let (_token, remaining) = token_and_rest.split_at(token.len());
replaced.push_str(literal);
replaced.push_str(value);
rest = remaining;
}
replaced.push_str(rest);
Ok(replaced)
}
#[must_use]
pub(crate) fn uses_workspace_rust_version(args: &[String]) -> bool {
args.iter().any(|arg| arg.contains(WORKSPACE_RUST_VERSION_TOKEN))
}
pub(crate) fn validate_placeholders(args: &[String], mode: Mode) -> Result<(), EachError> {
for arg in args {
if mode == Mode::Once {
if let Some(token) = PER_PACKAGE_TOKENS.iter().find(|t| arg.contains(**t)) {
return Err(
PlaceholderMisuseError::new((*token).to_owned(), "per-package token is not valid in --once mode".to_owned()).into(),
);
}
if arg.contains(TARGET_TOKEN) {
return Err(PlaceholderMisuseError::new(
TARGET_TOKEN.to_owned(),
"per-target token is not valid in --once mode".to_owned(),
)
.into());
}
if arg != PACKAGES_TOKEN && arg.contains(PACKAGES_TOKEN) {
return Err(PlaceholderMisuseError::new(
PACKAGES_TOKEN.to_owned(),
"must stand alone as a whole argument (it expands to multiple tokens)".to_owned(),
)
.into());
}
} else {
if arg.contains(PACKAGES_TOKEN) {
return Err(PlaceholderMisuseError::new(PACKAGES_TOKEN.to_owned(), "only valid in --once mode".to_owned()).into());
}
if mode == Mode::PerPackage && arg.contains(TARGET_TOKEN) {
return Err(PlaceholderMisuseError::new(TARGET_TOKEN.to_owned(), "only valid in per-target mode".to_owned()).into());
}
}
}
Ok(())
}
pub(crate) fn substitute(args: &[String], placeholders: &Placeholders) -> Result<Vec<String>, EachError> {
let mode = match placeholders {
Placeholders::Package { .. } => Mode::PerPackage,
Placeholders::Target { .. } => Mode::PerTarget,
Placeholders::Once { .. } => Mode::Once,
};
validate_placeholders(args, mode)?;
let mut out = Vec::with_capacity(args.len());
for arg in args {
match placeholders {
Placeholders::Package {
name,
spec,
version,
manifest,
..
} => {
#[expect(
clippy::literal_string_with_formatting_args,
reason = "cargo-each placeholder tokens, not format args"
)]
let replaced = replace_arg(
arg,
placeholders,
vec![("{name}", name), ("{spec}", spec), ("{version}", version), ("{manifest}", manifest)],
)?;
out.push(replaced);
}
Placeholders::Target {
name,
spec,
version,
manifest,
target,
..
} => {
#[expect(
clippy::literal_string_with_formatting_args,
reason = "cargo-each placeholder tokens, not format args"
)]
let replaced = replace_arg(
arg,
placeholders,
vec![
("{name}", name),
("{spec}", spec),
("{version}", version),
("{manifest}", manifest),
(TARGET_TOKEN, target),
],
)?;
out.push(replaced);
}
Placeholders::Once { packages, .. } => {
if arg == PACKAGES_TOKEN {
out.extend(packages.iter().cloned());
} else {
out.push(replace_arg(arg, placeholders, Vec::new())?);
}
}
}
}
Ok(out)
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::*;
fn pkg() -> Placeholders {
Placeholders::Package {
name: "cargo-anvil".to_owned(),
spec: "cargo-anvil@0.4.0".to_owned(),
version: "0.4.0".to_owned(),
manifest: "/ws/cargo-anvil/Cargo.toml".to_owned(),
workspace_rust_version: None,
}
}
fn args(parts: &[&str]) -> Vec<String> {
parts.iter().map(|s| (*s).to_owned()).collect()
}
#[test]
fn per_package_tokens_expand() {
let out = substitute(
&args(&["{name}", "{spec}", "{version}", "{manifest}", "{name}:{version}:{name}"]),
&pkg(),
)
.expect("substitute");
assert_eq!(
out,
[
"cargo-anvil",
"cargo-anvil@0.4.0",
"0.4.0",
"/ws/cargo-anvil/Cargo.toml",
"cargo-anvil:0.4.0:cargo-anvil",
]
);
}
#[test]
fn spec_and_name_distinct() {
let out = substitute(&args(&["--package", "{name}", "note={spec}"]), &pkg()).expect("substitute");
assert_eq!(out, ["--package", "cargo-anvil", "note=cargo-anvil@0.4.0"]);
}
#[test]
fn packages_token_rejected_in_per_package_mode() {
let err = substitute(&args(&["clippy", "{packages}"]), &pkg()).expect_err("misuse");
assert_eq!(
err.to_string(),
"placeholder `{packages}` cannot be used here: only valid in --once mode"
);
}
#[test]
fn once_expands_packages_token() {
let ph = Placeholders::Once {
packages: args(&["--package", "a@1", "--package", "b@2"]),
workspace_rust_version: None,
};
let out = substitute(&args(&["clippy", "{packages}", "--all-targets"]), &ph).expect("substitute");
assert_eq!(out, ["clippy", "--package", "a@1", "--package", "b@2", "--all-targets"]);
}
#[test]
fn once_rejects_per_package_token() {
let ph = Placeholders::Once {
packages: args(&["--workspace"]),
workspace_rust_version: None,
};
let err = substitute(&args(&["test", "--package", "{name}"]), &ph).expect_err("misuse");
assert_eq!(
err.to_string(),
"placeholder `{name}` cannot be used here: per-package token is not valid in --once mode"
);
}
#[test]
fn once_rejects_target_token() {
let ph = Placeholders::Once {
packages: args(&["--workspace"]),
workspace_rust_version: None,
};
let err = substitute(&args(&["test", "--test", "{target}"]), &ph).expect_err("misuse");
assert_eq!(
err.to_string(),
"placeholder `{target}` cannot be used here: per-target token is not valid in --once mode"
);
}
#[test]
fn once_rejects_embedded_packages_token() {
let ph = Placeholders::Once {
packages: args(&["--workspace"]),
workspace_rust_version: None,
};
let err = substitute(&args(&["x={packages}"]), &ph).expect_err("misuse");
assert_eq!(
err.to_string(),
"placeholder `{packages}` cannot be used here: must stand alone as a whole argument (it expands to multiple tokens)"
);
}
#[test]
fn target_mode_expands_package_and_target_tokens() {
let ph = Placeholders::Target {
name: "cargo-anvil".to_owned(),
spec: "cargo-anvil@0.4.0".to_owned(),
version: "0.4.0".to_owned(),
manifest: "/ws/cargo-anvil/Cargo.toml".to_owned(),
target: "loom".to_owned(),
workspace_rust_version: None,
};
let out = substitute(
&args(&["{name}", "{spec}", "{version}", "{manifest}", "{target}", "{target}:{name}"]),
&ph,
)
.expect("substitute");
assert_eq!(
out,
[
"cargo-anvil",
"cargo-anvil@0.4.0",
"0.4.0",
"/ws/cargo-anvil/Cargo.toml",
"loom",
"loom:cargo-anvil",
]
);
}
#[test]
fn target_token_is_rejected_in_per_package_mode() {
let err = substitute(&args(&["echo", "{target}"]), &pkg()).expect_err("misuse");
assert_eq!(
err.to_string(),
"placeholder `{target}` cannot be used here: only valid in per-target mode"
);
}
#[test]
fn unknown_and_brace_like_tokens_pass_through_verbatim() {
let input = args(&["{manfiest}", "x={Name}", "{", "}"]);
assert_eq!(substitute(&input, &pkg()).expect("substitute"), input);
}
#[test]
fn workspace_rust_version_expands_in_every_mode() {
let command = args(&["rustup", "toolchain", "install", "{workspace-rust-version}"]);
let mut package = pkg();
let Placeholders::Package {
workspace_rust_version, ..
} = &mut package
else {
unreachable!("pkg returns package placeholders");
};
*workspace_rust_version = Some("1.80".to_owned());
assert_eq!(
substitute(&command, &package).expect("package substitution"),
["rustup", "toolchain", "install", "1.80"]
);
let once = Placeholders::Once {
packages: args(&["--workspace"]),
workspace_rust_version: Some("1.80".to_owned()),
};
assert_eq!(
substitute(&command, &once).expect("once substitution"),
["rustup", "toolchain", "install", "1.80"]
);
}
#[test]
fn unresolved_workspace_rust_version_is_reported_in_package_and_target_modes() {
let command = args(&["echo", "{workspace-rust-version}"]);
let package_error = substitute(&command, &pkg()).expect_err("package value is unresolved");
assert!(package_error.to_string().contains("root value was not resolved"), "{package_error}");
let target = Placeholders::Target {
name: "crate".to_owned(),
spec: "crate@1.0.0".to_owned(),
version: "1.0.0".to_owned(),
manifest: "/ws/crate/Cargo.toml".to_owned(),
target: "example".to_owned(),
workspace_rust_version: None,
};
let target_error = substitute(&command, &target).expect_err("target value is unresolved");
assert!(target_error.to_string().contains("root value was not resolved"), "{target_error}");
}
#[test]
fn package_values_are_not_rescanned_for_workspace_tokens() {
let placeholders = Placeholders::Package {
name: "crate".to_owned(),
spec: "crate@1.0.0".to_owned(),
version: "1.0.0".to_owned(),
manifest: "/ws/{workspace-rust-version}/crate/Cargo.toml".to_owned(),
workspace_rust_version: Some("1.80".to_owned()),
};
assert_eq!(
substitute(&args(&["{workspace-rust-version}", "{manifest}"]), &placeholders).expect("substitute package placeholders"),
["1.80", "/ws/{workspace-rust-version}/crate/Cargo.toml"]
);
}
#[test]
fn target_values_are_not_rescanned_for_workspace_tokens() {
let placeholders = Placeholders::Target {
name: "crate".to_owned(),
spec: "crate@1.0.0".to_owned(),
version: "1.0.0".to_owned(),
manifest: "/ws/{workspace-rust-version}/crate/Cargo.toml".to_owned(),
target: "example".to_owned(),
workspace_rust_version: Some("1.80".to_owned()),
};
assert_eq!(
substitute(&args(&["{workspace-rust-version}", "{manifest}:{target}"]), &placeholders,)
.expect("substitute target placeholders"),
["1.80", "/ws/{workspace-rust-version}/crate/Cargo.toml:example"]
);
}
#[test]
fn manifest_values_are_not_rescanned_for_target_tokens() {
let placeholders = Placeholders::Target {
name: "crate".to_owned(),
spec: "crate@1.0.0".to_owned(),
version: "1.0.0".to_owned(),
manifest: "/ws/{target}/crate/Cargo.toml".to_owned(),
target: "example".to_owned(),
workspace_rust_version: None,
};
assert_eq!(
substitute(&args(&["{manifest}:{target}"]), &placeholders).expect("substitute target placeholders"),
["/ws/{target}/crate/Cargo.toml:example"]
);
}
#[test]
fn detects_workspace_rust_version_usage() {
assert!(uses_workspace_rust_version(&args(&["tool", "v={workspace-rust-version}"])));
assert!(!uses_workspace_rust_version(&args(&["tool", "{name}"])));
}
}