|
1 | 1 | //! Type definitions and functions for working with `wkg.toml` files. |
2 | 2 |
|
3 | 3 | use std::{ |
4 | | - collections::HashMap, |
| 4 | + collections::{HashMap, HashSet}, |
5 | 5 | path::{Path, PathBuf}, |
6 | 6 | }; |
7 | 7 |
|
8 | 8 | use anyhow::{Context, Result}; |
9 | 9 | use semver::VersionReq; |
10 | 10 | use serde::{Deserialize, Serialize}; |
| 11 | +use wasm_pkg_common::package::{PackageRef, PackageSpec}; |
11 | 12 | mod paths; |
12 | 13 | pub mod workspace; |
13 | 14 |
|
@@ -79,10 +80,56 @@ impl Manifest { |
79 | 80 | // `Manifest` validations, mirrors cargo's `Workspace::validate` |
80 | 81 | fn validate(&self) -> Result<()> { |
81 | 82 | self.validate_workspace_exclusivity()?; |
| 83 | + self.validate_override_keys()?; |
82 | 84 | // Add new validation rules with `self.validate_*()?;` |
83 | 85 | Ok(()) |
84 | 86 | } |
85 | 87 |
|
| 88 | + /// Checks that override keys parse and that no package is covered by both a bare and a |
| 89 | + /// versioned key. |
| 90 | + /// |
| 91 | + /// Run as part of [`validate`](Self::validate) on load, and again when resolving, since a |
| 92 | + /// `Manifest` can also be built directly through the library API without going through TOML. |
| 93 | + pub(crate) fn validate_override_keys(&self) -> Result<()> { |
| 94 | + let Some(overrides) = self.overrides.as_ref() else { |
| 95 | + return Ok(()); |
| 96 | + }; |
| 97 | + // `overrides` is a map, so walk it in a stable order |
| 98 | + let mut sorted_keys: Vec<&String> = overrides.keys().collect(); |
| 99 | + sorted_keys.sort_unstable(); |
| 100 | + |
| 101 | + let mut bare: HashSet<PackageRef> = HashSet::new(); |
| 102 | + let mut versioned: HashMap<PackageRef, Vec<&str>> = HashMap::new(); |
| 103 | + for key in sorted_keys { |
| 104 | + let spec: PackageSpec = key |
| 105 | + .parse() |
| 106 | + .with_context(|| format!("invalid override key `{key}`"))?; |
| 107 | + match spec.version { |
| 108 | + Some(_) => versioned.entry(spec.package).or_default().push(key), |
| 109 | + None => { |
| 110 | + bare.insert(spec.package); |
| 111 | + } |
| 112 | + } |
| 113 | + } |
| 114 | + |
| 115 | + let mut conflicts: Vec<String> = versioned |
| 116 | + .iter() |
| 117 | + .filter(|(package, _)| bare.contains(*package)) |
| 118 | + .map(|(package, keys)| { |
| 119 | + format!( |
| 120 | + "override `{package}` applies to every version of the package, so it \ |
| 121 | + conflicts with the versioned override(s) `{}`", |
| 122 | + keys.join("`, `") |
| 123 | + ) |
| 124 | + }) |
| 125 | + .collect(); |
| 126 | + if conflicts.is_empty() { |
| 127 | + return Ok(()); |
| 128 | + } |
| 129 | + conflicts.sort_unstable(); |
| 130 | + anyhow::bail!("{} - remove one or the other", conflicts.join("; ")); |
| 131 | + } |
| 132 | + |
86 | 133 | // no overrides or top-level metadata when workspace is present |
87 | 134 | fn validate_workspace_exclusivity(&self) -> Result<()> { |
88 | 135 | if self.workspace.is_none() { |
@@ -241,4 +288,68 @@ mod tests { |
241 | 288 | "manifest loaded from file does not match original manifest" |
242 | 289 | ); |
243 | 290 | } |
| 291 | + |
| 292 | + #[test] |
| 293 | + fn override_keys_may_carry_a_version() { |
| 294 | + let manifest = Manifest::from_toml( |
| 295 | + r#" |
| 296 | +[overrides] |
| 297 | +"foo:bar@0.1.0" = { path = "bar-0.1.0" } |
| 298 | +"foo:bar@0.2.0" = { path = "bar-0.2.0" } |
| 299 | +"foo:baz" = { path = "baz" } |
| 300 | +"#, |
| 301 | + ) |
| 302 | + .expect("versioned override keys should be accepted"); |
| 303 | + assert_eq!(manifest.overrides.unwrap().len(), 3); |
| 304 | + } |
| 305 | + |
| 306 | + #[test] |
| 307 | + fn override_keys_conflict_when_bare_and_versioned() { |
| 308 | + let err = Manifest::from_toml( |
| 309 | + r#" |
| 310 | +[overrides] |
| 311 | +"foo:bar" = { path = "bar" } |
| 312 | +"foo:bar@0.1.0" = { path = "bar-0.1.0" } |
| 313 | +"#, |
| 314 | + ) |
| 315 | + .expect_err("a bare key alongside a versioned one is ambiguous"); |
| 316 | + let err = format!("{err:#}"); |
| 317 | + assert!(err.contains("foo:bar@0.1.0"), "unexpected error: {err}"); |
| 318 | + } |
| 319 | + |
| 320 | + #[test] |
| 321 | + fn override_key_conflicts_are_all_reported_in_a_stable_order() { |
| 322 | + // Two conflicting packages: both must appear, and always in the same order, rather than |
| 323 | + // whichever the underlying map happened to yield first. |
| 324 | + let err = Manifest::from_toml( |
| 325 | + r#" |
| 326 | +[overrides] |
| 327 | +"zzz:two" = { path = "z" } |
| 328 | +"zzz:two@0.2.0" = { path = "z2" } |
| 329 | +"aaa:one" = { path = "a" } |
| 330 | +"aaa:one@0.1.0" = { path = "a1" } |
| 331 | +"#, |
| 332 | + ) |
| 333 | + .expect_err("both packages conflict"); |
| 334 | + let err = format!("{err:#}"); |
| 335 | + let aaa = err.find("aaa:one").expect("aaa:one should be reported"); |
| 336 | + let zzz = err.find("zzz:two").expect("zzz:two should be reported"); |
| 337 | + assert!(aaa < zzz, "conflicts should be sorted: {err}"); |
| 338 | + } |
| 339 | + |
| 340 | + #[test] |
| 341 | + fn override_keys_must_parse() { |
| 342 | + let err = Manifest::from_toml( |
| 343 | + r#" |
| 344 | +[overrides] |
| 345 | +"not a package ref" = { path = "bar" } |
| 346 | +"#, |
| 347 | + ) |
| 348 | + .expect_err("an unparseable override key should be rejected"); |
| 349 | + let err = format!("{err:#}"); |
| 350 | + assert!( |
| 351 | + err.contains("invalid override key"), |
| 352 | + "unexpected error: {err}" |
| 353 | + ); |
| 354 | + } |
244 | 355 | } |
0 commit comments