From e5b789616ec4c1cbd152c5ccbefe2d27ced4a18f Mon Sep 17 00:00:00 2001 From: nsfisis Date: Mon, 8 Jun 2026 11:26:03 +0900 Subject: 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, 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 --- crates/shirabe/src/command/update_command.rs | 89 +++++++++++----------------- 1 file changed, 34 insertions(+), 55 deletions(-) (limited to 'crates/shirabe/src/command/update_command.rs') 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; supply a wrapper - todo!("packages as IndexMap"), - ); - 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 = IndexMap::new(); - let root_requirements = array_merge( - // TODO(phase-b): array_merge for IndexMap - 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 { - 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!( "The temporary constraint \"{}\" for \"{}\" must be a subset of the constraint in your composer.json ({})", 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 - 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 - 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 = 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 = IndexMap::new(); - for (req, _constraint) in &_empty { + for (req, _constraint) in &requires { if PlatformRepository::is_platform_package(req) { continue; } -- cgit v1.3.1