diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-06-08 11:26:03 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-06-08 11:26:03 +0900 |
| commit | e5b789616ec4c1cbd152c5ccbefe2d27ced4a18f (patch) | |
| tree | 7a713cf1bcec30a1c69d5c434d8df6a7525dcbc2 /crates | |
| parent | 7439cdb08afe0882186a34f70c1e8878fcb7dca5 (diff) | |
| download | php-shirabe-e5b789616ec4c1cbd152c5ccbefe2d27ced4a18f.tar.gz php-shirabe-e5b789616ec4c1cbd152c5ccbefe2d27ced4a18f.tar.zst php-shirabe-e5b789616ec4c1cbd152c5ccbefe2d27ced4a18f.zip | |
feat(phase-c): resolve PHP-array-semantics phase-b TODOs
Resolve category K (array_* functions, integer keys, nested mutation,
sorting). Add shim variants (uasort over Vec<T>, uasort_map for IndexMap)
and delegate to existing typed variants (strtr_array, array_merge_map,
array_search_in_vec). Implement PHP array semantics directly where the
shape is fixed: canonical integer-key coercion (is_php_integer_key,
shared by config and FilesystemRepository::dumpToPhpCode), strict
array_search via trait-object pointer identity, array_reverse/array_chunk
preserve_keys loops, and the installed.php nested version mutations via
auto-vivify helpers.
Resolving the array_merge in UpdateCommand unmasked latent borrow bugs in
execute's tail (Rc input/output moved by value); fixed with .clone() to
match PHP reference sharing, and resolved the tightly-coupled Intervals
constraint check.
composerRequire reclassified to phase-c: it depends on the $GLOBALS
superglobal and PHP's require include mechanism, neither portable.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diffstat (limited to 'crates')
| -rw-r--r-- | crates/shirabe-php-shim/src/lib.rs | 11 | ||||
| -rw-r--r-- | crates/shirabe/src/autoload/autoload_generator.rs | 28 | ||||
| -rw-r--r-- | crates/shirabe/src/command/require_command.rs | 7 | ||||
| -rw-r--r-- | crates/shirabe/src/command/update_command.rs | 89 | ||||
| -rw-r--r-- | crates/shirabe/src/config.rs | 22 | ||||
| -rw-r--r-- | crates/shirabe/src/dependency_resolver/pool_builder.rs | 6 | ||||
| -rw-r--r-- | crates/shirabe/src/dependency_resolver/transaction.rs | 22 | ||||
| -rw-r--r-- | crates/shirabe/src/installer/installation_manager.rs | 8 | ||||
| -rw-r--r-- | crates/shirabe/src/io/console_io.rs | 2 | ||||
| -rw-r--r-- | crates/shirabe/src/repository/filesystem_repository.rs | 125 | ||||
| -rw-r--r-- | crates/shirabe/src/repository/repository_set.rs | 16 | ||||
| -rw-r--r-- | crates/shirabe/src/util/auth_helper.rs | 1 | ||||
| -rw-r--r-- | crates/shirabe/src/util/process_executor.rs | 11 |
13 files changed, 205 insertions, 143 deletions
diff --git a/crates/shirabe-php-shim/src/lib.rs b/crates/shirabe-php-shim/src/lib.rs index ad2736b..835e1c1 100644 --- a/crates/shirabe-php-shim/src/lib.rs +++ b/crates/shirabe-php-shim/src/lib.rs @@ -1614,9 +1614,16 @@ pub fn php_uname(_mode: &str) -> String { todo!() } -pub fn uasort<F>(_array: &mut Vec<String>, _compare: F) +pub fn uasort<T, F>(_array: &mut Vec<T>, _compare: F) where - F: Fn(&str, &str) -> i64, + F: FnMut(&T, &T) -> i64, +{ + todo!() +} + +pub fn uasort_map<K, V, F>(_array: &mut IndexMap<K, V>, _compare: F) +where + F: FnMut(&V, &V) -> i64, { todo!() } diff --git a/crates/shirabe/src/autoload/autoload_generator.rs b/crates/shirabe/src/autoload/autoload_generator.rs index a0e51e9..69ecb73 100644 --- a/crates/shirabe/src/autoload/autoload_generator.rs +++ b/crates/shirabe/src/autoload/autoload_generator.rs @@ -1628,11 +1628,27 @@ impl AutoloadGenerator { }; let mut autoload = package.get_autoload(); - // PHP comparison: $package === $rootPackage (object identity). We compare by name as best-effort. - let is_root = package.get_name() == root_package.get_name(); + let is_root = package.ptr_eq(&root_package.clone().into()); if self.dev_mode.unwrap_or(false) && is_root { - // TODO(phase-b): array_merge_recursive semantics (nested merge) not preserved - autoload.extend(root_package.get_dev_autoload()); + let merged = array_merge_recursive(vec![ + PhpMixed::Array( + autoload + .into_iter() + .map(|(k, v)| (k, Box::new(v))) + .collect(), + ), + PhpMixed::Array( + root_package + .get_dev_autoload() + .into_iter() + .map(|(k, v)| (k, Box::new(v))) + .collect(), + ), + ]); + autoload = match merged { + PhpMixed::Array(m) => m.into_iter().map(|(k, v)| (k, *v)).collect(), + _ => IndexMap::new(), + }; } // skip misconfigured packages @@ -1914,7 +1930,9 @@ impl AutoloadGenerator { } pub fn composer_require(_file_identifier: &str, _file: &str) { - // TODO(phase-b): PHP GLOBALS nested array access not supported + // TODO(phase-c): unportable — depends on the $GLOBALS superglobal + // ($GLOBALS['__composer_autoload_files']) and PHP's `require $file` include + // mechanism (see also ClassLoader's include closure), neither of which is modeled. todo!() } diff --git a/crates/shirabe/src/command/require_command.rs b/crates/shirabe/src/command/require_command.rs index 0ec809c..7fa4793 100644 --- a/crates/shirabe/src/command/require_command.rs +++ b/crates/shirabe/src/command/require_command.rs @@ -9,9 +9,8 @@ use shirabe_external_packages::symfony::console::input::InputInterface; use shirabe_external_packages::symfony::console::output::OutputInterface; use shirabe_php_shim::{ PhpMixed, RuntimeException, UnexpectedValueException, array_fill_keys, array_intersect, - array_keys, array_map, array_merge, array_merge_recursive, array_unique, count, empty, - file_exists, file_get_contents, file_put_contents, filesize, implode, is_writable, sprintf, - strtolower, unlink, + array_keys, array_map, array_merge, array_unique, count, empty, file_exists, file_get_contents, + file_put_contents, filesize, implode, is_writable, sprintf, strtolower, unlink, }; use crate::advisory::Auditor; @@ -396,8 +395,6 @@ impl RequireCommand { } else { "it is" }; - // TODO(phase-b): PHP's array_merge_recursive + array_unique on a list of - // string lists; collapsed here to a flat unique Vec<String>. let merged: Vec<String> = dev_packages.iter().flatten().cloned().collect(); let pkg_dev_tags: Vec<String> = array_unique(&merged); let warn_msg = format!( diff --git a/crates/shirabe/src/command/update_command.rs b/crates/shirabe/src/command/update_command.rs index 4fe9b6a..a4fbf99 100644 --- a/crates/shirabe/src/command/update_command.rs +++ b/crates/shirabe/src/command/update_command.rs @@ -10,7 +10,7 @@ use shirabe_external_packages::symfony::console::input::InputInterface; use shirabe_external_packages::symfony::console::output::OutputInterface; use shirabe_php_shim::{ InvalidArgumentException, PhpMixed, RuntimeException, array_filter, array_intersect, - array_keys, array_merge, array_search, count, empty, in_array, sprintf, strtolower, + array_keys, array_merge_map, array_search_in_vec, count, empty, in_array, sprintf, strtolower, }; use shirabe_semver::constraint::MultiConstraint; use shirabe_semver::intervals::Intervals; @@ -145,16 +145,9 @@ impl UpdateCommand { // replace the foo/bar:req by foo/bar in the allowlist for package in &allowlist_packages_with_requirements { - let package_name = Preg::replace(r"{^([^ =:]+)[ =:].*$}", "$1", package); - let index = array_search( - package, - // TODO(phase-b): array_search expects IndexMap<String, String>; supply a wrapper - todo!("packages as IndexMap<String, String>"), - ); - if let Some(idx) = index { - // TODO(phase-b): mutate packages[idx] — PHP integer-keyed array - let _ = idx; - let _ = package_name; + let package_name = Preg::replace(r"{^([^ =:]+)[ =:].*$}", "$1", package)?; + if let Some(idx) = array_search_in_vec(package, &packages) { + packages[idx] = package_name; } } } @@ -172,25 +165,20 @@ impl UpdateCommand { let parser = VersionParser::new(); let mut temporary_constraints: IndexMap<String, _> = IndexMap::new(); - let root_requirements = array_merge( - // TODO(phase-b): array_merge for IndexMap<String, Link> - todo!("root_package.get_requires() as PhpMixed"), - todo!("root_package.get_dev_requires() as PhpMixed"), - ); + let root_requirements = + array_merge_map(root_package.get_requires(), root_package.get_dev_requires()); for (package, constraint) in &reqs { let package = strtolower(package); let parsed_constraint = parser.parse_constraints(constraint)?; - temporary_constraints.insert(package.clone(), parsed_constraint); - // TODO(phase-b): access root_requirements[package].getConstraint() - let intersected: bool = todo!("Intervals::haveIntersections check"); - if let Some(_root_req) = todo!("root_requirements.get(&package)") as Option<PhpMixed> { - if !intersected { + temporary_constraints.insert(package.clone(), parsed_constraint.clone()); + if let Some(root_req) = root_requirements.get(&package) { + if !Intervals::have_intersections(&parsed_constraint, root_req.get_constraint())? { io.write_error3( &format!( "<error>The temporary constraint \"{}\" for \"{}\" must be a subset of the constraint in your composer.json ({})</error>", constraint, package, - todo!("root_requirements[package].get_pretty_constraint()"), + root_req.get_pretty_constraint(), ), true, io_interface::NORMAL, @@ -263,8 +251,8 @@ impl UpdateCommand { { packages = self.get_packages_interactively( io.clone(), - input, - output, + input.clone(), + output.clone(), &composer_handle, packages, )?; @@ -283,18 +271,7 @@ impl UpdateCommand { .as_bool() .unwrap_or(false) { - requires = array_merge( - // TODO(phase-b): array_merge for Vec<String> - todo!("requires as PhpMixed"), - todo!("array_keys(&root_package.get_dev_requires()) as PhpMixed"), - ) - .as_list() - .map(|l| { - l.iter() - .filter_map(|v| v.as_string().map(|s| s.to_string())) - .collect() - }) - .unwrap_or_default(); + requires.extend(array_keys(&root_package.get_dev_requires())); } if !packages.is_empty() { @@ -327,7 +304,8 @@ impl UpdateCommand { return Ok(-1); } - let mut command_event = CommandEvent::new(PluginEvents::COMMAND, "update", input, output); + let mut command_event = + CommandEvent::new(PluginEvents::COMMAND, "update", input.clone(), output); // TODO(phase-b): dispatch should accept the CommandEvent itself; passing the // event by name only for now to keep types aligned with EventDispatcher::dispatch. composer @@ -350,7 +328,7 @@ impl UpdateCommand { let config = composer.get_config(); let (prefer_source, prefer_dist) = - self.get_preferred_install_options(&*config.borrow(), input, false)?; + self.get_preferred_install_options(&*config.borrow(), input.clone(), false)?; let optimize = input .borrow() @@ -464,7 +442,7 @@ impl UpdateCommand { .set_update_mirrors(update_mirrors) .set_update_allow_list(packages.clone()) .set_update_allow_transitive_dependencies(update_allow_transitive_dependencies)? - .set_platform_requirement_filter(self.get_platform_requirement_filter(input)?) + .set_platform_requirement_filter(self.get_platform_requirement_filter(input.clone())?) .set_prefer_stable( input .borrow() @@ -487,7 +465,7 @@ impl UpdateCommand { IndexMap::new() }) .set_audit_config( - self.create_audit_config(&mut *composer.get_config().borrow_mut(), input)?, + self.create_audit_config(&mut *composer.get_config().borrow_mut(), input.clone())?, ) .set_minimal_update(minimal_changes); @@ -565,10 +543,9 @@ impl UpdateCommand { let composer_ref = crate::command::composer_full(composer); let platform_req_filter = self.get_platform_requirement_filter(input); let stability_flags = composer_ref.get_package().get_stability_flags(); - let requires = array_merge( - // TODO(phase-b): array_merge for IndexMap<String, Link> - todo!("composer.get_package().get_requires() as PhpMixed"), - todo!("composer.get_package().get_dev_requires() as PhpMixed"), + let requires = array_merge_map( + composer_ref.get_package().get_requires(), + composer_ref.get_package().get_dev_requires(), ); let filter: Option<String> = if packages.len() > 0 { @@ -606,21 +583,27 @@ impl UpdateCommand { } } let current_version = package.get_pretty_version(); - // TODO(phase-b): pull from requires[package.get_name()].get_pretty_constraint() - let constraint: Option<&str> = None; - // TODO(phase-b): derive from stabilityFlags / minimum_stability - let stability: &str = "stable"; + let constraint = requires + .get(&package.get_name()) + .map(|link| link.get_pretty_constraint()); + let stability = match stability_flags.get(&package.get_name()) { + Some(flag) => base_package::STABILITIES + .iter() + .find(|&(_, v)| v == flag) + .map(|(k, _)| k.to_string()) + .unwrap_or_default(), + None => composer_ref.get_package().get_minimum_stability(), + }; let latest_version = version_selector.find_best_candidate( &package.get_name(), constraint, - stability, + &stability, None, 0, None, PhpMixed::Bool(true), )?; let _ = &platform_req_filter; - let _ = &stability_flags; if let Some(latest) = latest_version { if package.get_version() != latest.get_version() || latest.is_dev() { autocompleter_values.insert( @@ -635,11 +618,7 @@ impl UpdateCommand { } } if 0 == installed_packages.len() { - // TODO(phase-b): iterate composer.get_package().get_requires() merged with - // get_dev_requires(); requires is currently a PhpMixed placeholder. - let _ = &requires; - let _empty: IndexMap<String, ()> = IndexMap::new(); - for (req, _constraint) in &_empty { + for (req, _constraint) in &requires { if PlatformRepository::is_platform_package(req) { continue; } diff --git a/crates/shirabe/src/config.rs b/crates/shirabe/src/config.rs index e036625..5333173 100644 --- a/crates/shirabe/src/config.rs +++ b/crates/shirabe/src/config.rs @@ -512,8 +512,7 @@ impl Config { } // store repo - // TODO(phase-b): is_int($name) where $name is an IndexMap key (PHP string-or-int) - let is_numeric_name = name.parse::<i64>().is_ok(); + let is_numeric_name = is_php_integer_key(name); if is_numeric_name { if !self.repositories.contains_key(name) { self.repositories.insert(name.clone(), repository.clone()); @@ -1278,3 +1277,22 @@ impl Config { ProcessExecutor::set_timeout(0); } } + +/// Whether a string array key would have been coerced to an integer key by PHP. +/// +/// PHP stores a string array key as an integer when it is a canonical decimal +/// representation: "0", or an optionally negative sequence of digits with no +/// leading zero and no leading plus sign, that fits in a platform integer. +pub(crate) fn is_php_integer_key(key: &str) -> bool { + if key == "0" { + return true; + } + let digits = key.strip_prefix('-').unwrap_or(key); + if digits.is_empty() || digits.as_bytes()[0] == b'0' { + return false; + } + if !digits.bytes().all(|b| b.is_ascii_digit()) { + return false; + } + key.parse::<i64>().is_ok() +} diff --git a/crates/shirabe/src/dependency_resolver/pool_builder.rs b/crates/shirabe/src/dependency_resolver/pool_builder.rs index 20b96e0..8647cef 100644 --- a/crates/shirabe/src/dependency_resolver/pool_builder.rs +++ b/crates/shirabe/src/dependency_resolver/pool_builder.rs @@ -7,9 +7,8 @@ use shirabe_external_packages::composer::pcre::Preg; use shirabe_external_packages::composer::semver::CompilingMatcher; use shirabe_external_packages::composer::semver::Intervals; use shirabe_php_shim::{ - LogicException, PhpMixed, array_chunk, array_flip, array_flip_strings, array_map, array_merge, - array_search, array_search_mixed, count, in_array, microtime, number_format, round, sprintf, - strpos, + LogicException, PhpMixed, array_flip, array_flip_strings, array_map, array_merge, array_search, + array_search_mixed, count, in_array, microtime, number_format, round, sprintf, strpos, }; use shirabe_semver::constraint::AnyConstraint; use shirabe_semver::constraint::MatchAllConstraint; @@ -474,7 +473,6 @@ impl PoolBuilder { } // Load packages in chunks of 50 to prevent memory usage build-up due to caches of all sorts - // TODO(phase-b): array_chunk shim signature expects &[T]; build IndexMap chunks manually. let mut package_batches: Vec<IndexMap<String, AnyConstraint>> = { let mut chunks: Vec<IndexMap<String, AnyConstraint>> = Vec::new(); let mut current: IndexMap<String, AnyConstraint> = IndexMap::new(); diff --git a/crates/shirabe/src/dependency_resolver/transaction.rs b/crates/shirabe/src/dependency_resolver/transaction.rs index 3450815..d20659c 100644 --- a/crates/shirabe/src/dependency_resolver/transaction.rs +++ b/crates/shirabe/src/dependency_resolver/transaction.rs @@ -4,6 +4,7 @@ use indexmap::IndexMap; use indexmap::IndexSet; use shirabe_php_shim::{ PhpMixed, array_filter, array_intersect, array_keys, array_pop, array_unshift, strcmp, uasort, + uasort_map, }; use crate::dependency_resolver::operation::InstallOperation; @@ -70,9 +71,7 @@ impl Transaction { /// @param PackageInterface[] $resultPackages fn set_result_package_maps(&mut self, result_packages: Vec<PackageInterfaceHandle>) { - // PHP: static function (PackageInterface $a, PackageInterface $b): int { ... }; - // TODO(phase-b): bridge the closure to uasort's argument type - let _package_sort = |a: &PackageInterfaceHandle, b: &PackageInterfaceHandle| -> i64 { + let package_sort = |a: &PackageInterfaceHandle, b: &PackageInterfaceHandle| -> i64 { // sort alias packages by the same name behind their non alias version if a.get_name() == b.get_name() { let a_is_alias = a.as_alias().is_some(); @@ -100,17 +99,12 @@ impl Transaction { .insert(package.ptr_id().to_string(), package); } - // TODO(phase-b): uasort signature mismatch — needs to operate on the IndexMap with a PackageInterface comparator - uasort( - todo!("&mut self.result_package_map"), - |_a: &str, _b: &str| -> i64 { todo!("package_sort") }, - ); + uasort_map(&mut self.result_package_map, package_sort); let names: Vec<String> = self.result_packages_by_name.keys().cloned().collect(); - for _name in &names { - uasort( - todo!("&mut self.result_packages_by_name[name]"), - |_a: &str, _b: &str| -> i64 { todo!("package_sort") }, - ); + for name in &names { + if let Some(packages) = self.result_packages_by_name.get_mut(name) { + uasort(packages, package_sort); + } } } @@ -297,8 +291,6 @@ impl Transaction { let mut plugins_with_deps: Vec<std::rc::Rc<dyn OperationInterface>> = vec![]; let mut plugin_requires: Vec<String> = vec![]; - // PHP: foreach (array_reverse($operations, true) as $idx => $op) - // TODO(phase-b): array_reverse preserves keys (true); iterate indices in reverse to mimic let mut to_remove: Vec<usize> = vec![]; for idx in (0..operations.len()).rev() { let op = &operations[idx]; diff --git a/crates/shirabe/src/installer/installation_manager.rs b/crates/shirabe/src/installer/installation_manager.rs index 17d284d..4a13e6b 100644 --- a/crates/shirabe/src/installer/installation_manager.rs +++ b/crates/shirabe/src/installer/installation_manager.rs @@ -79,9 +79,11 @@ impl InstallationManager { /// /// @param InstallerInterface $installer installer instance pub fn remove_installer(&mut self, installer: &dyn InstallerInterface) { - // TODO(phase-b): array_search for trait object identity needs concrete type info - let _ = installer; - let key: Option<usize> = None; + let target = installer as *const dyn InstallerInterface as *const (); + let key = self + .installers + .iter() + .position(|inst| inst.as_ref() as *const dyn InstallerInterface as *const () == target); if let Some(k) = key { array_splice(&mut self.installers, k as i64, Some(1), vec![]); self.cache = IndexMap::new(); diff --git a/crates/shirabe/src/io/console_io.rs b/crates/shirabe/src/io/console_io.rs index 987819d..a4a3c21 100644 --- a/crates/shirabe/src/io/console_io.rs +++ b/crates/shirabe/src/io/console_io.rs @@ -589,8 +589,6 @@ impl IOInterfaceImmutable for ConsoleIO { } if !is_array(&result) { - // PHP: (string) array_search($result, $choices, true) - // TODO(phase-b): array_search signature requires IndexMap<String, String> let result_str = result.as_string().unwrap_or("").to_string(); let haystack: IndexMap<String, String> = match &choices { PhpMixed::List(l) => l diff --git a/crates/shirabe/src/repository/filesystem_repository.rs b/crates/shirabe/src/repository/filesystem_repository.rs index fd7349a..4a18dc9 100644 --- a/crates/shirabe/src/repository/filesystem_repository.rs +++ b/crates/shirabe/src/repository/filesystem_repository.rs @@ -9,10 +9,11 @@ use shirabe_external_packages::composer::pcre::Preg; use shirabe_php_shim::{ Exception, InvalidArgumentException, LogicException, PhpMixed, SORT_NATURAL, UnexpectedValueException, array_flip, dirname, r#eval, file_get_contents, get_class, - get_class_err, get_debug_type, in_array, is_array, is_int, is_null, is_string, ksort, php_dir, + get_class_err, get_debug_type, in_array, is_array, is_null, is_string, ksort, php_dir, realpath, sort, sort_with_flags, str_repeat, strtr, trim, usort, var_export, }; +use crate::config::is_php_integer_key; use crate::installed_versions::InstalledVersions; use crate::installer::InstallationManager; use crate::json::JsonFile; @@ -394,8 +395,7 @@ impl FilesystemRepository { for (key, value) in array { lines.push_str(&str_repeat(" ", level as usize)); - lines.push_str(&if is_int(&PhpMixed::String(key.clone())) { - // TODO(phase-b): PHP integer-keyed array entries — IndexMap keys are strings + lines.push_str(&if is_php_integer_key(key) { format!("{} => ", key) } else { format!("{} => ", var_export(&PhpMixed::String(key.clone()), true)) @@ -548,54 +548,56 @@ impl FilesystemRepository { if PlatformRepository::is_platform_package(replace.get_target()) { continue; } - // PHP: dev_requirement handling - // TODO(phase-b): mutate nested versions['versions'][$replace->getTarget()]['dev_requirement'] - todo!("mutate nested versions['versions'][target]['dev_requirement']"); - #[allow(unreachable_code)] - { - let mut replaced = replace.get_pretty_constraint().to_string(); - if replaced == "self.version" { - replaced = package.get_pretty_version().to_string(); - } - // TODO(phase-b): mutate nested versions['versions'][$replace->getTarget()]['replaced'] - todo!("append replaced to versions['versions'][target]['replaced']"); + let mut replaced = replace.get_pretty_constraint().to_string(); + if replaced == "self.version" { + replaced = package.get_pretty_version().to_string(); } + record_replace_or_provide( + &mut versions, + replace.get_target(), + "replaced", + replaced, + is_dev_package, + ); } for (_, provide) in package.get_provides() { // exclude platform provides as when they are really there we can not check for their presence if PlatformRepository::is_platform_package(provide.get_target()) { continue; } - // TODO(phase-b): mutate nested versions['versions'][$provide->getTarget()]['dev_requirement'] - todo!("mutate nested versions['versions'][target]['dev_requirement']"); - #[allow(unreachable_code)] - { - let mut provided = provide.get_pretty_constraint().to_string(); - if provided == "self.version" { - provided = package.get_pretty_version().to_string(); - } - // TODO(phase-b): mutate nested versions['versions'][$provide->getTarget()]['provided'] - todo!("append provided to versions['versions'][target]['provided']"); + let mut provided = provide.get_pretty_constraint().to_string(); + if provided == "self.version" { + provided = package.get_pretty_version().to_string(); } + record_replace_or_provide( + &mut versions, + provide.get_target(), + "provided", + provided, + is_dev_package, + ); } } // add aliases for package in &packages { - let Some(_alias) = package.as_alias() else { + if package.as_alias().is_none() { continue; - }; - // TODO(phase-b): mutate nested versions['versions'][name]['aliases'] - todo!("append alias->getPrettyVersion() to versions['versions'][name]['aliases']"); - #[allow(unreachable_code)] + } + let pretty = package.get_pretty_version().to_string(); + push_to_list( + versions_entry(&mut versions, &package.get_name()), + "aliases", + pretty.clone(), + ); if package.as_root().is_some() { - // TODO(phase-b): same mutation on versions['root']['aliases'] - todo!("append alias->getPrettyVersion() to versions['root']['aliases']"); + if let Some(PhpMixed::Array(root_map)) = versions.get_mut("root") { + push_to_list(root_map, "aliases", pretty); + } } } if let Some(PhpMixed::Array(versions_map)) = versions.get_mut("versions") { - // TODO(phase-b): ksort signature mismatch on nested IndexMap; cast appropriately ksort(versions_map); } ksort(&mut versions); @@ -767,3 +769,62 @@ impl FilesystemRepository { result } } + +fn versions_entry<'a>( + versions: &'a mut IndexMap<String, PhpMixed>, + target: &str, +) -> &'a mut IndexMap<String, Box<PhpMixed>> { + let versions_map = match versions.get_mut("versions") { + Some(PhpMixed::Array(m)) => m, + _ => unreachable!("versions['versions'] is always an array"), + }; + match versions_map + .entry(target.to_string()) + .or_insert_with(|| Box::new(PhpMixed::Array(IndexMap::new()))) + .as_mut() + { + PhpMixed::Array(m) => m, + _ => unreachable!("versions['versions'][target] is always an array"), + } +} + +fn push_to_list(entry: &mut IndexMap<String, Box<PhpMixed>>, key: &str, value: String) { + if let PhpMixed::List(list) = entry + .entry(key.to_string()) + .or_insert_with(|| Box::new(PhpMixed::List(vec![]))) + .as_mut() + { + list.push(Box::new(PhpMixed::String(value))); + } +} + +fn record_replace_or_provide( + versions: &mut IndexMap<String, PhpMixed>, + target: &str, + key: &str, + value: String, + is_dev_package: bool, +) { + let entry = versions_entry(versions, target); + if !entry.contains_key("dev_requirement") { + entry.insert( + "dev_requirement".to_string(), + Box::new(PhpMixed::Bool(is_dev_package)), + ); + } else if !is_dev_package { + entry.insert( + "dev_requirement".to_string(), + Box::new(PhpMixed::Bool(false)), + ); + } + let already_present = match entry.get(key) { + Some(b) => matches!( + b.as_ref(), + PhpMixed::List(list) if list.iter().any(|v| v.as_string() == Some(value.as_str())) + ), + None => false, + }; + if !already_present { + push_to_list(entry, key, value); + } +} diff --git a/crates/shirabe/src/repository/repository_set.rs b/crates/shirabe/src/repository/repository_set.rs index aa5f128..f632fbc 100644 --- a/crates/shirabe/src/repository/repository_set.rs +++ b/crates/shirabe/src/repository/repository_set.rs @@ -5,8 +5,7 @@ use std::any::Any; use anyhow::Result; use indexmap::IndexMap; use shirabe_php_shim::{ - LogicException, PhpMixed, RuntimeException, array_merge, array_merge_recursive, ksort, - strtolower, + LogicException, PhpMixed, RuntimeException, array_merge, ksort, strtolower, }; use shirabe_semver::constraint::AnyConstraint; use shirabe_semver::constraint::MatchAllConstraint; @@ -411,13 +410,12 @@ impl RepositorySet { } } - let mut advisories = if !repo_advisories.is_empty() { - // PHP: array_merge_recursive([], ...$repoAdvisories) - // TODO(phase-b): array_merge_recursive signature expects PhpMixed arguments - todo!("array_merge_recursive across repo_advisories") - } else { - IndexMap::new() - }; + let mut advisories: IndexMap<String, Vec<AnySecurityAdvisory>> = IndexMap::new(); + for repo in repo_advisories { + for (name, list) in repo { + advisories.entry(name).or_default().extend(list); + } + } ksort(&mut advisories); Ok(advisories) diff --git a/crates/shirabe/src/util/auth_helper.rs b/crates/shirabe/src/util/auth_helper.rs index 7c1691b..b54b54b 100644 --- a/crates/shirabe/src/util/auth_helper.rs +++ b/crates/shirabe/src/util/auth_helper.rs @@ -304,7 +304,6 @@ impl AuthHelper { if let Some(prev_auth) = auth { if self.io.has_authentication(origin) { let current_auth = self.io.get_authentication(origin); - // TODO(phase-b): IndexMap equality compares all entries by value if prev_auth == current_auth { return Err(TransportException::new( format!("Invalid credentials for '{}', aborting.", url), diff --git a/crates/shirabe/src/util/process_executor.rs b/crates/shirabe/src/util/process_executor.rs index 837fb32..40a212b 100644 --- a/crates/shirabe/src/util/process_executor.rs +++ b/crates/shirabe/src/util/process_executor.rs @@ -14,8 +14,8 @@ use shirabe_external_packages::symfony::process::exception::RuntimeException as use shirabe_php_shim::{ LogicException, PhpMixed, RuntimeException, array_intersect, array_map, call_user_func, defined, escapeshellarg, explode, implode, in_array, is_array, is_callable, is_dir, is_numeric, - is_string, max, min, rtrim, sprintf, str_replace, strcspn, strlen, strpbrk, strtolower, strtr, - substr_replace, trim, usleep, + is_string, max, min, rtrim, sprintf, str_replace, strcspn, strlen, strpbrk, strtolower, + strtr_array, substr_replace, trim, usleep, }; use crate::io::IOInterface; @@ -754,12 +754,7 @@ impl ProcessExecutor { translation.insert("\u{2044}".to_string(), "/".to_string()); translation.insert("\u{2215}".to_string(), "/".to_string()); translation.insert("\u{00b4}".to_string(), "/".to_string()); - // PHP: strtr($argument, $translation) — variadic translation map - // TODO(phase-b): implement multi-target strtr; for now we apply replacements iteratively - for (from, to) in &translation { - argument = str_replace(from, to, &argument); - } - let _ = strtr; + argument = strtr_array(&argument, &translation); // In addition to whitespace, commas need quoting to preserve paths let mut quote = strpbrk(&argument, " \t,").is_some(); |
