diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-05-03 11:55:03 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-05-03 11:55:03 +0900 |
| commit | ae1aa6540761e54a76b8f7984cf93cd3a0d011d0 (patch) | |
| tree | f111e1c73977f0bffb6323b03f4210269b43b297 /crates/mozart/src | |
| parent | 30ae6c869adc7f3cb87a4d63edd6d0cda89d571d (diff) | |
| download | php-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.rs | 4 | ||||
| -rw-r--r-- | crates/mozart/src/commands/bump.rs | 6 | ||||
| -rw-r--r-- | crates/mozart/src/commands/check_platform_reqs.rs | 2 | ||||
| -rw-r--r-- | crates/mozart/src/commands/create_project.rs | 6 | ||||
| -rw-r--r-- | crates/mozart/src/commands/dependency.rs | 13 | ||||
| -rw-r--r-- | crates/mozart/src/commands/exec.rs | 3 | ||||
| -rw-r--r-- | crates/mozart/src/commands/install.rs | 15 | ||||
| -rw-r--r-- | crates/mozart/src/commands/licenses.rs | 4 | ||||
| -rw-r--r-- | crates/mozart/src/commands/outdated.rs | 10 | ||||
| -rw-r--r-- | crates/mozart/src/commands/reinstall.rs | 4 | ||||
| -rw-r--r-- | crates/mozart/src/commands/remove.rs | 30 | ||||
| -rw-r--r-- | crates/mozart/src/commands/repository.rs | 2 | ||||
| -rw-r--r-- | crates/mozart/src/commands/require.rs | 27 | ||||
| -rw-r--r-- | crates/mozart/src/commands/search.rs | 4 | ||||
| -rw-r--r-- | crates/mozart/src/commands/show.rs | 20 | ||||
| -rw-r--r-- | crates/mozart/src/commands/status.rs | 30 | ||||
| -rw-r--r-- | crates/mozart/src/commands/suggests.rs | 44 | ||||
| -rw-r--r-- | crates/mozart/src/commands/update.rs | 36 | ||||
| -rw-r--r-- | crates/mozart/src/commands/validate.rs | 2 |
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()) |
