aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart/src
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-05-03 11:55:03 +0900
committernsfisis <nsfisis@gmail.com>2026-05-03 11:55:03 +0900
commitae1aa6540761e54a76b8f7984cf93cd3a0d011d0 (patch)
treef111e1c73977f0bffb6323b03f4210269b43b297 /crates/mozart/src
parent30ae6c869adc7f3cb87a4d63edd6d0cda89d571d (diff)
downloadphp-mozart-ae1aa6540761e54a76b8f7984cf93cd3a0d011d0.tar.gz
php-mozart-ae1aa6540761e54a76b8f7984cf93cd3a0d011d0.tar.zst
php-mozart-ae1aa6540761e54a76b8f7984cf93cd3a0d011d0.zip
refactor: switch internal maps/sets from HashMap to IndexMap
Adopt indexmap workspace-wide so iteration order is deterministic and follows insertion order. The non-deterministic order of std HashMap otherwise leaks into resolver decisions when multiple valid solutions exist (e.g. cyclic require pairs under prefer-lowest), making behavior flaky and divergent from Composer's PHP-array semantics. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates/mozart/src')
-rw-r--r--crates/mozart/src/commands/audit.rs4
-rw-r--r--crates/mozart/src/commands/bump.rs6
-rw-r--r--crates/mozart/src/commands/check_platform_reqs.rs2
-rw-r--r--crates/mozart/src/commands/create_project.rs6
-rw-r--r--crates/mozart/src/commands/dependency.rs13
-rw-r--r--crates/mozart/src/commands/exec.rs3
-rw-r--r--crates/mozart/src/commands/install.rs15
-rw-r--r--crates/mozart/src/commands/licenses.rs4
-rw-r--r--crates/mozart/src/commands/outdated.rs10
-rw-r--r--crates/mozart/src/commands/reinstall.rs4
-rw-r--r--crates/mozart/src/commands/remove.rs30
-rw-r--r--crates/mozart/src/commands/repository.rs2
-rw-r--r--crates/mozart/src/commands/require.rs27
-rw-r--r--crates/mozart/src/commands/search.rs4
-rw-r--r--crates/mozart/src/commands/show.rs20
-rw-r--r--crates/mozart/src/commands/status.rs30
-rw-r--r--crates/mozart/src/commands/suggests.rs44
-rw-r--r--crates/mozart/src/commands/update.rs36
-rw-r--r--crates/mozart/src/commands/validate.rs2
19 files changed, 132 insertions, 130 deletions
diff --git a/crates/mozart/src/commands/audit.rs b/crates/mozart/src/commands/audit.rs
index 9cbff31..163a43a 100644
--- a/crates/mozart/src/commands/audit.rs
+++ b/crates/mozart/src/commands/audit.rs
@@ -187,7 +187,7 @@ fn load_installed_packages(working_dir: &Path, no_dev: bool) -> anyhow::Result<V
let vendor_dir = working_dir.join("vendor");
let installed = mozart_registry::installed::InstalledPackages::read(&vendor_dir)?;
- let dev_names: std::collections::HashSet<String> = installed
+ let dev_names: indexmap::IndexSet<String> = installed
.dev_package_names
.iter()
.map(|n| n.to_lowercase())
@@ -257,7 +257,7 @@ fn filter_advisories(
ignore_severity: &[String],
console: &mozart_core::console::Console,
) -> BTreeMap<String, Vec<MatchedAdvisory>> {
- let ignore_set: std::collections::HashSet<String> =
+ let ignore_set: indexmap::IndexSet<String> =
ignore_severity.iter().map(|s| s.to_lowercase()).collect();
let mut result: BTreeMap<String, Vec<MatchedAdvisory>> = BTreeMap::new();
diff --git a/crates/mozart/src/commands/bump.rs b/crates/mozart/src/commands/bump.rs
index 60b055f..3ad6ed6 100644
--- a/crates/mozart/src/commands/bump.rs
+++ b/crates/mozart/src/commands/bump.rs
@@ -1,7 +1,7 @@
use clap::Args;
+use indexmap::IndexMap;
use mozart_core::console::Verbosity;
use mozart_core::console_format;
-use std::collections::HashMap;
use std::path::PathBuf;
/// Exit code for stale lock file (matches Composer's BumpCommand::ERROR_LOCK_OUTDATED)
@@ -226,8 +226,8 @@ pub async fn execute(
/// Build a map of lowercase package names to (pretty_version, version_normalized) from composer.lock.
fn build_locked_versions_map(
lock: &mozart_registry::lockfile::LockFile,
-) -> HashMap<String, (String, Option<String>)> {
- let mut map: HashMap<String, (String, Option<String>)> = HashMap::new();
+) -> IndexMap<String, (String, Option<String>)> {
+ let mut map: IndexMap<String, (String, Option<String>)> = IndexMap::new();
let all_packages = lock
.packages
diff --git a/crates/mozart/src/commands/check_platform_reqs.rs b/crates/mozart/src/commands/check_platform_reqs.rs
index e9954f4..a890e0c 100644
--- a/crates/mozart/src/commands/check_platform_reqs.rs
+++ b/crates/mozart/src/commands/check_platform_reqs.rs
@@ -227,7 +227,7 @@ fn collect_from_installed_data(
no_dev: bool,
requirements: &mut BTreeMap<String, Vec<PlatformRequirement>>,
) {
- let dev_names: std::collections::HashSet<String> = installed
+ let dev_names: indexmap::IndexSet<String> = installed
.dev_package_names
.iter()
.map(|n| n.to_lowercase())
diff --git a/crates/mozart/src/commands/create_project.rs b/crates/mozart/src/commands/create_project.rs
index af77ba6..139550a 100644
--- a/crates/mozart/src/commands/create_project.rs
+++ b/crates/mozart/src/commands/create_project.rs
@@ -1,4 +1,5 @@
use clap::Args;
+use indexmap::IndexMap;
use mozart_core::console_format;
use mozart_core::package::{self, Stability};
use mozart_core::validation;
@@ -7,7 +8,6 @@ use mozart_registry::lockfile;
use mozart_registry::packagist;
use mozart_registry::resolver::{self, PlatformConfig, ResolveRequest};
use mozart_registry::version;
-use std::collections::HashMap;
use std::path::{Path, PathBuf};
#[derive(Args)]
@@ -413,7 +413,7 @@ pub async fn execute(
require_dev,
include_dev: dev_mode,
minimum_stability: proj_minimum_stability,
- stability_flags: HashMap::new(),
+ stability_flags: IndexMap::new(),
prefer_stable: composer_prefer_stable,
prefer_lowest: false,
platform: PlatformConfig::new(),
@@ -422,7 +422,7 @@ pub async fn execute(
repositories: std::sync::Arc::new(
mozart_registry::repository::RepositorySet::with_packagist(repo_cache.clone()),
),
- temporary_constraints: HashMap::new(),
+ temporary_constraints: IndexMap::new(),
raw_repositories: raw.repositories.clone(),
root_provide: raw
.provide
diff --git a/crates/mozart/src/commands/dependency.rs b/crates/mozart/src/commands/dependency.rs
index 6dcaec8..d044432 100644
--- a/crates/mozart/src/commands/dependency.rs
+++ b/crates/mozart/src/commands/dependency.rs
@@ -4,7 +4,8 @@
//! `prohibits` (aka `why-not`) answers: "Which packages prevent version X of package Y from being
//! installed?"
-use std::collections::{BTreeMap, HashSet};
+use indexmap::IndexSet;
+use std::collections::BTreeMap;
use std::path::Path;
use anyhow::Result;
@@ -233,7 +234,7 @@ fn get_dependents_forward(
needles: &[String],
recursive: bool,
) -> Result<Vec<DependencyResult>> {
- let needle_set: HashSet<String> = needles.iter().map(|n| n.to_lowercase()).collect();
+ let needle_set: IndexSet<String> = needles.iter().map(|n| n.to_lowercase()).collect();
// Build name→PackageInfo lookup
let pkg_map: BTreeMap<String, &PackageInfo> = packages
@@ -243,7 +244,7 @@ fn get_dependents_forward(
if recursive {
// Recursive: BFS from needles upward to root, building a tree
- let mut visited: HashSet<String> = HashSet::new();
+ let mut visited: IndexSet<String> = IndexSet::new();
let mut results: Vec<DependencyResult> = Vec::new();
for needle in needles {
@@ -318,8 +319,8 @@ fn recurse_dependents(
packages: &[PackageInfo],
needle: &str,
pkg_map: &BTreeMap<String, &PackageInfo>,
- visited: &mut HashSet<String>,
- _original_needles: &HashSet<String>,
+ visited: &mut IndexSet<String>,
+ _original_needles: &IndexSet<String>,
) -> Vec<DependencyResult> {
let _ = pkg_map; // kept for potential future use
let direct = collect_direct_requires(packages, needle);
@@ -545,7 +546,7 @@ pub fn print_table(results: &[DependencyResult], console: &mozart_core::console:
.max()
.unwrap_or(0);
- let mut seen: HashSet<String> = HashSet::new();
+ let mut seen: IndexSet<String> = IndexSet::new();
for r in results {
let key = format!(
"{}|{}|{}|{}",
diff --git a/crates/mozart/src/commands/exec.rs b/crates/mozart/src/commands/exec.rs
index 350ff5c..eaaf465 100644
--- a/crates/mozart/src/commands/exec.rs
+++ b/crates/mozart/src/commands/exec.rs
@@ -171,8 +171,7 @@ fn get_binaries(working_dir: &Path, bin_dir: &Path) -> Vec<(String, bool)> {
// Collect from root composer.json bin entries
let composer_json_path = working_dir.join("composer.json");
if let Ok(root) = mozart_core::package::read_from_file(&composer_json_path) {
- let existing: std::collections::HashSet<&str> =
- binaries.iter().map(|(n, _)| n.as_str()).collect();
+ let existing: indexmap::IndexSet<&str> = binaries.iter().map(|(n, _)| n.as_str()).collect();
let mut local: Vec<String> = root
.bin
.iter()
diff --git a/crates/mozart/src/commands/install.rs b/crates/mozart/src/commands/install.rs
index 9555ba7..b38194e 100644
--- a/crates/mozart/src/commands/install.rs
+++ b/crates/mozart/src/commands/install.rs
@@ -1,4 +1,5 @@
use clap::Args;
+use indexmap::IndexSet;
use mozart_core::console;
use mozart_core::console_format;
use mozart_registry::installed;
@@ -6,7 +7,7 @@ use mozart_registry::installer_executor::{
ExecuteContext, FilesystemExecutor, InstallerExecutor, PackageOperation,
};
use mozart_registry::lockfile;
-use std::collections::{BTreeMap, HashSet};
+use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
#[derive(Args)]
@@ -198,7 +199,7 @@ pub fn compute_operations<'a>(
}
// Compute removals: packages in installed but not in locked
- let locked_names: HashSet<String> = locked.iter().map(|p| p.name.to_lowercase()).collect();
+ let locked_names: IndexSet<String> = locked.iter().map(|p| p.name.to_lowercase()).collect();
let removals: Vec<String> = installed
.packages
@@ -254,7 +255,7 @@ fn topological_sort<'a>(
// Identify root packages: those not pulled in by any other package's
// requires (counting provides/replaces as a match).
- let mut required_by_others: HashSet<String> = HashSet::new();
+ let mut required_by_others: IndexSet<String> = IndexSet::new();
for pkg in &sorted {
let pkg_lower = pkg.name.to_lowercase();
for dep in pkg.require.keys() {
@@ -276,8 +277,8 @@ fn topological_sort<'a>(
.copied()
.collect();
- let mut visited: HashSet<String> = HashSet::new();
- let mut processed: HashSet<String> = HashSet::new();
+ let mut visited: IndexSet<String> = IndexSet::new();
+ let mut processed: IndexSet<String> = IndexSet::new();
let mut ordered: Vec<&'a lockfile::LockedPackage> = Vec::with_capacity(packages.len());
while let Some(pkg) = stack.pop() {
@@ -444,7 +445,7 @@ fn check_platform_requirements_against(
return Vec::new();
}
- let ignored: HashSet<String> = ignore_platform_req
+ let ignored: IndexSet<String> = ignore_platform_req
.iter()
.map(|s| s.to_lowercase())
.collect();
@@ -503,7 +504,7 @@ fn warn_platform_requirements(
return;
}
- let ignored_set: HashSet<String> = ignore_platform_req
+ let ignored_set: IndexSet<String> = ignore_platform_req
.iter()
.map(|s| s.to_lowercase())
.collect();
diff --git a/crates/mozart/src/commands/licenses.rs b/crates/mozart/src/commands/licenses.rs
index b066fde..5ce2b35 100644
--- a/crates/mozart/src/commands/licenses.rs
+++ b/crates/mozart/src/commands/licenses.rs
@@ -1,7 +1,7 @@
use clap::Args;
+use indexmap::IndexSet;
use mozart_core::console::{Console, Verbosity};
use serde::Serialize;
-use std::collections::HashSet;
use std::path::{Path, PathBuf};
#[derive(Args)]
@@ -102,7 +102,7 @@ fn load_installed_licenses(working_dir: &Path, no_dev: bool) -> anyhow::Result<V
let vendor_dir = working_dir.join("vendor");
let installed = mozart_registry::installed::InstalledPackages::read(&vendor_dir)?;
- let dev_names: HashSet<String> = installed
+ let dev_names: IndexSet<String> = installed
.dev_package_names
.iter()
.map(|n| n.to_lowercase())
diff --git a/crates/mozart/src/commands/outdated.rs b/crates/mozart/src/commands/outdated.rs
index 4d0226d..5a4f854 100644
--- a/crates/mozart/src/commands/outdated.rs
+++ b/crates/mozart/src/commands/outdated.rs
@@ -1,7 +1,7 @@
use clap::Args;
+use indexmap::IndexSet;
use mozart_core::matches_wildcard;
use std::cmp::Ordering;
-use std::collections::HashSet;
use std::path::{Path, PathBuf};
#[derive(Args)]
@@ -136,14 +136,14 @@ pub async fn execute(
};
// Build set of direct dependency names
- let direct_names: HashSet<String> = if let Some(ref root) = root_package {
- let mut names: HashSet<String> = root.require.keys().map(|k| k.to_lowercase()).collect();
+ let direct_names: IndexSet<String> = if let Some(ref root) = root_package {
+ let mut names: IndexSet<String> = root.require.keys().map(|k| k.to_lowercase()).collect();
if !args.no_dev {
names.extend(root.require_dev.keys().map(|k| k.to_lowercase()));
}
names
} else {
- HashSet::new()
+ IndexSet::new()
};
// Process each package
@@ -242,7 +242,7 @@ fn load_installed_packages(working_dir: &Path, no_dev: bool) -> anyhow::Result<V
let vendor_dir = working_dir.join("vendor");
let installed = mozart_registry::installed::InstalledPackages::read(&vendor_dir)?;
- let dev_names: HashSet<String> = installed
+ let dev_names: IndexSet<String> = installed
.dev_package_names
.iter()
.map(|n| n.to_lowercase())
diff --git a/crates/mozart/src/commands/reinstall.rs b/crates/mozart/src/commands/reinstall.rs
index c19d926..2df55c5 100644
--- a/crates/mozart/src/commands/reinstall.rs
+++ b/crates/mozart/src/commands/reinstall.rs
@@ -112,7 +112,7 @@ pub async fn execute(
// Step 5: Determine packages to reinstall.
// Build the full set of installed packages (prod + dev unless --no-dev).
- let dev_package_names: std::collections::HashSet<String> = installed
+ let dev_package_names: indexmap::IndexSet<String> = installed
.dev_package_names
.iter()
.map(|n| n.to_lowercase())
@@ -666,7 +666,7 @@ mod tests {
installed.packages.push(e2.clone());
installed.dev_package_names = vec!["phpunit/phpunit".to_string()];
- let dev_package_names: std::collections::HashSet<String> = installed
+ let dev_package_names: indexmap::IndexSet<String> = installed
.dev_package_names
.iter()
.map(|n| n.to_lowercase())
diff --git a/crates/mozart/src/commands/remove.rs b/crates/mozart/src/commands/remove.rs
index 20cb6a2..df8bf2b 100644
--- a/crates/mozart/src/commands/remove.rs
+++ b/crates/mozart/src/commands/remove.rs
@@ -1,11 +1,11 @@
use clap::Args;
+use indexmap::IndexMap;
use mozart_core::console::Verbosity;
use mozart_core::console_format;
use mozart_core::package;
use mozart_core::validation;
use mozart_registry::lockfile;
use mozart_registry::resolver::{self, PlatformConfig, ResolveRequest};
-use std::collections::HashMap;
#[derive(Args)]
pub struct RemoveArgs {
@@ -247,7 +247,7 @@ pub async fn execute(
require_dev,
include_dev: dev_mode,
minimum_stability,
- stability_flags: HashMap::new(),
+ stability_flags: IndexMap::new(),
prefer_stable: composer_prefer_stable,
prefer_lowest: false,
platform: PlatformConfig::new(),
@@ -256,7 +256,7 @@ pub async fn execute(
repositories: std::sync::Arc::new(
mozart_registry::repository::RepositorySet::with_packagist(repo_cache.clone()),
),
- temporary_constraints: HashMap::new(),
+ temporary_constraints: IndexMap::new(),
raw_repositories: raw.repositories.clone(),
root_provide: raw
.provide
@@ -517,7 +517,7 @@ async fn remove_unused(
require_dev,
include_dev: dev_mode,
minimum_stability,
- stability_flags: HashMap::new(),
+ stability_flags: IndexMap::new(),
prefer_stable: composer_prefer_stable,
prefer_lowest: false,
platform: PlatformConfig::new(),
@@ -526,7 +526,7 @@ async fn remove_unused(
repositories: std::sync::Arc::new(
mozart_registry::repository::RepositorySet::with_packagist(repo_cache.clone()),
),
- temporary_constraints: HashMap::new(),
+ temporary_constraints: IndexMap::new(),
raw_repositories: raw.repositories.clone(),
root_provide: raw
.provide
@@ -550,7 +550,7 @@ async fn remove_unused(
})?;
// Build set of resolved package names
- let resolved_names: std::collections::HashSet<String> =
+ let resolved_names: indexmap::IndexSet<String> =
resolved.iter().map(|p| p.name.to_lowercase()).collect();
// Find packages in the old lock that are not in the new resolution
@@ -847,9 +847,9 @@ mod tests {
#[tokio::test]
#[ignore]
async fn test_remove_full_e2e() {
+ use indexmap::IndexMap;
use mozart_registry::lockfile::{LockFileGenerationRequest, generate_lock_file};
use mozart_registry::resolver::{ResolveRequest, resolve};
- use std::collections::HashMap;
use tempfile::tempdir;
let dir = tempdir().unwrap();
@@ -870,7 +870,7 @@ mod tests {
require_dev: vec![],
include_dev: false,
minimum_stability: mozart_core::package::Stability::Stable,
- stability_flags: HashMap::new(),
+ stability_flags: IndexMap::new(),
prefer_stable: true,
prefer_lowest: false,
platform: mozart_registry::resolver::PlatformConfig::new(),
@@ -884,10 +884,10 @@ mod tests {
),
),
),
- temporary_constraints: HashMap::new(),
+ temporary_constraints: IndexMap::new(),
raw_repositories: vec![],
- root_provide: HashMap::new(),
- root_replace: HashMap::new(),
+ root_provide: IndexMap::new(),
+ root_replace: IndexMap::new(),
};
let resolved = resolve(&request)
.await
@@ -923,7 +923,7 @@ mod tests {
require_dev: vec![],
include_dev: false,
minimum_stability: mozart_core::package::Stability::Stable,
- stability_flags: HashMap::new(),
+ stability_flags: IndexMap::new(),
prefer_stable: true,
prefer_lowest: false,
platform: mozart_registry::resolver::PlatformConfig::new(),
@@ -937,10 +937,10 @@ mod tests {
),
),
),
- temporary_constraints: HashMap::new(),
+ temporary_constraints: IndexMap::new(),
raw_repositories: vec![],
- root_provide: HashMap::new(),
- root_replace: HashMap::new(),
+ root_provide: IndexMap::new(),
+ root_replace: IndexMap::new(),
};
let resolved2 = resolve(&request2)
.await
diff --git a/crates/mozart/src/commands/repository.rs b/crates/mozart/src/commands/repository.rs
index 0931b66..e8ca920 100644
--- a/crates/mozart/src/commands/repository.rs
+++ b/crates/mozart/src/commands/repository.rs
@@ -1085,7 +1085,7 @@ mod tests {
});
let result = normalize_repositories(&val);
assert_eq!(result.len(), 2);
- let names: std::collections::HashSet<&str> = result
+ let names: indexmap::IndexSet<&str> = result
.iter()
.filter_map(|v| v.get("name").and_then(|n| n.as_str()))
.collect();
diff --git a/crates/mozart/src/commands/require.rs b/crates/mozart/src/commands/require.rs
index 95b26ea..69d7ea2 100644
--- a/crates/mozart/src/commands/require.rs
+++ b/crates/mozart/src/commands/require.rs
@@ -1,4 +1,5 @@
use clap::Args;
+use indexmap::IndexMap;
use mozart_core::console::Verbosity;
use mozart_core::console_format;
use mozart_core::package::{self, Stability};
@@ -7,7 +8,6 @@ use mozart_registry::lockfile;
use mozart_registry::packagist;
use mozart_registry::resolver::{self, PlatformConfig, ResolveRequest};
use mozart_registry::version;
-use std::collections::HashMap;
use std::io::{BufRead, IsTerminal, Write};
#[derive(Args)]
@@ -133,7 +133,7 @@ pub struct RequireArgs {
/// Returns a list of `"vendor/package:constraint"` strings that the user confirmed,
/// or an empty vec if the user typed nothing / pressed Ctrl-D immediately.
async fn interactive_search_packages(
- already_required: &std::collections::HashSet<String>,
+ already_required: &indexmap::IndexSet<String>,
preferred_stability: Stability,
fixed: bool,
repo_cache: &mozart_registry::cache::Cache,
@@ -359,8 +359,7 @@ pub async fn execute(
let raw_check = package::read_from_file(&composer_path)?;
// Build set of already-required packages
- let mut already_required: std::collections::HashSet<String> =
- std::collections::HashSet::new();
+ let mut already_required: indexmap::IndexSet<String> = indexmap::IndexSet::new();
for k in raw_check.require.keys() {
already_required.insert(k.to_lowercase());
}
@@ -636,7 +635,7 @@ pub async fn execute(
require_dev,
include_dev: dev_mode,
minimum_stability,
- stability_flags: HashMap::new(),
+ stability_flags: IndexMap::new(),
prefer_stable,
prefer_lowest: args.prefer_lowest,
platform: PlatformConfig::new(),
@@ -645,7 +644,7 @@ pub async fn execute(
repositories: std::sync::Arc::new(
mozart_registry::repository::RepositorySet::with_packagist(repo_cache.clone()),
),
- temporary_constraints: HashMap::new(),
+ temporary_constraints: IndexMap::new(),
raw_repositories: raw.repositories.clone(),
root_provide: raw
.provide
@@ -1036,7 +1035,7 @@ mod tests {
require_dev: vec![],
include_dev: false,
minimum_stability: Stability::Stable,
- stability_flags: HashMap::new(),
+ stability_flags: IndexMap::new(),
prefer_stable: true,
prefer_lowest: false,
platform: PlatformConfig::new(),
@@ -1050,10 +1049,10 @@ mod tests {
),
),
),
- temporary_constraints: HashMap::new(),
+ temporary_constraints: IndexMap::new(),
raw_repositories: vec![],
- root_provide: HashMap::new(),
- root_replace: HashMap::new(),
+ root_provide: IndexMap::new(),
+ root_replace: IndexMap::new(),
};
let resolved = resolver::resolve(&request)
@@ -1106,7 +1105,7 @@ mod tests {
require_dev: vec![],
include_dev: false,
minimum_stability: Stability::Stable,
- stability_flags: HashMap::new(),
+ stability_flags: IndexMap::new(),
prefer_stable: true,
prefer_lowest: false,
platform: PlatformConfig::new(),
@@ -1120,10 +1119,10 @@ mod tests {
),
),
),
- temporary_constraints: HashMap::new(),
+ temporary_constraints: IndexMap::new(),
raw_repositories: vec![],
- root_provide: HashMap::new(),
- root_replace: HashMap::new(),
+ root_provide: IndexMap::new(),
+ root_replace: IndexMap::new(),
};
let resolved = resolver::resolve(&request)
diff --git a/crates/mozart/src/commands/search.rs b/crates/mozart/src/commands/search.rs
index accd6af..023bfdf 100644
--- a/crates/mozart/src/commands/search.rs
+++ b/crates/mozart/src/commands/search.rs
@@ -139,7 +139,7 @@ pub async fn execute(
// Deduplicate to unique vendor names (Composer returns vendor-only names
// for SEARCH_VENDOR mode).
- let mut seen = std::collections::HashSet::new();
+ let mut seen = indexmap::IndexSet::new();
let mut vendor_names: Vec<String> = Vec::new();
for r in &results {
let vendor = r.name.split('/').next().unwrap_or("").to_string();
@@ -515,7 +515,7 @@ mod tests {
];
let refs: Vec<&SearchResult> = results.iter().collect();
- let mut seen = std::collections::HashSet::new();
+ let mut seen = indexmap::IndexSet::new();
let mut vendor_names: Vec<String> = Vec::new();
for r in &refs {
let vendor = r.name.split('/').next().unwrap_or("").to_string();
diff --git a/crates/mozart/src/commands/show.rs b/crates/mozart/src/commands/show.rs
index fa77321..c59a595 100644
--- a/crates/mozart/src/commands/show.rs
+++ b/crates/mozart/src/commands/show.rs
@@ -1,8 +1,8 @@
use clap::Args;
+use indexmap::{IndexMap, IndexSet};
use mozart_core::console::Verbosity;
use mozart_core::console_format;
use mozart_core::matches_wildcard;
-use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
#[derive(Args)]
@@ -292,7 +292,7 @@ fn filter_installed_packages<'a>(
// --no-dev: exclude dev packages
if args.no_dev {
- let dev_names: HashSet<String> = installed
+ let dev_names: IndexSet<String> = installed
.dev_package_names
.iter()
.map(|n| n.to_lowercase())
@@ -305,7 +305,7 @@ fn filter_installed_packages<'a>(
let composer_json_path = working_dir.join("composer.json");
if composer_json_path.exists() {
let root = mozart_core::package::read_from_file(&composer_json_path)?;
- let mut direct_names: HashSet<String> =
+ let mut direct_names: IndexSet<String> =
root.require.keys().map(|k| k.to_lowercase()).collect();
if !args.no_dev {
direct_names.extend(root.require_dev.keys().map(|k| k.to_lowercase()));
@@ -812,7 +812,7 @@ async fn execute_locked(
let composer_json_path = working_dir.join("composer.json");
if composer_json_path.exists() {
let root = mozart_core::package::read_from_file(&composer_json_path)?;
- let mut direct_names: HashSet<String> =
+ let mut direct_names: IndexSet<String> =
root.require.keys().map(|k| k.to_lowercase()).collect();
if !args.no_dev {
direct_names.extend(root.require_dev.keys().map(|k| k.to_lowercase()));
@@ -1346,7 +1346,7 @@ fn show_tree(
let root = mozart_core::package::read_from_file(&composer_json_path)?;
// Load all locked packages into a map for quick lookup
- let pkg_map: HashMap<String, &mozart_registry::lockfile::LockedPackage>;
+ let pkg_map: IndexMap<String, &mozart_registry::lockfile::LockedPackage>;
let lock_storage;
if lock_path.exists() {
lock_storage = mozart_registry::lockfile::LockFile::read_from_file(&lock_path)?;
@@ -1357,7 +1357,7 @@ fn show_tree(
.map(|p| (p.name.to_lowercase(), p))
.collect();
} else {
- pkg_map = HashMap::new();
+ pkg_map = IndexMap::new();
}
// Determine roots to display: package filter or full tree
@@ -1389,7 +1389,7 @@ fn show_tree(
);
// Render each root dependency as a tree
- let mut visited_global: HashSet<String> = HashSet::new();
+ let mut visited_global: IndexSet<String> = IndexSet::new();
let count = root_reqs.len();
for (i, (dep_name, dep_constraint)) in root_reqs.iter().enumerate() {
let is_last = i == count - 1;
@@ -1415,10 +1415,10 @@ fn show_tree(
fn print_tree_node(
pkg_name: &str,
constraint: &str,
- pkg_map: &HashMap<String, &mozart_registry::lockfile::LockedPackage>,
+ pkg_map: &IndexMap<String, &mozart_registry::lockfile::LockedPackage>,
prefix: &str,
child_prefix: &str,
- visited: &mut HashSet<String>,
+ visited: &mut IndexSet<String>,
depth: usize,
console: &mozart_core::console::Console,
) {
@@ -1491,7 +1491,7 @@ fn print_tree_node(
);
}
- visited.remove(&key);
+ visited.shift_remove(&key);
} else {
// Package not found in lock file (platform package or not installed)
if !is_platform_package(&key) {
diff --git a/crates/mozart/src/commands/status.rs b/crates/mozart/src/commands/status.rs
index 6d8fc98..29b1e1b 100644
--- a/crates/mozart/src/commands/status.rs
+++ b/crates/mozart/src/commands/status.rs
@@ -1,7 +1,7 @@
use clap::Args;
+use indexmap::IndexMap;
use mozart_core::console::Verbosity;
use sha1::{Digest, Sha1};
-use std::collections::HashMap;
use std::path::{Path, PathBuf};
#[derive(Args)]
@@ -293,8 +293,8 @@ fn make_temp_dir(package_name: &str) -> anyhow::Result<PathBuf> {
/// Recursively hash all files in a directory.
///
/// Returns a map from relative path string to SHA-1 hex digest.
-fn hash_directory(dir: &Path) -> anyhow::Result<HashMap<String, String>> {
- let mut map = HashMap::new();
+fn hash_directory(dir: &Path) -> anyhow::Result<IndexMap<String, String>> {
+ let mut map = IndexMap::new();
hash_dir_recursive(dir, dir, &mut map)?;
Ok(map)
}
@@ -302,7 +302,7 @@ fn hash_directory(dir: &Path) -> anyhow::Result<HashMap<String, String>> {
fn hash_dir_recursive(
root: &Path,
current: &Path,
- map: &mut HashMap<String, String>,
+ map: &mut IndexMap<String, String>,
) -> anyhow::Result<()> {
let entries = match std::fs::read_dir(current) {
Ok(e) => e,
@@ -337,8 +337,8 @@ fn hash_dir_recursive(
/// Compare two hash maps (original vs installed) and return a list of changes.
fn compute_diff(
- original: &HashMap<String, String>,
- installed: &HashMap<String, String>,
+ original: &IndexMap<String, String>,
+ installed: &IndexMap<String, String>,
) -> Vec<FileChange> {
let mut changes: Vec<FileChange> = Vec::new();
@@ -418,7 +418,7 @@ mod tests {
#[test]
fn test_compute_diff_no_changes() {
- let mut map: HashMap<String, String> = HashMap::new();
+ let mut map: IndexMap<String, String> = IndexMap::new();
map.insert("src/Foo.php".to_string(), "abc123".to_string());
map.insert("src/Bar.php".to_string(), "def456".to_string());
@@ -430,10 +430,10 @@ mod tests {
#[test]
fn test_compute_diff_modified() {
- let mut original: HashMap<String, String> = HashMap::new();
+ let mut original: IndexMap<String, String> = IndexMap::new();
original.insert("src/Foo.php".to_string(), "abc123".to_string());
- let mut installed: HashMap<String, String> = HashMap::new();
+ let mut installed: IndexMap<String, String> = IndexMap::new();
installed.insert("src/Foo.php".to_string(), "xyz999".to_string());
let changes = compute_diff(&original, &installed);
@@ -446,9 +446,9 @@ mod tests {
#[test]
fn test_compute_diff_added() {
- let original: HashMap<String, String> = HashMap::new();
+ let original: IndexMap<String, String> = IndexMap::new();
- let mut installed: HashMap<String, String> = HashMap::new();
+ let mut installed: IndexMap<String, String> = IndexMap::new();
installed.insert("src/NewFile.php".to_string(), "aabbcc".to_string());
let changes = compute_diff(&original, &installed);
@@ -461,10 +461,10 @@ mod tests {
#[test]
fn test_compute_diff_removed() {
- let mut original: HashMap<String, String> = HashMap::new();
+ let mut original: IndexMap<String, String> = IndexMap::new();
original.insert("src/OldFile.php".to_string(), "112233".to_string());
- let installed: HashMap<String, String> = HashMap::new();
+ let installed: IndexMap<String, String> = IndexMap::new();
let changes = compute_diff(&original, &installed);
assert_eq!(changes.len(), 1);
@@ -476,12 +476,12 @@ mod tests {
#[test]
fn test_compute_diff_mixed() {
- let mut original: HashMap<String, String> = HashMap::new();
+ let mut original: IndexMap<String, String> = IndexMap::new();
original.insert("src/Unchanged.php".to_string(), "same".to_string());
original.insert("src/Modified.php".to_string(), "old".to_string());
original.insert("src/Removed.php".to_string(), "gone".to_string());
- let mut installed: HashMap<String, String> = HashMap::new();
+ let mut installed: IndexMap<String, String> = IndexMap::new();
installed.insert("src/Unchanged.php".to_string(), "same".to_string());
installed.insert("src/Modified.php".to_string(), "new".to_string());
installed.insert("src/Added.php".to_string(), "extra".to_string());
diff --git a/crates/mozart/src/commands/suggests.rs b/crates/mozart/src/commands/suggests.rs
index 394778f..6d1765e 100644
--- a/crates/mozart/src/commands/suggests.rs
+++ b/crates/mozart/src/commands/suggests.rs
@@ -1,8 +1,10 @@
use clap::Args;
+use indexmap::IndexMap;
+use indexmap::IndexSet;
use mozart_core::console;
use mozart_core::console::Verbosity;
use mozart_core::console_format;
-use std::collections::{BTreeMap, HashMap, HashSet};
+use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
#[derive(Args)]
@@ -76,17 +78,17 @@ pub async fn execute(
};
// 3. Determine direct-deps-only filter
- let (package_filter, direct_deps_only): (HashSet<String>, Option<HashSet<String>>) = {
+ let (package_filter, direct_deps_only): (IndexSet<String>, Option<IndexSet<String>>) = {
if !args.packages.is_empty() {
// Filter by the explicitly named packages
- let filter: HashSet<String> = args.packages.iter().map(|s| s.to_lowercase()).collect();
+ let filter: IndexSet<String> = args.packages.iter().map(|s| s.to_lowercase()).collect();
(filter, None)
} else if args.all {
- (HashSet::new(), None)
+ (IndexSet::new(), None)
} else {
// Default: only direct deps from composer.json
let direct = compute_direct_deps(&working_dir)?;
- (HashSet::new(), Some(direct))
+ (IndexSet::new(), Some(direct))
}
};
@@ -210,7 +212,7 @@ fn collect_suggestions_from_installed(
}
}
- let dev_names: HashSet<String> = installed
+ let dev_names: IndexSet<String> = installed
.dev_package_names
.iter()
.map(|n| n.to_lowercase())
@@ -273,11 +275,11 @@ fn collect_suggestions_from_root(working_dir: &Path) -> anyhow::Result<Vec<Sugge
fn collect_installed_names_from_lock(
working_dir: &Path,
no_dev: bool,
-) -> anyhow::Result<HashSet<String>> {
+) -> anyhow::Result<IndexSet<String>> {
let lock_path = working_dir.join("composer.lock");
let lock = mozart_registry::lockfile::LockFile::read_from_file(&lock_path)?;
- let mut names: HashSet<String> = HashSet::new();
+ let mut names: IndexSet<String> = IndexSet::new();
let mut all_packages: Vec<&mozart_registry::lockfile::LockedPackage> =
lock.packages.iter().collect();
@@ -309,17 +311,17 @@ fn collect_installed_names_from_lock(
fn collect_installed_names_from_installed(
working_dir: &Path,
no_dev: bool,
-) -> anyhow::Result<HashSet<String>> {
+) -> anyhow::Result<IndexSet<String>> {
let vendor_dir = working_dir.join("vendor");
let installed = mozart_registry::installed::InstalledPackages::read(&vendor_dir)?;
- let dev_names: HashSet<String> = installed
+ let dev_names: IndexSet<String> = installed
.dev_package_names
.iter()
.map(|n| n.to_lowercase())
.collect();
- let mut names: HashSet<String> = HashSet::new();
+ let mut names: IndexSet<String> = IndexSet::new();
for pkg in &installed.packages {
if no_dev && dev_names.contains(&pkg.name.to_lowercase()) {
@@ -356,7 +358,7 @@ fn collect_installed_names_from_installed(
fn add_platform_names_from_lock(
lock: &mozart_registry::lockfile::LockFile,
- names: &mut HashSet<String>,
+ names: &mut IndexSet<String>,
) {
// Collect platform keys from the lock's platform and platform_dev objects
if let Some(obj) = lock.platform.as_object() {
@@ -382,13 +384,13 @@ fn is_platform_package(name: &str) -> bool {
// ─── Direct deps helper ───────────────────────────────────────────────────────
-fn compute_direct_deps(working_dir: &Path) -> anyhow::Result<HashSet<String>> {
+fn compute_direct_deps(working_dir: &Path) -> anyhow::Result<IndexSet<String>> {
let composer_json_path = working_dir.join("composer.json");
if !composer_json_path.exists() {
- return Ok(HashSet::new());
+ return Ok(IndexSet::new());
}
let root = mozart_core::package::read_from_file(&composer_json_path)?;
- let mut deps: HashSet<String> = HashSet::new();
+ let mut deps: IndexSet<String> = IndexSet::new();
// Include the root package itself so its suggestions are shown
if !root.name.is_empty() {
deps.insert(root.name.to_lowercase());
@@ -417,7 +419,7 @@ fn sanitize_reason(reason: &str) -> String {
/// If the same source suggests the same target multiple times, the last reason wins.
/// This matches Composer's behavior where map insertion overwrites previous entries.
fn deduplicate_suggestions(suggestions: Vec<Suggestion>) -> Vec<Suggestion> {
- let mut seen: HashMap<(String, String), usize> = HashMap::new();
+ let mut seen: IndexMap<(String, String), usize> = IndexMap::new();
let mut deduped: Vec<Suggestion> = Vec::new();
for s in suggestions {
@@ -648,7 +650,7 @@ mod tests {
];
let refs: Vec<&Suggestion> = suggestions.iter().collect();
- let mut installed: HashSet<String> = HashSet::new();
+ let mut installed: IndexSet<String> = IndexSet::new();
installed.insert("ext-intl".to_string());
installed.insert("ext-mbstring".to_string());
@@ -671,7 +673,7 @@ mod tests {
];
let refs: Vec<&Suggestion> = suggestions.iter().collect();
- let mut filter: HashSet<String> = HashSet::new();
+ let mut filter: IndexSet<String> = IndexSet::new();
filter.insert("vendor/a".to_string());
filter.insert("vendor/c".to_string());
@@ -694,7 +696,7 @@ mod tests {
];
let refs: Vec<&Suggestion> = suggestions.iter().collect();
- let mut direct: HashSet<String> = HashSet::new();
+ let mut direct: IndexSet<String> = IndexSet::new();
direct.insert("vendor/direct".to_string());
let filtered: Vec<&Suggestion> = refs
@@ -715,7 +717,7 @@ mod tests {
make_suggestion("vendor/c", "vendor/z", ""),
];
let refs: Vec<&Suggestion> = suggestions.iter().collect();
- let installed: HashSet<String> = HashSet::new();
+ let installed: IndexSet<String> = IndexSet::new();
let filtered: Vec<&Suggestion> = refs
.iter()
@@ -755,7 +757,7 @@ mod tests {
let suggestions = collect_suggestions_from_locked(working_dir, false).unwrap();
assert_eq!(suggestions.len(), 2);
assert!(suggestions.iter().all(|s| s.source == "vendor/a"));
- let targets: HashSet<&str> = suggestions.iter().map(|s| s.target.as_str()).collect();
+ let targets: IndexSet<&str> = suggestions.iter().map(|s| s.target.as_str()).collect();
assert!(targets.contains("ext-intl"));
assert!(targets.contains("vendor/optional"));
}
diff --git a/crates/mozart/src/commands/update.rs b/crates/mozart/src/commands/update.rs
index 17b7c97..0c25a9e 100644
--- a/crates/mozart/src/commands/update.rs
+++ b/crates/mozart/src/commands/update.rs
@@ -1,10 +1,10 @@
use clap::Args;
+use indexmap::{IndexMap, IndexSet};
use mozart_core::console;
use mozart_core::console_format;
use mozart_core::package::{self, Stability};
use mozart_registry::lockfile;
use mozart_registry::resolver::{self, PlatformConfig, ResolveRequest, ResolvedPackage};
-use std::collections::{HashMap, HashSet};
#[derive(Args)]
pub struct UpdateArgs {
@@ -198,7 +198,7 @@ pub fn compute_update_changes(
dev_mode: bool,
) -> Vec<UpdateChange> {
// Build map of old lock packages keyed by lowercase name -> version string
- let mut old_map: HashMap<String, String> = HashMap::new();
+ let mut old_map: IndexMap<String, String> = IndexMap::new();
if let Some(old) = old_lock {
for pkg in &old.packages {
old_map.insert(pkg.name.to_lowercase(), pkg.version.clone());
@@ -211,7 +211,7 @@ pub fn compute_update_changes(
}
// Build map of new lock packages keyed by lowercase name -> version string
- let mut new_map: HashMap<String, String> = HashMap::new();
+ let mut new_map: IndexMap<String, String> = IndexMap::new();
for pkg in &new_lock.packages {
new_map.insert(pkg.name.to_lowercase(), pkg.version.clone());
}
@@ -302,11 +302,11 @@ pub fn apply_partial_update(
update_packages: &[String],
) -> Vec<ResolvedPackage> {
// Build a set of normalized package names we want to update
- let update_set: std::collections::HashSet<String> =
+ let update_set: indexmap::IndexSet<String> =
update_packages.iter().map(|s| s.to_lowercase()).collect();
// Build a map of old locked packages by name -> (version, version_normalized, is_dev)
- let mut old_pkg_map: HashMap<String, &lockfile::LockedPackage> = HashMap::new();
+ let mut old_pkg_map: IndexMap<String, &lockfile::LockedPackage> = IndexMap::new();
for pkg in &old_lock.packages {
old_pkg_map.insert(pkg.name.to_lowercase(), pkg);
}
@@ -414,7 +414,7 @@ pub fn expand_wildcards(
.collect();
let mut result: Vec<String> = Vec::new();
- let mut seen: HashSet<String> = HashSet::new();
+ let mut seen: IndexSet<String> = IndexSet::new();
for spec in specifiers {
if spec.contains('*') {
@@ -448,8 +448,8 @@ pub fn expand_wildcards(
// ─────────────────────────────────────────────────────────────────────────────
/// Build a lookup map from package name (lowercase) to its LockedPackage.
-fn build_lock_map(lock: &lockfile::LockFile) -> HashMap<String, &lockfile::LockedPackage> {
- let mut map = HashMap::new();
+fn build_lock_map(lock: &lockfile::LockFile) -> IndexMap<String, &lockfile::LockedPackage> {
+ let mut map = IndexMap::new();
for pkg in &lock.packages {
map.insert(pkg.name.to_lowercase(), pkg);
}
@@ -468,7 +468,7 @@ pub fn expand_with_direct_dependencies(
lock: &lockfile::LockFile,
) -> Vec<String> {
let lock_map = build_lock_map(lock);
- let mut result_set: HashSet<String> = packages.iter().cloned().collect();
+ let mut result_set: IndexSet<String> = packages.iter().cloned().collect();
let mut result: Vec<String> = packages;
for name in result.clone() {
@@ -503,7 +503,7 @@ pub fn expand_with_all_dependencies(
lock: &lockfile::LockFile,
) -> Vec<String> {
let lock_map = build_lock_map(lock);
- let mut result_set: HashSet<String> = packages.iter().cloned().collect();
+ let mut result_set: IndexSet<String> = packages.iter().cloned().collect();
let mut queue: Vec<String> = packages.clone();
let mut result: Vec<String> = packages;
@@ -665,7 +665,7 @@ pub fn apply_patch_only(
resolved: Vec<ResolvedPackage>,
old_lock: &lockfile::LockFile,
) -> Vec<ResolvedPackage> {
- let mut old_pkg_map: HashMap<String, &lockfile::LockedPackage> = HashMap::new();
+ let mut old_pkg_map: IndexMap<String, &lockfile::LockedPackage> = IndexMap::new();
for pkg in &old_lock.packages {
old_pkg_map.insert(pkg.name.to_lowercase(), pkg);
}
@@ -806,7 +806,7 @@ pub async fn run(
let dev_mode = !args.no_dev;
// Fix 1C + Fix 2: Parse --with constraints and inline constraint shorthand.
- let mut temporary_constraints: HashMap<String, String> = HashMap::new();
+ let mut temporary_constraints: IndexMap<String, String> = IndexMap::new();
// Parse --with constraints (format: "vendor/package:constraint")
for with_entry in &args.with {
@@ -887,7 +887,7 @@ pub async fn run(
require_dev,
include_dev: dev_mode,
minimum_stability,
- stability_flags: HashMap::new(),
+ stability_flags: IndexMap::new(),
prefer_stable,
prefer_lowest: args.prefer_lowest,
platform,
@@ -1166,7 +1166,7 @@ pub async fn run(
let bump_require_dev = mode == "all" || mode == "dev";
// Build locked versions map from the new lock
- let mut locked_versions: HashMap<String, (String, Option<String>)> = HashMap::new();
+ let mut locked_versions: IndexMap<String, (String, Option<String>)> = IndexMap::new();
for pkg in &new_lock.packages {
locked_versions.insert(
pkg.name.to_lowercase(),
@@ -1997,7 +1997,7 @@ mod tests {
require_dev: vec![],
include_dev: false,
minimum_stability: Stability::Stable,
- stability_flags: HashMap::new(),
+ stability_flags: IndexMap::new(),
prefer_stable: true,
prefer_lowest: false,
platform: PlatformConfig::new(),
@@ -2011,10 +2011,10 @@ mod tests {
),
),
),
- temporary_constraints: HashMap::new(),
+ temporary_constraints: IndexMap::new(),
raw_repositories: vec![],
- root_provide: HashMap::new(),
- root_replace: HashMap::new(),
+ root_provide: IndexMap::new(),
+ root_replace: IndexMap::new(),
};
let resolved = resolve(&request).await.expect("Resolution should succeed");
diff --git a/crates/mozart/src/commands/validate.rs b/crates/mozart/src/commands/validate.rs
index cec36b5..e2345c8 100644
--- a/crates/mozart/src/commands/validate.rs
+++ b/crates/mozart/src/commands/validate.rs
@@ -421,7 +421,7 @@ fn check_scripts_orphans(
obj: &serde_json::Map<String, serde_json::Value>,
result: &mut ValidationResult,
) {
- let script_keys: std::collections::HashSet<&str> = obj
+ let script_keys: indexmap::IndexSet<&str> = obj
.get("scripts")
.and_then(|v| v.as_object())
.map(|m| m.keys().map(|k| k.as_str()).collect())