diff options
Diffstat (limited to 'crates/mozart-core/src')
| -rw-r--r-- | crates/mozart-core/src/composer.rs | 26 | ||||
| -rw-r--r-- | crates/mozart-core/src/factory.rs | 28 | ||||
| -rw-r--r-- | crates/mozart-core/src/validation.rs | 52 |
3 files changed, 93 insertions, 13 deletions
diff --git a/crates/mozart-core/src/composer.rs b/crates/mozart-core/src/composer.rs index 66bae92..effcae4 100644 --- a/crates/mozart-core/src/composer.rs +++ b/crates/mozart-core/src/composer.rs @@ -219,11 +219,27 @@ impl LocalPackage { /// commands that walk the local install (currently: `dump-autoload`). pub struct LocalRepository { packages: Vec<LocalPackage>, + /// Mirrors `InstalledRepositoryInterface::getDevMode()`: `Some(true)` when + /// the last install ran with dev requires, `Some(false)` when run with + /// `--no-dev`, and `None` when the flag was absent (the legacy v1 + /// installed.json shape, or an in-memory repository that was never + /// hydrated from disk). Callers default to `true` on `None`, matching + /// `ReinstallCommand::execute`'s `getDevMode() ?? true`. + dev_mode: Option<bool>, } impl LocalRepository { pub fn new(packages: Vec<LocalPackage>) -> Self { - Self { packages } + Self { + packages, + dev_mode: None, + } + } + + /// Build a [`LocalRepository`] with an explicit `dev` flag taken from + /// `vendor/composer/installed.json`'s top-level `dev` field. + pub fn with_dev_mode(packages: Vec<LocalPackage>, dev_mode: Option<bool>) -> Self { + Self { packages, dev_mode } } /// Mirror of `WritableRepositoryInterface::getCanonicalPackages` — @@ -233,6 +249,14 @@ impl LocalRepository { pub fn canonical_packages(&self) -> impl Iterator<Item = &LocalPackage> { self.packages.iter() } + + /// Mirror of `InstalledRepositoryInterface::getDevMode()` — returns + /// `None` when the source `installed.json` did not record a `dev` + /// flag (e.g. legacy v1 array form). Callers should default to + /// `true` on `None`, matching PHP's `?? true` coalesce. + pub fn dev_mode(&self) -> Option<bool> { + self.dev_mode + } } /// Mirror of `Composer\Repository\RepositoryManager`. Today only the diff --git a/crates/mozart-core/src/factory.rs b/crates/mozart-core/src/factory.rs index c9d346b..6602056 100644 --- a/crates/mozart-core/src/factory.rs +++ b/crates/mozart-core/src/factory.rs @@ -208,8 +208,9 @@ pub fn create_composer(project_dir: PathBuf, composer_json: &Path) -> anyhow::Re project_dir.join(&config.vendor_dir) }; - let local_packages = read_local_packages(&vendor_dir)?; - let repository_manager = RepositoryManager::new(LocalRepository::new(local_packages)); + let (local_packages, dev_mode) = read_local_packages(&vendor_dir)?; + let repository_manager = + RepositoryManager::new(LocalRepository::with_dev_mode(local_packages, dev_mode)); let installation_manager = InstallationManager::new(vendor_dir); let autoload_generator = AutoloadGenerator::new(); @@ -252,21 +253,24 @@ pub fn create_composer(project_dir: PathBuf, composer_json: &Path) -> anyhow::Re /// dependency graph; the parsing that's actually load-bearing for the /// install-path computation is just the package name + optional /// `target-dir`. -fn read_local_packages(vendor_dir: &Path) -> anyhow::Result<Vec<LocalPackage>> { +fn read_local_packages(vendor_dir: &Path) -> anyhow::Result<(Vec<LocalPackage>, Option<bool>)> { let path = vendor_dir.join("composer/installed.json"); if !path.exists() { - return Ok(Vec::new()); + return Ok((Vec::new(), None)); } let content = std::fs::read_to_string(&path)?; let value: serde_json::Value = serde_json::from_str(&content)?; - let entries: &[serde_json::Value] = match &value { - serde_json::Value::Object(obj) => match obj.get("packages") { - Some(serde_json::Value::Array(arr)) => arr.as_slice(), - _ => return Ok(Vec::new()), - }, - serde_json::Value::Array(arr) => arr.as_slice(), - _ => return Ok(Vec::new()), + let (entries, dev_mode): (&[serde_json::Value], Option<bool>) = match &value { + serde_json::Value::Object(obj) => { + let entries = match obj.get("packages") { + Some(serde_json::Value::Array(arr)) => arr.as_slice(), + _ => return Ok((Vec::new(), obj.get("dev").and_then(|v| v.as_bool()))), + }; + (entries, obj.get("dev").and_then(|v| v.as_bool())) + } + serde_json::Value::Array(arr) => (arr.as_slice(), None), + _ => return Ok((Vec::new(), None)), }; let mut out = Vec::with_capacity(entries.len()); @@ -310,7 +314,7 @@ fn read_local_packages(vendor_dir: &Path) -> anyhow::Result<Vec<LocalPackage>> { extra, )); } - Ok(out) + Ok((out, dev_mode)) } fn read_package_reference( diff --git a/crates/mozart-core/src/validation.rs b/crates/mozart-core/src/validation.rs index 24f1705..c27eb91 100644 --- a/crates/mozart-core/src/validation.rs +++ b/crates/mozart-core/src/validation.rs @@ -115,6 +115,19 @@ pub fn parse_require_string(s: &str) -> Result<(String, String), String> { )) } +/// Mirror of `Composer\Package\BasePackage::packageNameToRegexp`. Each +/// character of `pattern` is `regex::escape`d, then `\*` is rewritten +/// to `.*`, and the result is anchored and compiled case-insensitively. +/// Used by selection commands (`reinstall`, `remove`, `update`, …) to +/// expand `vendor/*`-style globs against installed/locked package names. +pub fn package_name_to_regexp(pattern: &str) -> Regex { + let escaped = regex::escape(pattern).replace("\\*", ".*"); + // PHP wraps with `{^...$}i`; in Rust we anchor with `\A`/`\z` and + // pass `(?i)` for case-insensitivity. + Regex::new(&format!(r"(?i)\A{escaped}\z")) + .expect("package_name_to_regexp pattern always compiles") +} + #[cfg(test)] mod tests { use super::*; @@ -207,6 +220,45 @@ mod tests { } #[test] + fn test_package_name_to_regexp_exact() { + let re = package_name_to_regexp("monolog/monolog"); + assert!(re.is_match("monolog/monolog")); + assert!(re.is_match("Monolog/Monolog")); + assert!(!re.is_match("psr/log")); + assert!(!re.is_match("monolog/monolog-extra")); + assert!(!re.is_match("xmonolog/monolog")); + } + + #[test] + fn test_package_name_to_regexp_wildcard() { + let re = package_name_to_regexp("psr/*"); + assert!(re.is_match("psr/log")); + assert!(re.is_match("psr/container")); + assert!(re.is_match("psr/log/sub")); + assert!(!re.is_match("monolog/monolog")); + + let re = package_name_to_regexp("*/log"); + assert!(re.is_match("psr/log")); + assert!(re.is_match("monolog/log")); + assert!(!re.is_match("psr/container")); + + let re = package_name_to_regexp("symfony/*/bridge"); + assert!(re.is_match("symfony/http/bridge")); + assert!(!re.is_match("symfony/bridge")); + + let re = package_name_to_regexp("*"); + assert!(re.is_match("anything/at/all")); + } + + #[test] + fn test_package_name_to_regexp_escapes_metacharacters() { + // `.` must be literal, not "any character" + let re = package_name_to_regexp("foo.bar/baz"); + assert!(re.is_match("foo.bar/baz")); + assert!(!re.is_match("fooXbar/baz")); + } + + #[test] fn test_parse_require_string() { let (name, ver) = parse_require_string("foo/bar:^1.0").unwrap(); assert_eq!(name, "foo/bar"); |
