aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-06-08 11:26:03 +0900
committernsfisis <nsfisis@gmail.com>2026-06-08 11:26:03 +0900
commite5b789616ec4c1cbd152c5ccbefe2d27ced4a18f (patch)
tree7a713cf1bcec30a1c69d5c434d8df6a7525dcbc2 /crates
parent7439cdb08afe0882186a34f70c1e8878fcb7dca5 (diff)
downloadphp-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.rs11
-rw-r--r--crates/shirabe/src/autoload/autoload_generator.rs28
-rw-r--r--crates/shirabe/src/command/require_command.rs7
-rw-r--r--crates/shirabe/src/command/update_command.rs89
-rw-r--r--crates/shirabe/src/config.rs22
-rw-r--r--crates/shirabe/src/dependency_resolver/pool_builder.rs6
-rw-r--r--crates/shirabe/src/dependency_resolver/transaction.rs22
-rw-r--r--crates/shirabe/src/installer/installation_manager.rs8
-rw-r--r--crates/shirabe/src/io/console_io.rs2
-rw-r--r--crates/shirabe/src/repository/filesystem_repository.rs125
-rw-r--r--crates/shirabe/src/repository/repository_set.rs16
-rw-r--r--crates/shirabe/src/util/auth_helper.rs1
-rw-r--r--crates/shirabe/src/util/process_executor.rs11
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();