diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-06-07 18:47:59 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-06-07 19:29:26 +0900 |
| commit | e8d6a57d157ab778108bcb044a3dfa5952d4a5ae (patch) | |
| tree | eaa30eeee79b2c04ca95be843ae6a9dda69af246 /crates | |
| parent | b8a50e25aafb51a6d73f9695362854cc5ed58117 (diff) | |
| download | php-shirabe-e8d6a57d157ab778108bcb044a3dfa5952d4a5ae.tar.gz php-shirabe-e8d6a57d157ab778108bcb044a3dfa5952d4a5ae.tar.zst php-shirabe-e8d6a57d157ab778108bcb044a3dfa5952d4a5ae.zip | |
feat(phase-c): resolve dynamic-dispatch phase-b TODOs
Replace the PHP `call_user_func([$obj, $method], ...)` / `$obj->{'get'.$x}()`
dynamic dispatches (category E) with static Rust dispatch.
- json_config_source: inject the clean-update as a typed
`FnOnce(&mut JsonManipulator) -> Result<bool>` closure instead of a
method-name string, dropping the call_user_func_array round-trip through
PhpMixed args. The auth-config method override moves into the
add/remove_config_setting closures.
- config_command: dispatch addConfigSetting/addProperty on the concrete
JsonConfigSource via match.
- locker: select getRequires/getDevRequires and getReplaces/getProvides via
match on the existing handle getters (previously stubbed to empty Vec).
- create_project_command: reuse the existing get_links_for_type helper.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates')
| -rw-r--r-- | crates/shirabe/src/command/config_command.rs | 32 | ||||
| -rw-r--r-- | crates/shirabe/src/command/create_project_command.rs | 10 | ||||
| -rw-r--r-- | crates/shirabe/src/config/json_config_source.rs | 149 | ||||
| -rw-r--r-- | crates/shirabe/src/package/locker.rs | 19 |
4 files changed, 114 insertions, 96 deletions
diff --git a/crates/shirabe/src/command/config_command.rs b/crates/shirabe/src/command/config_command.rs index 945dfcb..2099bcc 100644 --- a/crates/shirabe/src/command/config_command.rs +++ b/crates/shirabe/src/command/config_command.rs @@ -9,9 +9,9 @@ use shirabe_external_packages::symfony::component::console::input::InputInterfac use shirabe_external_packages::symfony::component::console::output::OutputInterface; use shirabe_php_shim::{ ArrayObject, InvalidArgumentException, JsonObject, PhpMixed, RuntimeException, array_filter, - array_filter_use_key, array_is_list, array_map, array_merge, array_unique, call_user_func, - count, escapeshellcmd, exec, explode, file_exists, file_get_contents, implode, in_array, - is_array, is_bool, is_dir, is_numeric, is_object, is_string, json_encode, key, sort, sprintf, + array_filter_use_key, array_is_list, array_map, array_merge, array_unique, count, + escapeshellcmd, exec, explode, file_exists, file_get_contents, implode, in_array, is_array, + is_bool, is_dir, is_numeric, is_object, is_string, json_encode, key, sort, sprintf, str_replace, str_starts_with, strpos, strtolower, system, touch, var_export, }; @@ -1206,12 +1206,12 @@ impl ConfigCommand { } } - // TODO(phase-b): port PHP `call_user_func([$this->configSource, $method], $key, $normalizedValue)` - let _ = (method, key, normalized_value); - let _: PhpMixed = call_user_func( - method, - &[/* PhpMixed::String(key.to_string()), normalized_value */], - ); + let config_source = self.config_source.as_mut().unwrap(); + match method { + "addConfigSetting" => config_source.add_config_setting(key, normalized_value)?, + "addProperty" => config_source.add_property(key, normalized_value)?, + _ => unreachable!(), + } Ok(()) } @@ -1246,12 +1246,14 @@ impl ConfigCommand { .into()); } - // TODO(phase-b): port PHP `call_user_func([$this->configSource, $method], $key, $normalizer($valuesMixed))` - let _ = (method, key, normalizer(&values_mixed)); - let _: PhpMixed = call_user_func( - method, - &[/* PhpMixed::String(key.to_string()), normalizer(&values_mixed) */], - ); + let config_source = self.config_source.as_mut().unwrap(); + match method { + "addConfigSetting" => { + config_source.add_config_setting(key, normalizer(&values_mixed))? + } + "addProperty" => config_source.add_property(key, normalizer(&values_mixed))?, + _ => unreachable!(), + } Ok(()) } diff --git a/crates/shirabe/src/command/create_project_command.rs b/crates/shirabe/src/command/create_project_command.rs index bbde603..c2e9b99 100644 --- a/crates/shirabe/src/command/create_project_command.rs +++ b/crates/shirabe/src/command/create_project_command.rs @@ -557,18 +557,14 @@ impl CreateProjectCommand { )?)), false, ); - for (r#type, meta) in SUPPORTED_LINK_TYPES.iter() { - // PHP: $package->{'get'.$meta['method']}() — dynamic getter dispatch - // TODO(phase-b): dynamic getter dispatch by name - let _method = format!("get{}", meta.method); - let links: Vec<crate::package::Link> = vec![]; - for link in links { + for (r#type, _meta) in SUPPORTED_LINK_TYPES.iter() { + for link in package.get_links_for_type(r#type).values() { if link.get_pretty_constraint() == "self.version" { config_source.add_link( r#type, link.get_target(), &package.get_pretty_version(), - ); + )?; } } } diff --git a/crates/shirabe/src/config/json_config_source.rs b/crates/shirabe/src/config/json_config_source.rs index 532c1d8..8a8d40a 100644 --- a/crates/shirabe/src/config/json_config_source.rs +++ b/crates/shirabe/src/config/json_config_source.rs @@ -4,8 +4,8 @@ use crate::util::Silencer; use anyhow::Result; use indexmap::IndexMap; use shirabe_php_shim::{ - PHP_EOL, PhpMixed, RuntimeException, array_unshift, call_user_func_array, chmod, explode, - file_get_contents, file_put_contents, implode, is_writable, sprintf, + PHP_EOL, PhpMixed, RuntimeException, array_unshift, chmod, explode, file_get_contents, + file_put_contents, implode, is_writable, sprintf, }; use crate::config::ConfigSourceInterface; @@ -33,9 +33,9 @@ impl JsonConfigSource { /// @param mixed ...$args fn manipulate_json( &mut self, - method: &str, + clean: impl FnOnce(&mut JsonManipulator) -> Result<bool>, // TODO(phase-b): callback signature uses &mut $config (PHP reference) and variadic args - fallback: Box<dyn Fn(&mut PhpMixed, &mut Vec<PhpMixed>)>, + fallback: impl FnOnce(&mut PhpMixed, &mut Vec<PhpMixed>), mut args: Vec<PhpMixed>, ) -> Result<()> { let contents; @@ -73,35 +73,8 @@ impl JsonConfigSource { let new_file = !self.file.borrow().exists(); - // override manipulator method for auth config files - let mut method = method.to_string(); - if self.auth_config && method == "addConfigSetting" { - method = "addSubNode".to_string(); - let parts = explode(".", args[0].as_string().unwrap_or("")); - let main_node = parts.get(0).cloned().unwrap_or_default(); - let name = parts.get(1).cloned().unwrap_or_default(); - args = vec![ - PhpMixed::String(main_node), - PhpMixed::String(name), - args[1].clone(), - ]; - } else if self.auth_config && method == "removeConfigSetting" { - method = "removeSubNode".to_string(); - let parts = explode(".", args[0].as_string().unwrap_or("")); - let main_node = parts.get(0).cloned().unwrap_or_default(); - let name = parts.get(1).cloned().unwrap_or_default(); - args = vec![PhpMixed::String(main_node), PhpMixed::String(name)]; - } - // try to update cleanly - // PHP: call_user_func_array([$manipulator, $method], $args) - let manipulator_result: bool = call_user_func_array( - // TODO(phase-b): callable [manipulator, method] requires bound-method dispatch - todo!("[manipulator, method] callable"), - &PhpMixed::List(args.iter().map(|a| Box::new(a.clone())).collect()), - ) - .as_bool() - .unwrap_or(false); + let manipulator_result: bool = clean(&mut manipulator)?; if manipulator_result { file_put_contents( self.file.borrow().get_path(), @@ -262,13 +235,15 @@ impl ConfigSourceInterface for JsonConfigSource { fn add_repository(&mut self, name: &str, config: PhpMixed, append: bool) -> Result<()> { let name_owned = name.to_string(); + let clean_name = name_owned.clone(); + let clean_config = config.clone(); self.manipulate_json( - "addRepository", - Box::new(move |cfg: &mut PhpMixed, args: &mut Vec<PhpMixed>| { + move |m| m.add_repository(&clean_name, clean_config, append), + move |cfg: &mut PhpMixed, args: &mut Vec<PhpMixed>| { // TODO(phase-b): port the closure body — args are [$cfg, $repo, $repoConfig, $append] let _ = (cfg, args); todo!("addRepository fallback closure body"); - }), + }, vec![PhpMixed::String(name_owned), config, PhpMixed::Bool(append)], ) } @@ -282,13 +257,16 @@ impl ConfigSourceInterface for JsonConfigSource { ) -> Result<()> { let name_owned = name.to_string(); let reference_name_owned = reference_name.to_string(); + let clean_name = name_owned.clone(); + let clean_reference_name = reference_name_owned.clone(); + let clean_config = config.clone(); self.manipulate_json( - "insertRepository", - Box::new(move |cfg: &mut PhpMixed, args: &mut Vec<PhpMixed>| { + move |m| m.insert_repository(&clean_name, clean_config, &clean_reference_name, offset), + move |cfg: &mut PhpMixed, args: &mut Vec<PhpMixed>| { // TODO(phase-b): port the closure body let _ = (cfg, args); todo!("insertRepository fallback closure body"); - }), + }, vec![ PhpMixed::String(name_owned), config, @@ -299,15 +277,15 @@ impl ConfigSourceInterface for JsonConfigSource { } fn set_repository_url(&mut self, name: &str, url: &str) -> Result<()> { - let _name_owned = name.to_string(); - let _url_owned = url.to_string(); + let clean_name = name.to_string(); + let clean_url = url.to_string(); self.manipulate_json( - "setRepositoryUrl", - Box::new(move |cfg: &mut PhpMixed, args: &mut Vec<PhpMixed>| { + move |m| m.set_repository_url(&clean_name, &clean_url), + move |cfg: &mut PhpMixed, args: &mut Vec<PhpMixed>| { // PHP: foreach ($config['repositories'] ?? [] as $index => $repository) { ... } let _ = (cfg, args); todo!("setRepositoryUrl fallback closure body"); - }), + }, vec![ PhpMixed::String(name.to_string()), PhpMixed::String(url.to_string()), @@ -316,71 +294,105 @@ impl ConfigSourceInterface for JsonConfigSource { } fn remove_repository(&mut self, name: &str) -> Result<()> { + let clean_name = name.to_string(); self.manipulate_json( - "removeRepository", - Box::new(move |cfg: &mut PhpMixed, args: &mut Vec<PhpMixed>| { + move |m| m.remove_repository(&clean_name), + move |cfg: &mut PhpMixed, args: &mut Vec<PhpMixed>| { let _ = (cfg, args); todo!("removeRepository fallback closure body"); - }), + }, vec![PhpMixed::String(name.to_string())], ) } fn add_config_setting(&mut self, name: &str, value: PhpMixed) -> Result<()> { let auth_config = self.auth_config; + let clean_name = name.to_string(); + let clean_value = value.clone(); self.manipulate_json( - "addConfigSetting", - Box::new(move |cfg: &mut PhpMixed, args: &mut Vec<PhpMixed>| { + // override manipulator method for auth config files + move |m| { + if auth_config { + let parts = explode(".", &clean_name); + m.add_sub_node( + parts.get(0).map(String::as_str).unwrap_or(""), + parts.get(1).map(String::as_str).unwrap_or(""), + clean_value, + false, + ) + } else { + m.add_config_setting(&clean_name, clean_value) + } + }, + move |cfg: &mut PhpMixed, args: &mut Vec<PhpMixed>| { // PHP: [$key, $host] = explode('.', $key, 2); let _ = (cfg, args, auth_config); todo!("addConfigSetting fallback closure body"); - }), + }, vec![PhpMixed::String(name.to_string()), value], ) } fn remove_config_setting(&mut self, name: &str) -> Result<()> { let auth_config = self.auth_config; + let clean_name = name.to_string(); self.manipulate_json( - "removeConfigSetting", - Box::new(move |cfg: &mut PhpMixed, args: &mut Vec<PhpMixed>| { + // override manipulator method for auth config files + move |m| { + if auth_config { + let parts = explode(".", &clean_name); + m.remove_sub_node( + parts.get(0).map(String::as_str).unwrap_or(""), + parts.get(1).map(String::as_str).unwrap_or(""), + ) + } else { + m.remove_config_setting(&clean_name) + } + }, + move |cfg: &mut PhpMixed, args: &mut Vec<PhpMixed>| { let _ = (cfg, args, auth_config); todo!("removeConfigSetting fallback closure body"); - }), + }, vec![PhpMixed::String(name.to_string())], ) } fn add_property(&mut self, name: &str, value: PhpMixed) -> Result<()> { + let clean_name = name.to_string(); + let clean_value = value.clone(); self.manipulate_json( - "addProperty", - Box::new(move |cfg: &mut PhpMixed, args: &mut Vec<PhpMixed>| { + move |m| m.add_property(&clean_name, clean_value), + move |cfg: &mut PhpMixed, args: &mut Vec<PhpMixed>| { let _ = (cfg, args); todo!("addProperty fallback closure body"); - }), + }, vec![PhpMixed::String(name.to_string()), value], ) } fn remove_property(&mut self, name: &str) -> Result<()> { + let clean_name = name.to_string(); self.manipulate_json( - "removeProperty", - Box::new(move |cfg: &mut PhpMixed, args: &mut Vec<PhpMixed>| { + move |m| m.remove_property(&clean_name), + move |cfg: &mut PhpMixed, args: &mut Vec<PhpMixed>| { let _ = (cfg, args); todo!("removeProperty fallback closure body"); - }), + }, vec![PhpMixed::String(name.to_string())], ) } fn add_link(&mut self, r#type: &str, name: &str, value: &str) -> Result<()> { + let clean_type = r#type.to_string(); + let clean_name = name.to_string(); + let clean_value = value.to_string(); self.manipulate_json( - "addLink", - Box::new(move |cfg: &mut PhpMixed, args: &mut Vec<PhpMixed>| { + move |m| m.add_link(&clean_type, &clean_name, &clean_value, false), + move |cfg: &mut PhpMixed, args: &mut Vec<PhpMixed>| { // PHP: $config[$type][$name] = $value; let _ = (cfg, args); todo!("addLink fallback closure body"); - }), + }, vec![ PhpMixed::String(r#type.to_string()), PhpMixed::String(name.to_string()), @@ -390,25 +402,28 @@ impl ConfigSourceInterface for JsonConfigSource { } fn remove_link(&mut self, r#type: &str, name: &str) -> Result<()> { + let clean_type = r#type.to_string(); + let clean_name = name.to_string(); self.manipulate_json( - "removeSubNode", - Box::new(move |cfg: &mut PhpMixed, args: &mut Vec<PhpMixed>| { + move |m| m.remove_sub_node(&clean_type, &clean_name), + move |cfg: &mut PhpMixed, args: &mut Vec<PhpMixed>| { // PHP: unset($config[$type][$name]); let _ = (cfg, args); todo!("removeLink fallback (unset subnode) closure body"); - }), + }, vec![ PhpMixed::String(r#type.to_string()), PhpMixed::String(name.to_string()), ], )?; + let clean_type = r#type.to_string(); self.manipulate_json( - "removeMainKeyIfEmpty", - Box::new(move |cfg: &mut PhpMixed, args: &mut Vec<PhpMixed>| { + move |m| m.remove_main_key_if_empty(&clean_type), + move |cfg: &mut PhpMixed, args: &mut Vec<PhpMixed>| { // PHP: if (0 === count($config[$type])) { unset($config[$type]); } let _ = (cfg, args); todo!("removeLink fallback (unset main key if empty) closure body"); - }), + }, vec![PhpMixed::String(r#type.to_string())], ) } diff --git a/crates/shirabe/src/package/locker.rs b/crates/shirabe/src/package/locker.rs index 51177db..91affe2 100644 --- a/crates/shirabe/src/package/locker.rs +++ b/crates/shirabe/src/package/locker.rs @@ -951,9 +951,12 @@ impl Locker { let installed_repo = InstalledRepository::new(vec![/* set.repo, root_repo */]); // PHP: call_user_func([$package, $set['method']]) - // TODO(phase-b): dynamic method dispatch by name - let links: Vec<Link> = vec![]; - for link in links { + let links = match set.method.as_str() { + "getRequires" => package.get_requires(), + "getDevRequires" => package.get_dev_requires(), + _ => unreachable!(), + }; + for link in links.values() { if PlatformRepository::is_platform_package(&link.get_target()) { continue; } @@ -984,10 +987,12 @@ impl Locker { ] .iter() { - // TODO(phase-b): dynamic method dispatch - let provider_links: Vec<Link> = vec![]; - let _ = method; - for provider_link in provider_links { + let provider_links = match *method { + "getReplaces" => provider.get_replaces(), + "getProvides" => provider.get_provides(), + _ => unreachable!(), + }; + for provider_link in provider_links.values() { if provider_link.get_target() == link.get_target() { description = sprintf( text, |
