aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart-core/src
diff options
context:
space:
mode:
Diffstat (limited to 'crates/mozart-core/src')
-rw-r--r--crates/mozart-core/src/composer.rs26
-rw-r--r--crates/mozart-core/src/factory.rs28
-rw-r--r--crates/mozart-core/src/validation.rs52
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");