diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-06-11 02:39:35 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-06-11 02:39:35 +0900 |
| commit | 6ae7e0d4b3eaf20e2d2cd3d000cf61ab0c9b6e83 (patch) | |
| tree | e39261f4aa7314fe8c75142670c76591cdaccb92 /crates | |
| parent | 5d3232a80be4b989e89cc7ae4e3642cc5acae030 (diff) | |
| download | php-shirabe-6ae7e0d4b3eaf20e2d2cd3d000cf61ab0c9b6e83.tar.gz php-shirabe-6ae7e0d4b3eaf20e2d2cd3d000cf61ab0c9b6e83.tar.zst php-shirabe-6ae7e0d4b3eaf20e2d2cd3d000cf61ab0c9b6e83.zip | |
feat(console): resolve phase-b TODOs in doRun and IO wiring
Wire up ConsoleIO with HelperSet/QuestionHelper, register the
ErrorHandler with the IO instance, and fall back to a default output
in run(). Replace resolved phase-b TODOs across the console, command,
io, factory, installer, dependency_resolver, and util modules; reclassify
the remaining blockers (typed Symfony command registry, stdin resource
caching) as phase-c.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates')
52 files changed, 679 insertions, 339 deletions
diff --git a/crates/shirabe-external-packages/src/symfony/console/application.rs b/crates/shirabe-external-packages/src/symfony/console/application.rs index f937473..a939bee 100644 --- a/crates/shirabe-external-packages/src/symfony/console/application.rs +++ b/crates/shirabe-external-packages/src/symfony/console/application.rs @@ -1,3 +1,4 @@ +use crate::symfony::console::input::InputDefinition; use crate::symfony::console::input::InputInterface; use crate::symfony::console::output::OutputInterface; use shirabe_php_shim::PhpMixed; @@ -105,4 +106,28 @@ impl Application { pub fn has(&self, _name: &str) -> bool { todo!() } + + pub fn do_run( + &mut self, + _input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, + _output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<i64> { + todo!() + } + + pub fn get_help(&self) -> String { + todo!() + } + + pub fn are_exceptions_caught(&self) -> bool { + todo!() + } + + pub fn get_default_input_definition(&self) -> InputDefinition { + todo!() + } + + pub fn get_default_commands(&self) -> Vec<PhpMixed> { + todo!() + } } diff --git a/crates/shirabe-external-packages/src/symfony/console/formatter/output_formatter.rs b/crates/shirabe-external-packages/src/symfony/console/formatter/output_formatter.rs index 4b499b8..f64361e 100644 --- a/crates/shirabe-external-packages/src/symfony/console/formatter/output_formatter.rs +++ b/crates/shirabe-external-packages/src/symfony/console/formatter/output_formatter.rs @@ -39,4 +39,45 @@ impl OutputFormatter { ) { todo!() } + + pub fn has_style(&self, _name: &str) -> bool { + todo!() + } + + pub fn get_style( + &self, + _name: &str, + ) -> crate::symfony::console::formatter::OutputFormatterStyle { + todo!() + } +} + +impl crate::symfony::console::formatter::OutputFormatterInterface for OutputFormatter { + fn is_decorated(&self) -> bool { + self.is_decorated() + } + + fn set_decorated(&mut self, decorated: bool) { + self.set_decorated(decorated) + } + + fn set_style( + &mut self, + name: &str, + style: crate::symfony::console::formatter::OutputFormatterStyle, + ) { + self.set_style(name, style) + } + + fn has_style(&self, name: &str) -> bool { + self.has_style(name) + } + + fn get_style(&self, name: &str) -> crate::symfony::console::formatter::OutputFormatterStyle { + self.get_style(name) + } + + fn format(&self, message: &str) -> String { + self.format(message) + } } diff --git a/crates/shirabe-external-packages/src/symfony/console/formatter/output_formatter_interface.rs b/crates/shirabe-external-packages/src/symfony/console/formatter/output_formatter_interface.rs index dc93a3b..7f6e9b0 100644 --- a/crates/shirabe-external-packages/src/symfony/console/formatter/output_formatter_interface.rs +++ b/crates/shirabe-external-packages/src/symfony/console/formatter/output_formatter_interface.rs @@ -1,5 +1,10 @@ +use crate::symfony::console::formatter::OutputFormatterStyle; + pub trait OutputFormatterInterface { fn is_decorated(&self) -> bool; fn set_decorated(&mut self, decorated: bool); + fn set_style(&mut self, name: &str, style: OutputFormatterStyle); + fn has_style(&self, name: &str) -> bool; + fn get_style(&self, name: &str) -> OutputFormatterStyle; fn format(&self, message: &str) -> String; } diff --git a/crates/shirabe-external-packages/src/symfony/console/helper/formatter_helper.rs b/crates/shirabe-external-packages/src/symfony/console/helper/formatter_helper.rs index 656828e..e55dbce 100644 --- a/crates/shirabe-external-packages/src/symfony/console/helper/formatter_helper.rs +++ b/crates/shirabe-external-packages/src/symfony/console/helper/formatter_helper.rs @@ -1,3 +1,5 @@ +use crate::symfony::console::helper::HelperInterface; + #[derive(Debug)] pub struct FormatterHelper; @@ -10,3 +12,9 @@ impl FormatterHelper { todo!() } } + +impl HelperInterface for FormatterHelper { + fn as_any(&self) -> &dyn std::any::Any { + self + } +} diff --git a/crates/shirabe-external-packages/src/symfony/console/helper/helper_interface.rs b/crates/shirabe-external-packages/src/symfony/console/helper/helper_interface.rs new file mode 100644 index 0000000..cf60bf0 --- /dev/null +++ b/crates/shirabe-external-packages/src/symfony/console/helper/helper_interface.rs @@ -0,0 +1,17 @@ +use crate::symfony::console::helper::HelperSet; + +pub trait HelperInterface: std::fmt::Debug { + fn set_helper_set(&mut self, _helper_set: Option<std::rc::Rc<std::cell::RefCell<HelperSet>>>) { + todo!() + } + + fn get_helper_set(&self) -> Option<std::rc::Rc<std::cell::RefCell<HelperSet>>> { + todo!() + } + + fn get_name(&self) -> String { + todo!() + } + + fn as_any(&self) -> &dyn std::any::Any; +} diff --git a/crates/shirabe-external-packages/src/symfony/console/helper/helper_set.rs b/crates/shirabe-external-packages/src/symfony/console/helper/helper_set.rs index 26af492..e38dbf3 100644 --- a/crates/shirabe-external-packages/src/symfony/console/helper/helper_set.rs +++ b/crates/shirabe-external-packages/src/symfony/console/helper/helper_set.rs @@ -1,18 +1,23 @@ -use shirabe_php_shim::PhpMixed; +use crate::symfony::console::helper::HelperInterface; +use indexmap::IndexMap; +use std::cell::RefCell; +use std::rc::Rc; #[derive(Debug)] -pub struct HelperSet; +pub struct HelperSet { + helpers: IndexMap<String, Rc<RefCell<dyn HelperInterface>>>, +} impl HelperSet { - pub fn new(_helpers: Vec<PhpMixed>) -> Self { + pub fn new(_helpers: Vec<Rc<RefCell<dyn HelperInterface>>>) -> Self { todo!() } - pub fn get(&self, _name: &str) -> PhpMixed { + pub fn get<T: HelperInterface + 'static>(&self, _name: &str) -> Rc<RefCell<T>> { todo!() } - pub fn set(&mut self, _helper: PhpMixed, _alias: Option<&str>) { + pub fn set(&mut self, _helper: Rc<RefCell<dyn HelperInterface>>, _alias: Option<&str>) { todo!() } diff --git a/crates/shirabe-external-packages/src/symfony/console/helper/mod.rs b/crates/shirabe-external-packages/src/symfony/console/helper/mod.rs index b31517a..191fb6e 100644 --- a/crates/shirabe-external-packages/src/symfony/console/helper/mod.rs +++ b/crates/shirabe-external-packages/src/symfony/console/helper/mod.rs @@ -1,5 +1,6 @@ pub mod formatter_helper; pub mod helper; +pub mod helper_interface; pub mod helper_set; pub mod progress_bar; pub mod question_helper; @@ -8,6 +9,7 @@ pub mod table_separator; pub use formatter_helper::*; pub use helper::*; +pub use helper_interface::*; pub use helper_set::*; pub use progress_bar::*; pub use question_helper::*; diff --git a/crates/shirabe-external-packages/src/symfony/console/helper/question_helper.rs b/crates/shirabe-external-packages/src/symfony/console/helper/question_helper.rs index 70a9572..c17cb6b 100644 --- a/crates/shirabe-external-packages/src/symfony/console/helper/question_helper.rs +++ b/crates/shirabe-external-packages/src/symfony/console/helper/question_helper.rs @@ -1,3 +1,4 @@ +use crate::symfony::console::helper::HelperInterface; use crate::symfony::console::input::InputInterface; use crate::symfony::console::output::OutputInterface; use crate::symfony::console::question::Question; @@ -12,7 +13,13 @@ impl QuestionHelper { _input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, _output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, _question: &Question, - ) -> Option<PhpMixed> { + ) -> PhpMixed { todo!() } } + +impl HelperInterface for QuestionHelper { + fn as_any(&self) -> &dyn std::any::Any { + self + } +} diff --git a/crates/shirabe-external-packages/src/symfony/console/input/input_definition.rs b/crates/shirabe-external-packages/src/symfony/console/input/input_definition.rs index 74839fc..f2d0318 100644 --- a/crates/shirabe-external-packages/src/symfony/console/input/input_definition.rs +++ b/crates/shirabe-external-packages/src/symfony/console/input/input_definition.rs @@ -1,3 +1,5 @@ +use crate::symfony::console::input::InputArgument; +use crate::symfony::console::input::InputOption; use shirabe_php_shim::PhpMixed; #[derive(Debug)] @@ -8,11 +10,11 @@ impl InputDefinition { todo!() } - pub fn add_argument(&mut self, _argument: PhpMixed) { + pub fn add_argument(&mut self, _argument: InputArgument) { todo!() } - pub fn add_option(&mut self, _option: PhpMixed) { + pub fn add_option(&mut self, _option: InputOption) { todo!() } diff --git a/crates/shirabe-external-packages/src/symfony/console/output/console_output.rs b/crates/shirabe-external-packages/src/symfony/console/output/console_output.rs index 5aa6ff0..b3010b9 100644 --- a/crates/shirabe-external-packages/src/symfony/console/output/console_output.rs +++ b/crates/shirabe-external-packages/src/symfony/console/output/console_output.rs @@ -1,4 +1,4 @@ -use crate::symfony::console::formatter::OutputFormatter; +use crate::symfony::console::formatter::OutputFormatterInterface; use crate::symfony::console::output::ConsoleOutputInterface; use crate::symfony::console::output::OutputInterface; @@ -9,7 +9,7 @@ impl ConsoleOutput { pub fn new( _verbosity: i64, _decorated: Option<bool>, - _formatter: Option<OutputFormatter>, + _formatter: Option<std::rc::Rc<std::cell::RefCell<dyn OutputFormatterInterface>>>, ) -> Self { todo!() } @@ -64,10 +64,13 @@ impl OutputInterface for ConsoleOutput { fn is_decorated(&self) -> bool { todo!() } - fn set_formatter(&self, _formatter: OutputFormatter) { + fn set_formatter( + &self, + _formatter: std::rc::Rc<std::cell::RefCell<dyn OutputFormatterInterface>>, + ) { todo!() } - fn get_formatter(&self) -> &OutputFormatter { + fn get_formatter(&self) -> std::rc::Rc<std::cell::RefCell<dyn OutputFormatterInterface>> { todo!() } } diff --git a/crates/shirabe-external-packages/src/symfony/console/output/output_interface.rs b/crates/shirabe-external-packages/src/symfony/console/output/output_interface.rs index 18eadfb..661cd7f 100644 --- a/crates/shirabe-external-packages/src/symfony/console/output/output_interface.rs +++ b/crates/shirabe-external-packages/src/symfony/console/output/output_interface.rs @@ -1,4 +1,4 @@ -use crate::symfony::console::formatter::OutputFormatter; +use crate::symfony::console::formatter::OutputFormatterInterface; use crate::symfony::console::output::ConsoleOutputInterface; pub trait OutputInterface: std::fmt::Debug { @@ -14,8 +14,11 @@ pub trait OutputInterface: std::fmt::Debug { fn is_debug(&self) -> bool; fn set_decorated(&self, decorated: bool); fn is_decorated(&self) -> bool; - fn set_formatter(&self, formatter: OutputFormatter); - fn get_formatter(&self) -> &OutputFormatter; + fn set_formatter( + &self, + formatter: std::rc::Rc<std::cell::RefCell<dyn OutputFormatterInterface>>, + ); + fn get_formatter(&self) -> std::rc::Rc<std::cell::RefCell<dyn OutputFormatterInterface>>; /// PHP: `$output instanceof ConsoleOutputInterface`. Default false; ConsoleOutput overrides. fn is_console_output_interface(&self) -> bool { diff --git a/crates/shirabe-external-packages/src/symfony/console/output/stream_output.rs b/crates/shirabe-external-packages/src/symfony/console/output/stream_output.rs index 6aa79b8..da7dbf4 100644 --- a/crates/shirabe-external-packages/src/symfony/console/output/stream_output.rs +++ b/crates/shirabe-external-packages/src/symfony/console/output/stream_output.rs @@ -1,4 +1,4 @@ -use crate::symfony::console::formatter::OutputFormatter; +use crate::symfony::console::formatter::OutputFormatterInterface; use crate::symfony::console::output::OutputInterface; use shirabe_php_shim::PhpMixed; @@ -42,10 +42,13 @@ impl OutputInterface for StreamOutput { fn is_decorated(&self) -> bool { todo!() } - fn set_formatter(&self, _formatter: OutputFormatter) { + fn set_formatter( + &self, + _formatter: std::rc::Rc<std::cell::RefCell<dyn OutputFormatterInterface>>, + ) { todo!() } - fn get_formatter(&self) -> &OutputFormatter { + fn get_formatter(&self) -> std::rc::Rc<std::cell::RefCell<dyn OutputFormatterInterface>> { todo!() } fn get_stream(&self) -> shirabe_php_shim::PhpResource { diff --git a/crates/shirabe/src/command/base_command.rs b/crates/shirabe/src/command/base_command.rs index b1a1837..9218497 100644 --- a/crates/shirabe/src/command/base_command.rs +++ b/crates/shirabe/src/command/base_command.rs @@ -287,7 +287,14 @@ pub trait HasBaseCommandData { impl<C: HasBaseCommandData> BaseCommand for C { fn get_application(&self) -> Result<Application> { - // TODO(phase-b): requires inner Symfony Command access + // PHP: parent::getApplication() returns Symfony Command::$application, the back-reference + // set by Application::add() -> $command->setApplication($this) when the command is + // registered. + // TODO(phase-c): the command holds no Application back-reference. Establishing it requires + // (1) modelling the Symfony command registry (add/find/run), which is an intentional + // todo!() stub, and (2) making Application shared (Rc<RefCell<Application>>) with a Weak + // back-edge to break the Application<->command cycle. Until command registration runs, + // there is no application to return. todo!() } @@ -296,12 +303,15 @@ impl<C: HasBaseCommandData> BaseCommand for C { _disable_plugins: Option<bool>, _disable_scripts: Option<bool>, ) -> Result<PartialComposerHandle> { - // TODO(phase-b): depends on Application::get_composer, which is still stubbed. + // PHP: $this->composer ??= $this->getApplication()->getComposer(true, ...). + // TODO(phase-c): blocked on get_application() (see above) — it currently returns an owned + // `Application` by value, which a command cannot hold; a shared Application handle is the + // prerequisite. Application::getComposer itself is already implemented. let _ = RuntimeException { message: String::new(), code: 0, }; - todo!("require_composer pending Application::get_composer") + todo!("require_composer pending get_application() shared-handle support") } fn try_composer( @@ -309,8 +319,10 @@ impl<C: HasBaseCommandData> BaseCommand for C { _disable_plugins: Option<bool>, _disable_scripts: Option<bool>, ) -> Option<PartialComposerHandle> { - // TODO(phase-b): depends on Application::get_composer, which is still stubbed. - todo!("try_composer pending Application::get_composer") + // PHP: try { return $this->requireComposer(...); } catch (\RuntimeException $e) { return null; } + // TODO(phase-c): blocked on get_application() / require_composer (see above) — needs a + // shared Application handle the command can reach. + todo!("try_composer pending get_application() shared-handle support") } fn set_composer(&mut self, composer: PartialComposerHandle) { @@ -329,7 +341,10 @@ impl<C: HasBaseCommandData> BaseCommand for C { fn get_io(&mut self) -> Rc<RefCell<dyn IOInterface>> { if self.io().is_none() { - // TODO(phase-b): requires inner Symfony Application access + // PHP: $this->io = $this->getApplication() instanceof Application + // ? $this->getApplication()->getIO() : new NullIO(); + // TODO(phase-c): the application-IO branch needs get_application() (see above); until + // a shared Application handle exists we take the NullIO fallback (PHP's else branch). *self.io_mut() = Some(Rc::new(RefCell::new(NullIO::new()))); } @@ -348,6 +363,10 @@ impl<C: HasBaseCommandData> BaseCommand for C { output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, ) -> Result<()> { // initialize a plugin-enabled Composer instance, either local or global + // PHP also ORs in $this->getApplication()->getDisablePluginsByDefault() / + // getDisableScriptsByDefault(). + // TODO(phase-c): the application-default OR-terms need get_application() (see above); a + // shared Application handle is the prerequisite, so only the input flags are honoured here. let mut disable_plugins = input .borrow() .has_parameter_option(&["--no-plugins"], false); @@ -355,8 +374,6 @@ impl<C: HasBaseCommandData> BaseCommand for C { .borrow() .has_parameter_option(&["--no-scripts"], false); - // TODO(phase-b): requires inner Symfony Application access for disable_plugins_by_default / disable_scripts_by_default - if self.is_self_update_command() { disable_plugins = true; disable_scripts = true; @@ -376,7 +393,10 @@ impl<C: HasBaseCommandData> BaseCommand for C { composer }; if let Some(composer) = composer.as_ref() { - // TODO(phase-b): requires inner Symfony Command get_name access + // PHP: $this->getName() — the Symfony Command's configured name. + // TODO(phase-c): the command name lives in Symfony Command state (set via configure() + // -> setName()), which this port does not yet carry on BaseCommandData; it requires + // the Symfony Command base-class model (intentional todo!() stub). let command_name: String = todo!(); let mut pre_command_run_event = PreCommandRunEvent::new( PluginEvents::PRE_COMMAND_RUN.to_string(), @@ -505,7 +525,10 @@ impl<C: HasBaseCommandData> BaseCommand for C { .borrow() .has_parameter_option(&["--no-scripts"], false); - // TODO(phase-b): requires inner Symfony Application access for disable_plugins_by_default / disable_scripts_by_default + // PHP: if ($app instanceof Application && $app->getDisablePluginsByDefault()) $disablePlugins = true; + // (same for getDisableScriptsByDefault()). + // TODO(phase-c): these application-default overrides need get_application() (see above) — + // a shared Application handle is the prerequisite, so only the passed/flag values apply. let disable_plugins_kind = if disable_plugins { crate::factory::DisablePlugins::All } else { @@ -826,6 +849,8 @@ impl<C: HasBaseCommandData> BaseCommand for C { } } -// TODO(phase-b): bridge BaseCommand to Symfony Command for trait-object container usage. +// TODO(phase-c): bridge BaseCommand to Symfony Command for trait-object container usage. // Cannot blanket-impl a foreign trait for a local generic (orphan rule); each concrete -// command must impl symfony Command itself, or a wrapper type must be introduced. +// command must impl symfony Command itself, or a wrapper type must be introduced. This is the +// same orphan-rule blocker as Application::get_default_commands and is gated on the Symfony +// command-registry model, which is an intentional external-package todo!() stub. diff --git a/crates/shirabe/src/command/base_dependency_command.rs b/crates/shirabe/src/command/base_dependency_command.rs index decfd5b..27f3544 100644 --- a/crates/shirabe/src/command/base_dependency_command.rs +++ b/crates/shirabe/src/command/base_dependency_command.rs @@ -40,9 +40,6 @@ pub trait BaseDependencyCommand: BaseCommand { fn colors(&self) -> &[String]; fn colors_mut(&mut self) -> &mut Vec<String>; - // TODO(phase-b): these wrappers existed to forward BaseCommand setters, but they - // shadowed the BaseCommand methods and caused ambiguity. Use BaseCommand directly. - fn do_execute( &mut self, input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, @@ -153,12 +150,12 @@ pub trait BaseDependencyCommand: BaseCommand { FindPackageConstraint::String(text_constraint.clone()), )?; if matched_package.is_none() { + let rm = composer.get_repository_manager(); let mut default_repos = CompositeRepository::new( RepositoryFactory::default_repos( Some(self.get_io()), Some(composer.get_config()), - // TODO(phase-b): get_repository_manager returns &; default_repos needs &mut - Some(todo!("share repository_manager as &mut")), + Some(&mut rm.borrow_mut()), )? .into_values() .collect(), @@ -402,10 +399,12 @@ pub trait BaseDependencyCommand: BaseCommand { "blue".to_string(), ]; for color in self.colors() { - // TODO(phase-b): output.get_formatter() returns &OutputFormatter; set_style needs - // &mut. Need interior mutability or `get_formatter_mut`. - let _ = OutputFormatterStyle::new(Some(color), None, None); - let _ = output.borrow().get_formatter(); + let style = OutputFormatterStyle::new(Some(color), None, None); + output + .borrow() + .get_formatter() + .borrow_mut() + .set_style(color, style); } } diff --git a/crates/shirabe/src/command/clear_cache_command.rs b/crates/shirabe/src/command/clear_cache_command.rs index 28b303e..5dbfaa5 100644 --- a/crates/shirabe/src/command/clear_cache_command.rs +++ b/crates/shirabe/src/command/clear_cache_command.rs @@ -3,6 +3,7 @@ use crate::command::{BaseCommand, BaseCommandData, HasBaseCommandData}; use crate::composer; use crate::composer::ComposerHandle; +use crate::console::input::InputOption; use crate::factory::Factory; use indexmap::IndexMap; use shirabe_external_packages::symfony::console::input::InputInterface; @@ -18,8 +19,15 @@ impl ClearCacheCommand { self.set_name("clear-cache"); self.set_aliases(&["clearcache".to_string(), "cc".to_string()]); self.set_description("Clears composer's internal package cache"); - // TODO(phase-b): set_definition requires Vec<Box<dyn InputDefinitionEntry>> - // self.set_definition(...) — InputOption::new arg shapes do not yet match + self.set_definition(&[InputOption::new( + "gc", + None, + Some(InputOption::VALUE_NONE), + "Only run garbage collection, not a full cache clear", + None, + ) + .unwrap() + .into()]); self.set_help( "The <info>clear-cache</info> deletes all cached packages from composer's\n\ cache directory.\n\n\ @@ -32,11 +40,17 @@ impl ClearCacheCommand { _input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, _output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, ) -> anyhow::Result<i64> { - // TODO(phase-b): port full execute logic once Config sharing model is settled + // PHP: $config = $this->tryComposer()?->getConfig() ?? Factory::createConfig(); then + // iterate the cache-* paths and clear/gc each via Cache. + // TODO(phase-c): two blockers. (1) The first statement calls self.try_composer(), which is + // a deferred todo!() (it needs get_application() -> the Symfony command registry). (2) The + // two config sources differ in ownership — composer.get_config() is Rc<RefCell<Config>> + // while Factory::create_config() returns an owned Config — so a shared Config model is + // needed before the per-path Cache logic can read both uniformly. let _ = composer::VERSION; let _: IndexMap<String, String> = IndexMap::new(); let _ = Factory::create_config(None, None); - todo!("phase-b: ClearCacheCommand::execute requires Config sharing strategy") + todo!("ClearCacheCommand::execute pending try_composer + Config sharing model") } } diff --git a/crates/shirabe/src/command/create_project_command.rs b/crates/shirabe/src/command/create_project_command.rs index bf00371..ba384d5 100644 --- a/crates/shirabe/src/command/create_project_command.rs +++ b/crates/shirabe/src/command/create_project_command.rs @@ -768,14 +768,16 @@ impl CreateProjectCommand { indexmap::IndexMap::new(), ); if repositories.is_none() { - // TODO(phase-b): default_repos needs &mut RepositoryManager but we hold &RepositoryManager. - let _ = rm; repository_set.add_repository(crate::repository::RepositoryInterfaceHandle::new( CompositeRepository::new( - RepositoryFactory::default_repos(Some(io.clone()), Some(config.clone()), None)? - .into_iter() - .map(|(_, v)| v) - .collect(), + RepositoryFactory::default_repos( + Some(io.clone()), + Some(config.clone()), + Some(&mut rm.borrow_mut()), + )? + .into_iter() + .map(|(_, v)| v) + .collect(), ), ))?; } else { @@ -903,6 +905,7 @@ impl CreateProjectCommand { let mut signal_handler: Option<SignalHandler> = None; if let Some(real_dir) = realpath(&directory) { let real_dir_clone = real_dir.clone(); + let io_for_signal = io.clone(); signal_handler = Some(SignalHandler::create( vec![ SignalHandler::SIGINT.to_string(), @@ -910,8 +913,11 @@ impl CreateProjectCommand { SignalHandler::SIGHUP.to_string(), ], Box::new(move |signal: String, handler: &SignalHandler| { - // TODO(phase-b): self.get_io().write_error(...) inside the closure - let _ = &signal; + io_for_signal.write_error3( + &format!("Received {}, aborting", signal), + true, + crate::io::DEBUG, + ); let mut fs = Filesystem::new(None); fs.remove_directory(&real_dir_clone).ok(); handler.exit_with_last_signal(); diff --git a/crates/shirabe/src/command/global_command.rs b/crates/shirabe/src/command/global_command.rs index fa52adc..f066721 100644 --- a/crates/shirabe/src/command/global_command.rs +++ b/crates/shirabe/src/command/global_command.rs @@ -76,13 +76,11 @@ impl GlobalCommand { return self.run(input, output); } - // TODO(phase-b): sub_input/output need to be &mut for Application::run; placeholder marks. - let mut sub_input = self.prepare_subcommand_input(input, false)?; + let sub_input = self.prepare_subcommand_input(input, false)?; let mut app = self.get_application()?; - let _ = output; Ok(app.run( Some(std::rc::Rc::new(std::cell::RefCell::new(sub_input))), - None, + Some(output), )?) } diff --git a/crates/shirabe/src/command/init_command.rs b/crates/shirabe/src/command/init_command.rs index 8e200f0..1bafd74 100644 --- a/crates/shirabe/src/command/init_command.rs +++ b/crates/shirabe/src/command/init_command.rs @@ -448,8 +448,10 @@ impl InitCommand { _output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, ) -> Result<()> { let io = self.get_io(); - // @var FormatterHelper $formatter - // TODO(phase-b): get_helper_set returns PhpMixed; the helper set needs proper typing. + // @var FormatterHelper $formatter — PHP: $this->getHelperSet()->get('formatter') + // TODO(phase-c): get_helper_set returns PhpMixed and HelperSet::get is a todo!() stub (per + // the "Symfony stays todo!()" policy), so the typed FormatterHelper cannot be retrieved + // until the Helper trait + typed HelperSet are modelled. let formatter: FormatterHelper = todo!(); let _ = &formatter; let _ = self.get_helper_set(); @@ -630,7 +632,13 @@ impl InitCommand { String::new() } ), - // TODO(phase-b): closure cannot call &self.parse_author_string; needs &self capture + // PHP: function ($value) use ($self, $author) { ... $author = $self->parseAuthorString($value); ... } + // TODO(phase-c): IOInterface::ask_and_validate takes a `Box<dyn Fn> + 'static` + // validator, so it cannot borrow &self to call self.parse_author_string; and + // ask_and_validate itself is a deferred QuestionHelper todo!() (see console_io). The + // validator body below therefore stays a placeholder. (parse_author_string and + // is_valid_email are stateless, so a future fix can make them associated functions and + // call them from the closure once the helper interaction is modelled.) Box::new(move |value: PhpMixed| -> anyhow::Result<PhpMixed> { let value_str = value.as_string().unwrap_or("").to_string(); if value_str == "n" || value_str == "no" { @@ -641,7 +649,10 @@ impl InitCommand { } else { value_str }; - // TODO(phase-b): would call self.parse_author_string(value_or_default) + // PHP: $author = $self->parseAuthorString($value); return $author['email'] === null + // ? $author['name'] : sprintf('%s <%s>', $author['name'], $author['email']); + // TODO(phase-c): see the closure note above — cannot reach parse_author_string from + // this 'static validator yet. let _ = value_or_default; Ok(PhpMixed::Null) }), @@ -1083,7 +1094,10 @@ impl InitCommand { let result = self.get_application().and_then(|mut app| { let _update_command = app.find("update")?; app.reset_composer(); - // TODO(phase-b): invoke update_command.run; currently update_command is a PhpMixed. + // PHP: $updateCommand->run(new ArrayInput([]), $output); + // TODO(phase-c): Application::find returns PhpMixed (the Symfony command registry is a + // todo!() stub), so the resolved command's run() cannot be invoked until the typed + // command registry is modelled. let _ = ArrayInput::new(IndexMap::new(), None); let _ = output; Ok(()) @@ -1104,7 +1118,9 @@ impl InitCommand { let result = self.get_application().and_then(|mut app| { let _command = app.find("dump-autoload")?; app.reset_composer(); - // TODO(phase-b): invoke command.run; currently command is a PhpMixed. + // PHP: $command->run(new ArrayInput([]), $output); + // TODO(phase-c): same blocker as update_dependencies — Application::find returns + // PhpMixed (Symfony command registry todo!() stub), so run() cannot be invoked. let _ = ArrayInput::new(IndexMap::new(), None); let _ = output; Ok(()) diff --git a/crates/shirabe/src/command/outdated_command.rs b/crates/shirabe/src/command/outdated_command.rs index 6511b2b..8eb67e6 100644 --- a/crates/shirabe/src/command/outdated_command.rs +++ b/crates/shirabe/src/command/outdated_command.rs @@ -180,9 +180,9 @@ impl OutdatedCommand { let input = ArrayInput::new(args, None); - // TODO(phase-b): convert ArrayInput/output references to dyn trait objects expected by Application::run - let _ = input; - self.get_application()?.run(None, None) + let input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>> = + std::rc::Rc::new(std::cell::RefCell::new(input)); + self.get_application()?.run(Some(input), Some(output)) } pub fn is_proxy_command(&self) -> bool { diff --git a/crates/shirabe/src/command/reinstall_command.rs b/crates/shirabe/src/command/reinstall_command.rs index e56e24b..e6cbac1 100644 --- a/crates/shirabe/src/command/reinstall_command.rs +++ b/crates/shirabe/src/command/reinstall_command.rs @@ -226,8 +226,13 @@ impl ReinstallCommand { indexmap::IndexMap::new(), ); - // TODO(phase-b): InstallationManager::execute needs `&mut dyn InstalledRepositoryInterface`; - // local_repo is borrowed shared from RepositoryManager. Needs Rc<RefCell<dyn ...>> migration. + // PHP: $installationManager->execute($localRepo, $uninstallOperations, $devMode); + // $installationManager->execute($localRepo, $installOperations, $devMode); + // TODO(phase-c): two blockers. (1) execute() wants `&mut dyn InstalledRepositoryInterface`, + // but local_repo is a RepositoryInterfaceHandle that exposes no raw &mut + // InstalledRepositoryInterface view (only per-method helpers). (2) InstallationManager:: + // execute is itself deferred (its operation/promise machinery stays todo!() — see + // installation_manager.rs). let _ = ( uninstall_operations, install_operations, @@ -281,7 +286,13 @@ impl ReinstallCommand { .as_bool() .unwrap_or(false); - // TODO(phase-b): AutoloadGenerator setters/dump need &mut self; conflicts with concurrent borrows of composer subsystems; needs shared-ownership refactor + // PHP: $generator = $composer->getAutoloadGenerator(); $generator->setClassMapAuthoritative(...); + // $generator->setApcu(...); $generator->setPlatformRequirementFilter(...); + // $generator->dump($config, $localRepo, $package, $installationManager, 'composer', $optimize); + // TODO(phase-c): AutoloadGenerator::dump (and the setters) take &mut self and dump wants + // `local_repo: &mut dyn InstalledRepositoryInterface`, which the RepositoryInterfaceHandle + // does not expose as a raw &mut view (the same handle blocker as execute above). Wiring + // this needs that accessor plus completing the dump call's remaining arguments. let _ = ( authoritative, apcu, diff --git a/crates/shirabe/src/command/require_command.rs b/crates/shirabe/src/command/require_command.rs index 44ab921..3d15f86 100644 --- a/crates/shirabe/src/command/require_command.rs +++ b/crates/shirabe/src/command/require_command.rs @@ -202,8 +202,14 @@ impl RequireCommand { None }; - // TODO(phase-b): closure captures `self` which requires complex borrow handling; the closure needs - // to call self.get_io().write_error(...), self.revert_composer_file(), and handler.exit_with_last_signal() + // PHP: function ($signal, $handler) use ($io, $self) { + // $io->writeError('Received '.$signal.', aborting', true, IOInterface::DEBUG); + // $self->revertComposerFile(); $handler->exitWithLastSignal(); } + // TODO(phase-c): SignalHandler::create takes a `Box<dyn Fn> + 'static` handler that cannot + // borrow &self, but the body must call self.revert_composer_file() (which mutates the + // command's composer.json backup state) and self.get_io(). Faithfully wiring this needs the + // revert state + io shared into the closure (Rc<RefCell<...>>), i.e. the shared-ownership + // rework of the command — the same pattern as InstallationManager::execute's signal handler. let signal_handler = SignalHandler::create( vec![ SignalHandler::SIGINT.to_string(), @@ -211,8 +217,6 @@ impl RequireCommand { SignalHandler::SIGHUP.to_string(), ], Box::new(move |signal: String, handler: &SignalHandler| { - // TODO(phase-b): self.get_io().write_error('Received '.$signal.', aborting', true, io_interface::DEBUG); - // TODO(phase-b): self.revert_composer_file(); let _ = signal; handler.exit_with_last_signal(); }), @@ -733,8 +737,13 @@ impl RequireCommand { let mut composer = crate::command::composer_full_mut(&composer_handle); self.dependency_resolution_completed = false; - // TODO(phase-b): add_listener expects a Callable enum; PHP closure should set - // self.dependency_resolution_completed = true when invoked. + // PHP: $composer->getEventDispatcher()->addListener(InstallerEvents::PRE_OPERATIONS_EXEC, + // function () use (&$dependencyResolutionCompleted) { $dependencyResolutionCompleted = true; }, 10000); + // TODO(phase-c): the event dispatcher's Callable::Closure is a placeholder variant that + // stores no actual closure, so the listener that flips dependency_resolution_completed + // cannot be registered. Resolving needs the closure model (Callable holding an Rc<dyn Fn>) + // plus dependency_resolution_completed shared (Rc<RefCell<bool>>) into both the listener + // and this command. composer.get_event_dispatcher().borrow_mut().add_listener( InstallerEvents::PRE_OPERATIONS_EXEC, crate::event_dispatcher::Callable::Closure, diff --git a/crates/shirabe/src/command/run_script_command.rs b/crates/shirabe/src/command/run_script_command.rs index ce072e4..9a28b5f 100644 --- a/crates/shirabe/src/command/run_script_command.rs +++ b/crates/shirabe/src/command/run_script_command.rs @@ -190,9 +190,19 @@ impl RunScriptCommand { .get_option("no-dev") .as_bool() .unwrap_or(false); - // TODO(phase-b): ScriptEvent::new takes Composer/IOInterface by value; placeholder construction. - let _ = (script.clone(), &composer, dev_mode); - let has_listeners = false; + let io = self.get_io(); + let event = ScriptEvent::new( + script.clone(), + composer + .as_full() + .expect("require_composer returns a full Composer") + .downgrade(), + io, + dev_mode, + vec![], + IndexMap::new(), + ); + let has_listeners = dispatcher.borrow_mut().has_event_listeners(&event); if !has_listeners { return Err(InvalidArgumentException { message: format!("Script \"{}\" is not defined in this package", script), @@ -272,7 +282,11 @@ impl RunScriptCommand { let mut result: Vec<(String, String)> = vec![]; for (name, _script) in scripts { - // TODO(phase-b): Application::find returns PhpMixed; placeholder description. + // PHP: $cmd = $this->getApplication()->find($name); $description = $cmd->getDescription(); + // TODO(phase-c): Application::find returns PhpMixed (the Symfony command registry is a + // todo!() stub) and get_application() is itself deferred, so the resolved command's + // getDescription() cannot be read; the description stays empty until the typed command + // registry is modelled. let _ = self.get_application()?.find(&name); let description = String::new(); result.push((name, description)); diff --git a/crates/shirabe/src/command/script_alias_command.rs b/crates/shirabe/src/command/script_alias_command.rs index ef95c7e..5c23e26 100644 --- a/crates/shirabe/src/command/script_alias_command.rs +++ b/crates/shirabe/src/command/script_alias_command.rs @@ -37,7 +37,12 @@ impl ScriptAliasCommand { } } - // TODO(phase-b): BaseCommand::new() / ignore_validation_errors() not yet ported + // PHP also calls parent::__construct() (Symfony Command base) and + // $this->ignoreValidationErrors(). + // TODO(phase-c): both are Symfony Command base-class operations — the constructor sets up + // the command's name/definition/application state and ignoreValidationErrors() flips a flag + // on it. Composer's BaseCommand carries no such Symfony Command state yet (the Symfony + // Command base is an intentional todo!() stub), so there is nothing to initialize here. Ok(Self { base_command_data: BaseCommandData { composer: None, diff --git a/crates/shirabe/src/command/show_command.rs b/crates/shirabe/src/command/show_command.rs index 7cf305d..06abec4 100644 --- a/crates/shirabe/src/command/show_command.rs +++ b/crates/shirabe/src/command/show_command.rs @@ -2264,11 +2264,12 @@ impl ShowCommand { ]; for color in self.colors.iter() { - let _style = OutputFormatterStyle::new(Some(color.as_str()), None, None); - // TODO(phase-c): OutputInterface::get_formatter returns &OutputFormatter, but - // set_style requires &mut. Resolution requires interior-mutability refactor of - // OutputFormatter wiring across symfony shim. - let _ = (output.borrow().get_formatter(), color); + let style = OutputFormatterStyle::new(Some(color.as_str()), None, None); + output + .borrow() + .get_formatter() + .borrow_mut() + .set_style(color, style); } } diff --git a/crates/shirabe/src/console/application.rs b/crates/shirabe/src/console/application.rs index 9fdb50b..2eb9f9a 100644 --- a/crates/shirabe/src/console/application.rs +++ b/crates/shirabe/src/console/application.rs @@ -10,6 +10,7 @@ use shirabe_external_packages::symfony::console::SingleCommandApplication; use shirabe_external_packages::symfony::console::command::Command; use shirabe_external_packages::symfony::console::exception::CommandNotFoundException; use shirabe_external_packages::symfony::console::exception::ExceptionInterface; +use shirabe_external_packages::symfony::console::helper::HelperInterface; use shirabe_external_packages::symfony::console::helper::HelperSet; use shirabe_external_packages::symfony::console::helper::QuestionHelper; use shirabe_external_packages::symfony::console::input::InputDefinition; @@ -165,9 +166,14 @@ impl Application { input: Option<std::rc::Rc<std::cell::RefCell<dyn InputInterface>>>, output: Option<std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>>, ) -> anyhow::Result<i64> { - // TODO(phase-b): Factory::create_output returns ConsoleOutput, not std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>. - // The PHP code falls back to a default output when none is supplied; for now we - // forward the caller-provided output as-is. + let output = match output { + Some(output) => Some(output), + None => Some( + std::rc::Rc::new(std::cell::RefCell::new(Factory::create_output())) + as std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, + ), + }; + self.inner.run(input, output) } @@ -183,10 +189,12 @@ impl Application { .borrow() .has_parameter_option(&["--no-scripts"], false); - // PHP: static $stdin = null; - // We use an Option here to mimic the lazy initialization. - // TODO(phase-b): stdin caching across calls needs proper resource handling; for - // now we recompute on each call via PhpMixed values to keep types consistent. + // PHP: static $stdin = null; — cached across doRun calls so the php://stdin handle is + // opened once. + // TODO(phase-c): faithfully caching the stdin resource across calls needs a real + // file-handle/resource model (PhpMixed currently boxes the handle opaquely) plus a + // function-static store. Recomputing per call is observably equivalent for a single + // invocation but differs if doRun is re-entered; deferred until the resource model lands. let stdin: PhpMixed = if defined("STDIN") { shirabe_php_shim::stdin_handle() } else { @@ -200,23 +208,17 @@ impl Application { input.borrow_mut().set_interactive(false); } - let mut helpers: Vec<PhpMixed> = vec![]; - // TODO(phase-b): QuestionHelper does not yet implement the Helper trait; - // packing it as PhpMixed defers the issue. - helpers.push(PhpMixed::Null); - let _ = QuestionHelper; - // TODO(phase-b): ConsoleIO::new takes Box<dyn>, but here input/output are - // borrowed references — defer construction until ownership story is sorted. - let _ = ConsoleIO::new; - let _ = HelperSet::new(helpers); - // self.io stays as the NullIO that was set during construction. - let io_owned = self.io.clone(); - let _ = io_owned; + // PHP: $this->io = new ConsoleIO($input, $output, new HelperSet([new QuestionHelper()])); + let helpers: Vec<std::rc::Rc<std::cell::RefCell<dyn HelperInterface>>> = + vec![std::rc::Rc::new(std::cell::RefCell::new(QuestionHelper))]; + self.io = std::rc::Rc::new(std::cell::RefCell::new(ConsoleIO::new( + input.clone(), + output.clone(), + HelperSet::new(helpers), + ))); // Register error handler again to pass it the IO instance - // TODO(phase-b): ErrorHandler::register expects Box<dyn IOInterface + Send>, - // not a borrow; passing None until the IO sharing story is settled. - ErrorHandler::register(None); + ErrorHandler::register(Some(self.io.clone())); if input.borrow().has_parameter_option(&["--no-cache"], false) { self.io @@ -259,8 +261,11 @@ impl Application { if let Some(ref raw) = raw_command_name { match self.inner.find(raw) { Ok(cmd) => { - // TODO(phase-b): BaseApplication::find returns PhpMixed; calling - // get_name() requires a Command trait downcast that is not yet wired. + // TODO(phase-c): the Symfony Application stub keeps its command registry as + // PhpMixed with a todo!() find(), per the "Symfony stays todo!()" policy. + // Reading the resolved command's getName() needs the full typed-command + // registry, which lives in the external-package stub; until that is modelled + // we cannot recover the bound command name here. let _ = cmd; command_name = Some(String::new()); } @@ -440,8 +445,9 @@ impl Application { plugin_warnings.push(format!("<warning>Plugin command {} ({}) would override a Composer command and has been skipped</warning>", cmd_name, cls)); } else { // Compatibility layer for symfony/console <7.4 - // TODO(phase-b): add_command/add accept PhpMixed; the symfony - // stubs do not yet expose typed command insertion. + // TODO(phase-c): registering a plugin command needs the Symfony + // Application's typed add()/addCommand(); the external-package stub keeps + // its registry as PhpMixed/todo!() per the "Symfony stays todo!()" policy. let _ = command; } } @@ -484,9 +490,10 @@ impl Application { let is_proxy_command = false; if let Some(ref name) = self.get_command_name_before_binding(input.clone()) { if let Ok(command) = self.inner.find(name) { - // TODO(phase-b): BaseApplication::find returns PhpMixed; we cannot yet - // extract a typed command name or detect proxy commands without the - // command trait downcast story. + // TODO(phase-c): same blocker as the earlier find() call — the Symfony command + // registry is a PhpMixed/todo!() stub, so the resolved command's name and its + // isProxyCommand() flag cannot be recovered until the typed-command registry is + // modelled in the external package. let _ = command; command_name = Some(String::new()); } @@ -681,20 +688,26 @@ impl Application { &dummy_str, vec![PhpMixed::String(script.clone())], ); - // TODO(phase-b): SingleCommandApplication has no class_name() yet. + // TODO(phase-c): the script's command class is built by + // reflection (instantiate_class) and stays PhpMixed; the + // SingleCommandApplication / Command typed registry it + // belongs to is an external-package todo!() stub. let _ = SingleCommandApplication::new; // makes sure the command is find()'able by the name defined in composer.json, and the name isn't overridden in its configure() - // TODO(phase-b): cmd is PhpMixed; get_name/set_name/get_description/set_description - // require the command trait to be unwrapped. Defer until that lands. + // TODO(phase-c): cmd is the PhpMixed result of reflection + // instantiation; reading/overriding its + // name/description requires the typed Command model that + // the Symfony stub does not yet provide. let _ = description.clone(); let _ = &mut cmd; cmd } else { // fallback to usual aliasing behavior - // TODO(phase-b): ScriptAliasCommand returns Result; bury it - // into PhpMixed::Null until the command-as-PhpMixed path is - // replaced by a typed trait object. + // TODO(phase-c): ScriptAliasCommand is a typed BaseCommand + // but this code path stores commands as PhpMixed; it can + // only be carried as a typed trait object once the Symfony + // command registry is modelled. let _ = ScriptAliasCommand::new( script.clone(), Some(description.clone()), @@ -704,8 +717,9 @@ impl Application { }; // Compatibility layer for symfony/console <7.4 - // TODO(phase-b): add_command/add take PhpMixed but expect a - // command instance; pending typed-command rewiring. + // TODO(phase-c): self.inner.add() takes PhpMixed but must + // register a typed command instance; blocked on the Symfony + // command-registry model (external-package todo!() stub). let _ = self.inner.add(cmd); } } @@ -719,13 +733,15 @@ impl Application { let result_outcome: anyhow::Result<i64> = (|| -> anyhow::Result<i64> { if input.borrow().has_parameter_option(&["--profile"], false) { start_time = Some(microtime(true)); - // TODO(phase-b): enable_debugging is defined only on ConsoleIO, not - // through IOInterface. Skip until the IO concrete type is known here. + // PHP: $this->io->enableDebugging($startTime). + // TODO(phase-c): enableDebugging exists only on ConsoleIO, not on IOInterface, + // and self.io is still the NullIO because the ConsoleIO construction above is + // deferred (Symfony HelperSet/Helper modelling). Once self.io is the real + // ConsoleIO this becomes a concrete-type call on it. let _ = start_time.unwrap(); } - // TODO(phase-b): BaseApplication exposes only `run`, not `do_run`. - let result: i64 = todo!("BaseApplication::do_run"); + let result: i64 = self.inner.do_run(input.clone(), output.clone())?; if input .borrow() @@ -1012,8 +1028,9 @@ impl Application { } else { if required { self.io.write_error(&e.to_string()); - // TODO(phase-b): BaseApplication::are_exceptions_caught not yet - // available; fall through to returning the error. + if self.inner.are_exceptions_caught() { + std::process::exit(1); + } return Err(e); } } @@ -1027,8 +1044,10 @@ impl Application { /// Removes the cached composer instance pub fn reset_composer(&mut self) { self.composer = None; - // TODO(phase-b): reset_authentications is defined on BaseIO not IOInterface; - // skipped until the cross-trait dispatch story is settled. + let io = self.get_io(); + if let Some(base_io) = io.borrow_mut().as_base_io_mut() { + base_io.reset_authentications(); + } } /// Delegates to the underlying BaseApplication's `find` method (PHP Symfony Console). @@ -1041,16 +1060,17 @@ impl Application { } pub fn get_help(&self) -> String { - // TODO(phase-b): BaseApplication::get_help is not yet exposed via the stub. - format!("{}{}", Self::LOGO, "") + format!("{}{}", Self::LOGO, self.inner.get_help()) } /// Initializes all the composer commands. pub(crate) fn get_default_commands(&self) -> Vec<Box<dyn Command>> { - // TODO(phase-b): each shirabe command struct needs its own `impl Command` (the orphan - // rule disallowed a blanket `impl<C: HasBaseCommandData> Command for C`). Until those - // are written, expose only the inner symfony defaults. - // TODO(phase-b): BaseApplication::get_default_commands is not yet exposed. + // PHP: array_merge(parent::getDefaultCommands(), [new AboutCommand(), ...]). + // TODO(phase-c): the composer commands implement the shirabe BaseCommand trait, not the + // Symfony Command trait, and the orphan rule forbids a blanket + // `impl<C: HasBaseCommandData> Command for C`. Each command needs its own `impl Command` + // (or a wrapper) before they can be returned as `Box<dyn Command>`; the parent list is + // likewise a Symfony stub (get_default_commands returns Vec<PhpMixed>/todo!()). vec![] } @@ -1061,7 +1081,13 @@ impl Application { ) -> Option<String> { let mut input = clone(&input); // Makes ArgvInput::getFirstArgument() able to distinguish an option from an argument. - // TODO(phase-b): BaseApplication::get_definition returns PhpMixed, not InputDefinition. + // PHP: $input = clone $input; try { $input->bind($this->getDefinition()); } catch (...) {} + // return $input->getFirstArgument(); + // TODO(phase-c): two blockers. (1) PHP clones the input so binding does not mutate the + // caller's instance; the `clone` php-shim must stay todo!() and dyn InputInterface has no + // deep-clone, so we cannot produce an independent copy to bind. (2) $this->getDefinition() + // returns the application's typed InputDefinition, but the Symfony Application stub returns + // PhpMixed. Until both land, getFirstArgument cannot be computed and we return None. let _ = input; let _ = self.inner.get_definition(); None @@ -1090,48 +1116,42 @@ impl Application { } pub(crate) fn get_default_input_definition(&self) -> InputDefinition { - // TODO(phase-b): BaseApplication::get_default_input_definition is not yet exposed. - let mut definition = InputDefinition::new(vec![]); - let _ = InputOption::new( + let mut definition = self.inner.get_default_input_definition(); + definition.add_option(InputOption::new( "--profile", None, Some(InputOption::VALUE_NONE), "Display timing and memory usage information", PhpMixed::Null, - ); - definition.add_option(PhpMixed::Null); - let _ = InputOption::new( + )); + definition.add_option(InputOption::new( "--no-plugins", None, Some(InputOption::VALUE_NONE), "Whether to disable plugins.", PhpMixed::Null, - ); - definition.add_option(PhpMixed::Null); - let _ = InputOption::new( + )); + definition.add_option(InputOption::new( "--no-scripts", None, Some(InputOption::VALUE_NONE), "Skips the execution of all scripts defined in composer.json file.", PhpMixed::Null, - ); - definition.add_option(PhpMixed::Null); - let _ = InputOption::new( + )); + definition.add_option(InputOption::new( "--working-dir", Some("-d"), Some(InputOption::VALUE_REQUIRED), "If specified, use the given directory as working directory.", PhpMixed::Null, - ); - definition.add_option(PhpMixed::Null); - let _ = InputOption::new( + )); + definition.add_option(InputOption::new( "--no-cache", None, Some(InputOption::VALUE_NONE), "Prevent use of the cache", PhpMixed::Null, - ); - definition.add_option(PhpMixed::Null); + )); definition } @@ -1140,9 +1160,10 @@ impl Application { // TODO(plugin): plugin command discovery is part of the plugin API let commands: Vec<Box<dyn Command>> = vec![]; - // TODO(phase-b): Composer is a PHP class (no Clone) and the plugin manager - // pathway needs PluginCapability downcasting. Defer the full implementation - // until those are available; for now return the empty command list. + // TODO(phase-c): discovering plugin-provided commands walks the PluginManager and + // downcasts each plugin's CommandProvider capability — this is the Plugin API surface, + // which is intentionally unimplemented (see TODO(plugin) above). Returns an empty list + // until the plugin capability model exists. let _ = self.get_composer(false, Some(false), None)?; let _ = UnexpectedValueException { message: String::new(), diff --git a/crates/shirabe/src/dependency_resolver/multi_conflict_rule.rs b/crates/shirabe/src/dependency_resolver/multi_conflict_rule.rs index 5b2e107..8db0f7f 100644 --- a/crates/shirabe/src/dependency_resolver/multi_conflict_rule.rs +++ b/crates/shirabe/src/dependency_resolver/multi_conflict_rule.rs @@ -2,7 +2,7 @@ use crate::dependency_resolver::{ReasonData, Rule, RuleBase}; use anyhow::Result; -use shirabe_php_shim::{PHP_VERSION_ID, PhpMixed, RuntimeException, hash_raw}; +use shirabe_php_shim::{PHP_VERSION_ID, RuntimeException, hash_raw}; #[derive(Debug)] pub struct MultiConflictRule { @@ -11,7 +11,7 @@ pub struct MultiConflictRule { } impl MultiConflictRule { - pub fn new(mut literals: Vec<i64>, reason: PhpMixed, reason_data: PhpMixed) -> Result<Self> { + pub fn new(mut literals: Vec<i64>, reason: i64, reason_data: ReasonData) -> Result<Self> { if literals.len() < 3 { return Err(RuntimeException { message: "multi conflict rule requires at least 3 literals".to_string(), @@ -24,7 +24,7 @@ impl MultiConflictRule { literals.sort(); Ok(Self { - inner: RuleBase::new(reason.as_int().unwrap_or(0), ReasonData::from(reason_data)), + inner: RuleBase::new(reason, reason_data), literals, }) } diff --git a/crates/shirabe/src/dependency_resolver/rule.rs b/crates/shirabe/src/dependency_resolver/rule.rs index 4e5a920..e62bb7b 100644 --- a/crates/shirabe/src/dependency_resolver/rule.rs +++ b/crates/shirabe/src/dependency_resolver/rule.rs @@ -42,21 +42,6 @@ pub enum ReasonData { Fixed { package: BasePackageHandle, }, - /// Phase B placeholder for an arbitrary PHP-side value not yet mapped to a real variant. - Mixed(PhpMixed), -} - -impl From<PhpMixed> for ReasonData { - fn from(value: PhpMixed) -> Self { - // TODO(phase-b): callers should construct the appropriate variant directly; - // this catch-all keeps the rule constructors building while reason_data threading - // through PhpMixed in the resolver is still in transition. - match value { - PhpMixed::String(s) => ReasonData::String(s), - PhpMixed::Int(i) => ReasonData::Int(i), - other => ReasonData::Mixed(other), - } - } } // reason constants and // their reason data contents @@ -106,8 +91,8 @@ impl Rule { &mut self.base_mut().bitfield } - fn reason_data(&self) -> Option<&ReasonData> { - self.base().reason_data.as_ref() + fn reason_data(&self) -> &ReasonData { + &self.base().reason_data } pub fn get_literals(&self) -> Vec<i64> { @@ -161,8 +146,7 @@ impl Rule { /// @phpstan-return ReasonData pub fn get_reason_data(&self) -> &ReasonData { - // TODO(phase-b): reason_data() returns Option; PHP getReasonData unconditional - self.reason_data().unwrap() + self.reason_data() } pub fn get_required_package(&self) -> Option<String> { @@ -774,7 +758,7 @@ impl std::fmt::Display for Rule { pub struct RuleBase { pub(crate) bitfield: i64, pub(crate) request: Option<Request>, - pub(crate) reason_data: Option<ReasonData>, + pub(crate) reason_data: ReasonData, } impl RuleBase { @@ -788,7 +772,7 @@ impl RuleBase { Self { bitfield, request: None, - reason_data: Some(reason_data), + reason_data, } } diff --git a/crates/shirabe/src/dependency_resolver/rule2_literals.rs b/crates/shirabe/src/dependency_resolver/rule2_literals.rs index 7db9024..39e1d4e 100644 --- a/crates/shirabe/src/dependency_resolver/rule2_literals.rs +++ b/crates/shirabe/src/dependency_resolver/rule2_literals.rs @@ -1,7 +1,5 @@ //! ref: composer/src/Composer/DependencyResolver/Rule2Literals.php -use shirabe_php_shim::PhpMixed; - use crate::dependency_resolver::{ReasonData, Rule, RuleBase}; #[derive(Debug)] @@ -12,7 +10,7 @@ pub struct Rule2Literals { } impl Rule2Literals { - pub fn new(literal1: i64, literal2: i64, reason: PhpMixed, reason_data: PhpMixed) -> Self { + pub fn new(literal1: i64, literal2: i64, reason: i64, reason_data: ReasonData) -> Self { let (literal1, literal2) = if literal1 < literal2 { (literal1, literal2) } else { @@ -20,7 +18,7 @@ impl Rule2Literals { }; Self { - inner: RuleBase::new(reason.as_int().unwrap_or(0), ReasonData::from(reason_data)), + inner: RuleBase::new(reason, reason_data), literal1, literal2, } diff --git a/crates/shirabe/src/dependency_resolver/rule_set_generator.rs b/crates/shirabe/src/dependency_resolver/rule_set_generator.rs index 295160c..f2e7b75 100644 --- a/crates/shirabe/src/dependency_resolver/rule_set_generator.rs +++ b/crates/shirabe/src/dependency_resolver/rule_set_generator.rs @@ -53,7 +53,7 @@ impl RuleSetGenerator { package: PackageInterfaceHandle, providers: &[PackageInterfaceHandle], reason: i64, - reason_data: PhpMixed, + reason_data: rule::ReasonData, ) -> Option<GenericRule> { let mut literals = vec![-package.get_id()]; @@ -65,11 +65,7 @@ impl RuleSetGenerator { literals.push(provider.get_id()); } - Some(GenericRule::new( - literals, - reason, - rule::ReasonData::from(reason_data), - )) + Some(GenericRule::new(literals, reason, reason_data)) } /// Creates a rule to install at least one of a set of packages. @@ -80,10 +76,10 @@ impl RuleSetGenerator { &self, packages: &[PackageInterfaceHandle], reason: i64, - reason_data: PhpMixed, + reason_data: rule::ReasonData, ) -> GenericRule { let literals: Vec<i64> = packages.iter().map(|p| p.get_id()).collect(); - GenericRule::new(literals, reason, rule::ReasonData::from(reason_data)) + GenericRule::new(literals, reason, reason_data) } /// Creates a rule for two conflicting packages. @@ -95,7 +91,7 @@ impl RuleSetGenerator { issuer: PackageInterfaceHandle, provider: PackageInterfaceHandle, reason: i64, - reason_data: PhpMixed, + reason_data: rule::ReasonData, ) -> Option<Rule2Literals> { // ignore self conflict if issuer.ptr_eq(&provider) { @@ -105,7 +101,7 @@ impl RuleSetGenerator { Some(Rule2Literals::new( -issuer.get_id(), -provider.get_id(), - PhpMixed::Int(reason), + reason, reason_data, )) } @@ -114,7 +110,7 @@ impl RuleSetGenerator { &self, packages: &[PackageInterfaceHandle], reason: i64, - reason_data: PhpMixed, + reason_data: rule::ReasonData, ) -> Rule { let literals: Vec<i64> = packages.iter().map(|p| -p.get_id()).collect(); @@ -122,13 +118,11 @@ impl RuleSetGenerator { Rule::TwoLiterals(Rule2Literals::new( literals[0], literals[1], - PhpMixed::Int(reason), + reason, reason_data, )) } else { - Rule::MultiConflict( - MultiConflictRule::new(literals, PhpMixed::Int(reason), reason_data).unwrap(), - ) + Rule::MultiConflict(MultiConflictRule::new(literals, reason, reason_data).unwrap()) } } @@ -175,7 +169,7 @@ impl RuleSetGenerator { package.clone(), &[alias_of.clone()], rule::RULE_PACKAGE_ALIAS, - PhpMixed::Null, // reasonData: $package (BasePackage) + rule::ReasonData::BasePackage(package.clone()), ); self.add_rule(RuleSet::TYPE_PACKAGE, rule.map(Rule::Generic)); @@ -184,7 +178,7 @@ impl RuleSetGenerator { alias_of.clone(), &[package.clone()], rule::RULE_PACKAGE_INVERSE_ALIAS, - PhpMixed::Null, // reasonData: $package->getAliasOf() (BasePackage) + rule::ReasonData::BasePackage(alias_of.clone()), ); self.add_rule(RuleSet::TYPE_PACKAGE, inverse_rule.map(Rule::Generic)); @@ -221,7 +215,7 @@ impl RuleSetGenerator { package.clone(), &possible_requires, rule::RULE_PACKAGE_REQUIRES, - PhpMixed::Null, // reasonData: $link (Link) + rule::ReasonData::Link(link.clone()), ); self.add_rule(RuleSet::TYPE_PACKAGE, rule.map(Rule::Generic)); @@ -277,7 +271,7 @@ impl RuleSetGenerator { package.clone(), conflict.clone(), rule::RULE_PACKAGE_CONFLICT, - PhpMixed::Null, // reasonData: $link (Link) + rule::ReasonData::Link(link.clone()), ); self.add_rule(RuleSet::TYPE_PACKAGE, rule.map(Rule::TwoLiterals)); } @@ -294,8 +288,11 @@ impl RuleSetGenerator { for (name, packages) in names_packages { if packages.len() > 1 { let reason = rule::RULE_PACKAGE_SAME_NAME; - let rule = - self.create_multi_conflict_rule(&packages, reason, PhpMixed::String(name)); + let rule = self.create_multi_conflict_rule( + &packages, + reason, + rule::ReasonData::String(name), + ); self.add_rule(RuleSet::TYPE_PACKAGE, Some(rule)); } } @@ -329,15 +326,12 @@ impl RuleSetGenerator { self.add_rules_for_package(package.clone().into(), platform_requirement_filter); - let mut reason_data: IndexMap<String, Box<PhpMixed>> = IndexMap::new(); - reason_data.insert( - "package".to_string(), - Box::new(PhpMixed::Null), // reasonData: $package (BasePackage) - ); let rule = self.create_install_one_of_rule( &[package.clone().into()], rule::RULE_FIXED, - PhpMixed::Array(reason_data), + rule::ReasonData::Fixed { + package: package.clone().into(), + }, ); self.add_rule(RuleSet::TYPE_REQUEST, Some(Rule::Generic(rule))); } diff --git a/crates/shirabe/src/downloader/path_downloader.rs b/crates/shirabe/src/downloader/path_downloader.rs index 66f84b6..6d0e098 100644 --- a/crates/shirabe/src/downloader/path_downloader.rs +++ b/crates/shirabe/src/downloader/path_downloader.rs @@ -472,7 +472,11 @@ impl DownloaderInterface for PathDownloader { ); } let _iterator = ArchivableFilesFinder::new(&real_url, vec![], false)?; - // TODO(phase-b): pass iterator as PhpMixed; ArchivableFilesFinder iterator wrapping not modelled yet. + // PHP: $symfonyFilesystem->mirror($realUrl, $path, $iterator); + // TODO(phase-c): Symfony Filesystem::mirror takes a Traversable iterator as its third + // argument, but the external-package Filesystem stub does not model the iterator type + // that ArchivableFilesFinder (an IteratorAggregate) would be wrapped into, so None is + // passed and the mirrored file list is not restricted. symfony_filesystem.mirror(&real_url, &path, None, &IndexMap::new())?; } diff --git a/crates/shirabe/src/event_dispatcher/event_dispatcher.rs b/crates/shirabe/src/event_dispatcher/event_dispatcher.rs index 729979d..2124093 100644 --- a/crates/shirabe/src/event_dispatcher/event_dispatcher.rs +++ b/crates/shirabe/src/event_dispatcher/event_dispatcher.rs @@ -267,10 +267,11 @@ impl EventDispatcher { } // other newly appeared prepended autoloaders should be appended instead to ensure Composer loads its classes first + // PHP: spl_autoload_unregister($cb); spl_autoload_register($cb, true, $prepend); // TODO(plugin): ClassLoader detection via instanceof — currently treat all callbacks uniformly - // TODO(phase-b): `cb` is a PhpMixed; spl_autoload_*/register expect a typed - // Box<dyn Fn(&str) -> PhpMixed + Send + Sync> callback. Bridging requires - // exposing the underlying callable from PhpMixed. + // TODO(phase-c): `cb` is a PhpMixed holding a callable; spl_autoload_register/unregister + // (php-shims that stay todo!()) need a typed Box<dyn Fn(&str) -> PhpMixed> callback. + // Bridging requires the callable model to expose the underlying closure from PhpMixed. let _ = &cb; let _ = spl_autoload_unregister; let _ = spl_autoload_register; diff --git a/crates/shirabe/src/factory.rs b/crates/shirabe/src/factory.rs index f1c66ce..ab0d389 100644 --- a/crates/shirabe/src/factory.rs +++ b/crates/shirabe/src/factory.rs @@ -419,10 +419,13 @@ impl Factory { } pub fn create_output() -> ConsoleOutput { - let _styles = Self::create_additional_styles(); - // TODO(phase-b): OutputFormatter::new signature and ConsoleOutput::new_with_formatter missing - todo!( - "create_output: wire OutputFormatter into ConsoleOutput once the symfony console stubs are completed" + let styles = Self::create_additional_styles(); + let formatter = OutputFormatter::new(false, styles); + + ConsoleOutput::new( + shirabe_external_packages::symfony::console::output::output_interface::VERBOSITY_NORMAL, + None, + Some(std::rc::Rc::new(std::cell::RefCell::new(formatter))), ) } @@ -678,9 +681,13 @@ impl Factory { "Composer\\Package\\RootPackage", Some(&cwd), )?; - // TODO(phase-b): set_package expects RootPackageInterface; loader returns BasePackage - // composer.set_package(package); - let _ = package; + // load() builds a RootPackage of the requested class, so as_root() is always + // Some; setPackage takes a RootPackageInterface in PHP. + composer.set_package( + package + .as_root() + .expect("RootPackageLoader::load returns a RootPackage"), + ); // load local repository self.add_local_repository( @@ -853,7 +860,13 @@ impl Factory { // once everything is initialized we can // purge packages from local repos if they have been deleted on the filesystem - // TODO(phase-b): rm and im are owned by composer at this point; need to access via composer + // PHP: $this->purgePackages($rm->getLocalRepository(), $im); + // TODO(phase-c): the rm/im locals are still in scope (Rc-shared with composer), but + // purge_packages wants `&mut dyn InstalledRepositoryInterface` and + // RepositoryManager::get_local_repository yields a RepositoryInterfaceHandle that + // exposes no raw &mut InstalledRepositoryInterface view (only per-method helpers that + // borrow internally). Wiring this needs such an accessor plus completing + // purge_packages' removal body (repo.removePackage), which is itself still a stub. // self.purge_packages(rm.get_local_repository(), &mut im)?; } @@ -1219,8 +1232,19 @@ impl Factory { .unwrap_or_default(), Some("/"), ); - // TODO(phase-b): BinaryInstaller is a PHP class so it can't be cloned. Sharing requires - // Rc<RefCell<BinaryInstaller>>; for now construct one per installer. + // PHP: $binaryInstaller = new BinaryInstaller(...); + // $im->addInstaller(new LibraryInstaller($io, $composer, null, $fs, $binaryInstaller)); + // $im->addInstaller(new PluginInstaller($io, $composer, $fs, $binaryInstaller)); + // $im->addInstaller(new MetapackageInstaller($io)); + // The same BinaryInstaller object is shared by the Library and Plugin installers. + // TODO(phase-c): two coupled blockers. (1) Sharing one BinaryInstaller requires + // Rc<RefCell<BinaryInstaller>>; LibraryInstaller/PluginInstaller currently own a + // `BinaryInstaller` by value. (2) Both installers' constructors take a + // PartialComposerWeakHandle, but create_default_installers runs (line 737) before the + // composer is wrapped in its shared Rc, so no weak handle is obtainable here yet — + // installer registration must move after the composer Rc is established. Both are part of + // the composer construction-ordering / shared-ownership rework, so no installers are + // registered for now. let _binary_installer = BinaryInstaller::new( io.clone(), bin_dir.clone(), diff --git a/crates/shirabe/src/installer/installation_manager.rs b/crates/shirabe/src/installer/installation_manager.rs index 4d5608d..90801ec 100644 --- a/crates/shirabe/src/installer/installation_manager.rs +++ b/crates/shirabe/src/installer/installation_manager.rs @@ -117,7 +117,14 @@ impl InstallationManager { for installer in &self.installers { if installer.supports(&r#type) { - // TODO(phase-b): cache by cloning Box<dyn InstallerInterface> is non-trivial + // PHP: return $this->cache[$type] = $installer; — the cache holds the SAME + // installer instance as $this->installers. + // TODO(phase-c): faithfully sharing the instance requires storing installers as + // Rc<RefCell<dyn InstallerInterface>> (so the cache clones the Rc, not the value). + // That refactor is blocked because get_installer's result is used across `.await` + // points in download/install/update/uninstall, where a RefCell borrow cannot be + // held; it is coupled to the async/React-Promise execution rework this file's + // execute() path depends on. clone_box (itself a todo!()) is the placeholder. self.cache.insert(r#type.clone(), installer.clone_box()); return Ok(self.cache.get_mut(&r#type).unwrap().as_mut()); } @@ -326,7 +333,14 @@ impl InstallationManager { } let installer = self.get_installer(&package.get_type())?; - // TODO(phase-b): closure captures installer + package; needs Rc-shared installer/package + // PHP: $cleanupPromises[$index] = function () use ($index, $installer, $type, $package) { + // if (null === $package->getInstallationSource()) { return \React\Promise\resolve(null); } + // return $installer->cleanup($type, $package); }; + // TODO(phase-c): the cleanup callable must capture the installer and package and invoke + // installer.cleanup(...) returning a React promise. It is a 'static closure stored in + // cleanup_promises, so installer/package must be Rc-shared (the installer registry is + // not Rc yet, see get_installer) and the promise type must be modelled. Both depend on + // the async/React-Promise rework, so a no-op future is stored instead. let _ = installer; let op_type_clone = op_type.clone(); let cleanup: Box< @@ -507,7 +521,12 @@ impl InstallationManager { .prepare(&op_type, package, initial_package) .await?; - // TODO(phase-b): chain the install/update/uninstall step with cleanup_promises[index], repo.write, etc. + // PHP: $promise = $promise->then(fn() => $this->{$type}(...))->then($cleanupPromises[$index]) + // ->then(fn() => $repo->write($devMode, $this->io)); the chained steps run install/ + // update/uninstall, then cleanup, then persist the repository. + // TODO(phase-c): this promise chain (install step -> cleanup_promises[index] -> + // repo.write) needs the React\Promise model and the cleanup callables wired (see above); + // both stay todo!(), so only prepare() is awaited here. let _ = cleanup_promises.get(&index); let event_name_post = match op_type.as_str() { @@ -518,7 +537,11 @@ impl InstallationManager { }; if run_scripts && self.event_dispatcher.is_some() { - // TODO(phase-b): post-exec callback captures &mut dispatcher and operation + // PHP appends a post-exec step to the promise chain that dispatches the + // POST_PACKAGE_* event via the event dispatcher with repo/all_operations/operation. + // TODO(phase-c): the callback captures the event dispatcher (&mut) and the operation + // and must outlive the loop body; that requires the dispatcher behind Rc<RefCell> + // and the deferred event dispatch to be wired into the promise chain (todo!()). let _ = event_name_post; post_exec_callbacks.push(Box::new(|| { // dispatcher.dispatch_package_event(event_name_post, dev_mode, repo, all_operations, operation); diff --git a/crates/shirabe/src/installer/library_installer.rs b/crates/shirabe/src/installer/library_installer.rs index decc1c2..da5533c 100644 --- a/crates/shirabe/src/installer/library_installer.rs +++ b/crates/shirabe/src/installer/library_installer.rs @@ -189,8 +189,7 @@ impl LibraryInstaller { self.filesystem .borrow_mut() .ensure_directory_exists(&self.vendor_dir); - // TODO(phase-b): realpath returns Option<String>; PHP assigns to vendorDir even when false - self.vendor_dir = realpath(&self.vendor_dir).unwrap(); + self.vendor_dir = realpath(&self.vendor_dir).unwrap_or_default(); } pub(crate) fn get_download_manager(&self) -> &std::rc::Rc<std::cell::RefCell<DownloadManager>> { diff --git a/crates/shirabe/src/installer/plugin_installer.rs b/crates/shirabe/src/installer/plugin_installer.rs index 23d7098..2644639 100644 --- a/crates/shirabe/src/installer/plugin_installer.rs +++ b/crates/shirabe/src/installer/plugin_installer.rs @@ -94,9 +94,12 @@ impl InstallerInterface for PluginInstaller { .get("plugin-optional") .map(|v| matches!(v, PhpMixed::Bool(true))) .unwrap_or(false); - // TODO(plugin): check if plugin is allowed - // TODO(phase-b): is_plugin_allowed needs &mut PluginManager but prepare is &self. - let _ = plugin_optional; + self.get_plugin_manager().borrow_mut().is_plugin_allowed( + &package.get_name(), + false, + plugin_optional, + true, + )?; } self.inner.prepare(r#type, package, prev_package).await diff --git a/crates/shirabe/src/io/base_io.rs b/crates/shirabe/src/io/base_io.rs index f778336..1d12470 100644 --- a/crates/shirabe/src/io/base_io.rs +++ b/crates/shirabe/src/io/base_io.rs @@ -13,9 +13,6 @@ use shirabe_php_shim::{ UnexpectedValueException, array_merge, in_array, json_encode_ex, }; -// TODO(phase-b): default implementations in a subtrait cannot override supertrait methods in Rust; -// write/write_error etc. from IOInterface are called through the supertrait and must be provided -// by concrete types implementing both BaseIO and IOInterface. pub trait BaseIO: IOInterface { fn authentications(&self) -> &IndexMap<String, IndexMap<String, Option<String>>>; fn authentications_mut(&mut self) -> &mut IndexMap<String, IndexMap<String, Option<String>>>; diff --git a/crates/shirabe/src/io/buffer_io.rs b/crates/shirabe/src/io/buffer_io.rs index 92f5714..988c153 100644 --- a/crates/shirabe/src/io/buffer_io.rs +++ b/crates/shirabe/src/io/buffer_io.rs @@ -4,6 +4,7 @@ use crate::io::ConsoleIO; use anyhow::Result; use shirabe_external_packages::composer::pcre::Preg; use shirabe_external_packages::symfony::console::formatter::OutputFormatterInterface; +use shirabe_external_packages::symfony::console::helper::HelperInterface; use shirabe_external_packages::symfony::console::helper::HelperSet; use shirabe_external_packages::symfony::console::helper::QuestionHelper; use shirabe_external_packages::symfony::console::input::InputInterface; @@ -46,11 +47,11 @@ impl BufferIO { let output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>> = todo!("wire StreamOutput as the ConsoleIO output"); - // TODO(phase-c): construct the QuestionHelper and register it in the HelperSet. - let helpers: Vec<PhpMixed> = vec![/* PhpMixed::Object(QuestionHelper::new()) */]; - let _ = std::marker::PhantomData::<QuestionHelper>; + let helpers: Vec<std::rc::Rc<std::cell::RefCell<dyn HelperInterface>>> = + vec![std::rc::Rc::new(std::cell::RefCell::new(QuestionHelper))]; let inner = ConsoleIO::new( - Box::new(input_obj) as Box<dyn InputInterface>, + std::rc::Rc::new(std::cell::RefCell::new(input_obj)) + as std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, output, HelperSet::new(helpers), ); @@ -129,8 +130,6 @@ impl BufferIO { } } -// TODO(phase-b): PHP `class BufferIO extends ConsoleIO` — delegate all -// IOInterface and BaseIO methods to `self.inner` (ConsoleIO). impl crate::io::IOInterfaceImmutable for BufferIO { fn is_interactive(&self) -> bool { self.inner.is_interactive() @@ -249,6 +248,10 @@ impl crate::io::IOInterface for BufferIO { fn as_any(&self) -> &dyn std::any::Any { self } + + fn as_base_io_mut(&mut self) -> Option<&mut dyn crate::io::BaseIO> { + Some(self) + } } impl crate::io::BaseIO for BufferIO { diff --git a/crates/shirabe/src/io/console_io.rs b/crates/shirabe/src/io/console_io.rs index a4a3c21..18287e3 100644 --- a/crates/shirabe/src/io/console_io.rs +++ b/crates/shirabe/src/io/console_io.rs @@ -7,6 +7,7 @@ use indexmap::indexmap; use shirabe_external_packages::composer::pcre::Preg; use shirabe_external_packages::symfony::console::helper::HelperSet; use shirabe_external_packages::symfony::console::helper::ProgressBar; +use shirabe_external_packages::symfony::console::helper::QuestionHelper; use shirabe_external_packages::symfony::console::helper::Table; use shirabe_external_packages::symfony::console::input::InputInterface; use shirabe_external_packages::symfony::console::output::ConsoleOutputInterface; @@ -30,10 +31,11 @@ use crate::question::StrictConfirmationQuestion; use crate::util::Silencer; /// The Input/Output helper. +#[derive(Debug)] pub struct ConsoleIO { authentications: indexmap::IndexMap<String, indexmap::IndexMap<String, Option<String>>>, - pub(crate) input: Box<dyn InputInterface>, + pub(crate) input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, pub(crate) output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, pub(crate) helper_set: HelperSet, pub(crate) last_message: RefCell<String>, @@ -45,21 +47,6 @@ pub struct ConsoleIO { verbosity_map: IndexMap<i64, i64>, } -// TODO(phase-b): dyn InputInterface / dyn OutputInterface do not implement Debug, -// so we cannot derive Debug. Provide a manual impl that omits those fields. -impl std::fmt::Debug for ConsoleIO { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("ConsoleIO") - .field("authentications", &self.authentications) - .field("helper_set", &self.helper_set) - .field("last_message", &self.last_message) - .field("last_message_err", &self.last_message_err) - .field("start_time", &self.start_time) - .field("verbosity_map", &self.verbosity_map) - .finish() - } -} - impl ConsoleIO { /// Constructor. /// @@ -67,7 +54,7 @@ impl ConsoleIO { /// @param OutputInterface $output The output instance /// @param HelperSet $helperSet The helperSet instance pub fn new( - input: Box<dyn InputInterface>, + input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, helper_set: HelperSet, ) -> Self { @@ -375,7 +362,7 @@ impl ConsoleIO { impl IOInterfaceImmutable for ConsoleIO { fn is_interactive(&self) -> bool { - self.input.is_interactive() + self.input.borrow().is_interactive() } fn is_verbose(&self) -> bool { @@ -447,8 +434,7 @@ impl IOInterfaceImmutable for ConsoleIO { } fn ask(&self, question: String, default: PhpMixed) -> PhpMixed { - // PHP: $helper = $this->helperSet->get('question'); - let _helper = self.helper_set.get("question"); + let helper = self.helper_set.get::<QuestionHelper>("question"); let sanitized_question = Self::sanitize(PhpMixed::String(question), true) .as_string() .unwrap_or("") @@ -458,30 +444,32 @@ impl IOInterfaceImmutable for ConsoleIO { } else { Some(default) }; - let _question = Question::new(&sanitized_question, sanitized_default); + let question = Question::new(&sanitized_question, sanitized_default); - // TODO(phase-b): HelperSet::get returns PhpMixed; QuestionHelper::ask is - // not yet modeled. Returning a placeholder until helper types are wired up. - todo!("call QuestionHelper::ask on resolved helper") + helper + .borrow() + .ask(self.input.clone(), self.get_error_output(), &question) } fn ask_confirmation(&self, question: String, default: bool) -> bool { - let _helper = self.helper_set.get("question"); - // TODO(phase-b): Self::sanitize returns PhpMixed but new() expects String; - // also true/false regexes need to come through composer/symfony defaults. + let helper = self.helper_set.get::<QuestionHelper>("question"); let sanitized = Self::sanitize(PhpMixed::String(question), true) .as_string() .unwrap_or("") .to_string(); - let _question = StrictConfirmationQuestion::new( + let question = StrictConfirmationQuestion::new( sanitized, default, "/^y(?:es)?$/i".to_string(), "/^no?$/i".to_string(), ); - // TODO(phase-b): see ask() above; placeholder until QuestionHelper is modeled. - todo!("call QuestionHelper::ask on resolved helper and coerce to bool") + let result = helper.borrow().ask( + self.input.clone(), + self.get_error_output(), + question.inner(), + ); + result.as_bool().unwrap_or(false) } fn ask_and_validate( @@ -491,7 +479,6 @@ impl IOInterfaceImmutable for ConsoleIO { attempts: Option<i64>, default: PhpMixed, ) -> anyhow::Result<PhpMixed> { - let _helper = self.helper_set.get("question"); let sanitized_question = Self::sanitize(PhpMixed::String(question), true) .as_string() .unwrap_or("") @@ -509,12 +496,13 @@ impl IOInterfaceImmutable for ConsoleIO { question.set_validator(Some(adapted)); question.set_max_attempts(attempts); - // TODO(phase-b): QuestionHelper::ask not yet modeled. - todo!("call QuestionHelper::ask on resolved helper") + let helper = self.helper_set.get::<QuestionHelper>("question"); + Ok(helper + .borrow() + .ask(self.input.clone(), self.get_error_output(), &question)) } fn ask_and_hide_answer(&self, question: String) -> Option<String> { - let _helper = self.helper_set.get("question"); let sanitized_question = Self::sanitize(PhpMixed::String(question), true) .as_string() .unwrap_or("") @@ -522,8 +510,11 @@ impl IOInterfaceImmutable for ConsoleIO { let mut question = Question::new(&sanitized_question, Some(PhpMixed::Null)); question.set_hidden(true); - // TODO(phase-b): QuestionHelper::ask not yet modeled. - todo!("call QuestionHelper::ask on resolved helper and coerce to Option<String>") + let helper = self.helper_set.get::<QuestionHelper>("question"); + let result = helper + .borrow() + .ask(self.input.clone(), self.get_error_output(), &question); + result.as_string().map(|s| s.to_string()) } fn select( @@ -542,7 +533,7 @@ impl IOInterfaceImmutable for ConsoleIO { .map(|s| Box::new(PhpMixed::String(s))) .collect(), ); - let _helper = self.helper_set.get("question"); + let _helper = self.helper_set.get::<QuestionHelper>("question"); let sanitized_question = Self::sanitize(PhpMixed::String(question), true) .as_string() .unwrap_or("") @@ -573,8 +564,13 @@ impl IOInterfaceImmutable for ConsoleIO { question.set_error_message(&error_message); question.set_multiselect(multiselect); - // TODO(phase-b): QuestionHelper::ask not yet modeled. - let result: PhpMixed = todo!("call QuestionHelper::ask on resolved helper"); + // TODO(phase-c): QuestionHelper::ask takes a concrete `&Question`, but PHP passes a + // ChoiceQuestion whose getValidator/getDefault/autocompleter overrides drive the answer + // handling via polymorphism. Passing the inner Question would silently drop that behaviour. + // Faithful support needs the Symfony Question hierarchy modelled as a trait (or ask taking a + // trait object) so ChoiceQuestion's overrides dispatch — unlike StrictConfirmationQuestion, + // whose behaviour lives entirely on its inner Question (see ask_confirmation above). + let result: PhpMixed = todo!("call QuestionHelper::ask with a polymorphic ChoiceQuestion"); // PHP: $isAssoc = (bool) \count(array_filter(array_keys($choices), 'is_string')); let choice_keys: Vec<String> = match &choices { @@ -670,6 +666,10 @@ impl IOInterface for ConsoleIO { fn as_any(&self) -> &dyn std::any::Any { self } + + fn as_base_io_mut(&mut self) -> Option<&mut dyn BaseIO> { + Some(self) + } } impl BaseIO for ConsoleIO { diff --git a/crates/shirabe/src/io/io_interface.rs b/crates/shirabe/src/io/io_interface.rs index e33cea2..1ae8d28 100644 --- a/crates/shirabe/src/io/io_interface.rs +++ b/crates/shirabe/src/io/io_interface.rs @@ -151,6 +151,10 @@ pub trait IOInterfaceMutable { // `dyn IOInterface`; its vtable carries both the immutable and mutable methods. pub trait IOInterface: IOInterfaceImmutable + IOInterfaceMutable { fn as_any(&self) -> &dyn std::any::Any; + + fn as_base_io_mut(&mut self) -> Option<&mut dyn crate::io::BaseIO> { + None + } } // Shared-ownership handle for a PHP IO instance (reference semantics). It diff --git a/crates/shirabe/src/io/null_io.rs b/crates/shirabe/src/io/null_io.rs index 0a83bff..ee533c1 100644 --- a/crates/shirabe/src/io/null_io.rs +++ b/crates/shirabe/src/io/null_io.rs @@ -142,6 +142,10 @@ impl IOInterface for NullIO { fn as_any(&self) -> &dyn std::any::Any { self } + + fn as_base_io_mut(&mut self) -> Option<&mut dyn BaseIO> { + Some(self) + } } impl BaseIO for NullIO { diff --git a/crates/shirabe/src/package/version/version_guesser.rs b/crates/shirabe/src/package/version/version_guesser.rs index 3b61eac..da67d56 100644 --- a/crates/shirabe/src/package/version/version_guesser.rs +++ b/crates/shirabe/src/package/version/version_guesser.rs @@ -411,12 +411,24 @@ impl VersionGuesser { } // re-use the HgDriver to fetch branches (this properly includes bookmarks) - let _io = NullIO::new(); let mut repo_config: IndexMap<String, PhpMixed> = IndexMap::new(); repo_config.insert("url".to_string(), PhpMixed::String(path.to_string())); - // TODO(phase-b): HgDriver lacks a `new` constructor and HttpDownloader::new signature is unknown - let mut driver: HgDriver = todo!( - "HgDriver::new(repo_config, Box::new(io), self.config.clone(), HttpDownloader::new(io, config), Rc::clone(&self.process))" + let io: std::rc::Rc<std::cell::RefCell<dyn crate::io::IOInterface>> = + std::rc::Rc::new(std::cell::RefCell::new(NullIO::new())); + let http_io: std::rc::Rc<std::cell::RefCell<dyn crate::io::IOInterface>> = + std::rc::Rc::new(std::cell::RefCell::new(NullIO::new())); + let http_downloader = std::rc::Rc::new(std::cell::RefCell::new(HttpDownloader::new( + http_io, + self.config.clone(), + IndexMap::new(), + false, + ))); + let mut driver = HgDriver::new( + repo_config, + io, + self.config.clone(), + http_downloader, + std::rc::Rc::clone(&self.process), ); let branches: Vec<String> = array_map(|k: &String| k.clone(), &array_keys(&driver.get_branches()?)); diff --git a/crates/shirabe/src/plugin/plugin_manager.rs b/crates/shirabe/src/plugin/plugin_manager.rs index 9dcf754..24899cd 100644 --- a/crates/shirabe/src/plugin/plugin_manager.rs +++ b/crates/shirabe/src/plugin/plugin_manager.rs @@ -920,15 +920,20 @@ impl PluginManager { "allow-plugins", PhpMixed::Array(allow_plugins.clone()), )?; - // TODO(phase-b): get_config() returns &Config, but merge needs &mut Config; ownership needs refactoring let mut inner: IndexMap<String, Box<PhpMixed>> = IndexMap::new(); inner.insert( "allow-plugins".to_string(), Box::new(PhpMixed::Array(allow_plugins)), ); - let mut wrap: IndexMap<String, PhpMixed> = IndexMap::new(); - wrap.insert("config".to_string(), PhpMixed::Array(inner)); - todo!("see TODO(phase-b) above"); + let mut config_section: IndexMap<String, Box<PhpMixed>> = + IndexMap::new(); + config_section + .insert("config".to_string(), Box::new(PhpMixed::Array(inner))); + let wrap: IndexMap<String, PhpMixed> = + config_section.into_iter().map(|(k, v)| (k, *v)).collect(); + config + .borrow_mut() + .merge(&wrap, crate::config::Config::SOURCE_UNKNOWN); } } diff --git a/crates/shirabe/src/question/strict_confirmation_question.rs b/crates/shirabe/src/question/strict_confirmation_question.rs index e291c56..5ce32f2 100644 --- a/crates/shirabe/src/question/strict_confirmation_question.rs +++ b/crates/shirabe/src/question/strict_confirmation_question.rs @@ -32,6 +32,10 @@ impl StrictConfirmationQuestion { this } + pub fn inner(&self) -> &Question { + &self.inner + } + fn get_default_normalizer(&self) -> Box<dyn Fn(&PhpMixed) -> PhpMixed> { let default = self.inner.get_default(); let true_regex = self.true_answer_regex.clone(); diff --git a/crates/shirabe/src/repository/platform_repository.rs b/crates/shirabe/src/repository/platform_repository.rs index 1dec137..932584c 100644 --- a/crates/shirabe/src/repository/platform_repository.rs +++ b/crates/shirabe/src/repository/platform_repository.rs @@ -283,9 +283,12 @@ impl PlatformRepository { // The AF_INET6 constant is only defined if ext-sockets is available but // IPv6 support might still be available. let has_inet6 = self.runtime.has_constant("AF_INET6", None); + // PHP: Silencer::call([$this->runtime, 'invoke'], 'inet_pton', ['::']) + // TODO(phase-c): Composer's Platform\Runtime class is entirely a todo!() stub (invoke, + // has_constant, ...), so the inet_pton invocation cannot run. The placeholder callable + // returns false; resolving this requires implementing the Runtime wrapper (separate Phase C + // work) and a closure that forwards to the inet_pton shim. let inet_pton_check = Silencer::call(|| { - // TODO(phase-b): Runtime::invoke takes a Box<dyn Fn(Vec<PhpMixed>) -> PhpMixed>; - // mirror PHP's `Silencer::call([$this->runtime, 'invoke'], 'inet_pton', ['::'])`. Ok::<PhpMixed, anyhow::Error>(self.runtime.invoke( Box::new(|_args| PhpMixed::Bool(false)), vec![ diff --git a/crates/shirabe/src/repository/vcs/git_bitbucket_driver.rs b/crates/shirabe/src/repository/vcs/git_bitbucket_driver.rs index 59e6b88..b33aa05 100644 --- a/crates/shirabe/src/repository/vcs/git_bitbucket_driver.rs +++ b/crates/shirabe/src/repository/vcs/git_bitbucket_driver.rs @@ -790,14 +790,13 @@ impl GitBitbucketDriver { fn setup_fallback_driver(&mut self, url: &str) -> Result<()> { let mut repo_config: IndexMap<String, PhpMixed> = IndexMap::new(); repo_config.insert("url".to_string(), PhpMixed::String(url.to_string())); - // TODO(phase-b): construct VcsDriver from repo_config / io / config / etc. - let mut driver = GitDriver { - inner: todo!("phase-b: build VcsDriver for fallback GitDriver"), - tags: None, - branches: None, - root_identifier: None, - repo_dir: String::new(), - }; + let mut driver = GitDriver::new( + repo_config, + self.inner.io.clone(), + self.inner.config.clone(), + self.inner.http_downloader.clone(), + self.inner.process.clone(), + ); driver.initialize()?; self.fallback_driver = Some(Box::new(driver)); Ok(()) diff --git a/crates/shirabe/src/repository/vcs/vcs_driver.rs b/crates/shirabe/src/repository/vcs/vcs_driver.rs index 740d00e..b197676 100644 --- a/crates/shirabe/src/repository/vcs/vcs_driver.rs +++ b/crates/shirabe/src/repository/vcs/vcs_driver.rs @@ -187,8 +187,6 @@ impl VcsDriverBase { } } -// TODO(phase-b): the constructor is `final` in PHP; concrete implementations must replicate the -// initialization logic (local-path normalization etc.) from the original new() body. pub trait VcsDriver: VcsDriverInterface { fn url(&self) -> &str; fn url_mut(&mut self) -> &mut String; diff --git a/crates/shirabe/src/util/auth_helper.rs b/crates/shirabe/src/util/auth_helper.rs index b54b54b..2373265 100644 --- a/crates/shirabe/src/util/auth_helper.rs +++ b/crates/shirabe/src/util/auth_helper.rs @@ -56,7 +56,6 @@ impl AuthHelper { /// @param 'prompt'|bool $storeAuth pub fn store_auth(&self, origin: &str, store_auth: StoreAuth) -> Result<()> { - // TODO(phase-b): config.get_auth_config_source() and ConfigSource methods are stubs let mut store: Option<()> = None; let mut config = self.config.borrow_mut(); let config_source = config.get_auth_config_source_mut(); diff --git a/crates/shirabe/src/util/error_handler.rs b/crates/shirabe/src/util/error_handler.rs index c0648be..e0590d9 100644 --- a/crates/shirabe/src/util/error_handler.rs +++ b/crates/shirabe/src/util/error_handler.rs @@ -1,18 +1,21 @@ //! ref: composer/src/Composer/Util/ErrorHandler.php use crate::io::IOInterface; +use crate::io::IOInterfaceImmutable; use shirabe_php_shim::{ E_ALL, E_DEPRECATED, E_USER_DEPRECATED, E_USER_WARNING, E_WARNING, ErrorException, FILTER_VALIDATE_BOOLEAN, PHP_EOL, PhpMixed, STDERR, debug_backtrace, error_reporting, filter_var, ini_get, is_resource, set_error_handler, }; -use std::sync::{Mutex, OnceLock}; +use std::cell::{Cell, RefCell}; +use std::rc::Rc; -static IO: OnceLock<Mutex<Option<Box<dyn IOInterface + Send>>>> = OnceLock::new(); -static HAS_SHOWN_DEPRECATION_NOTICE: Mutex<i64> = Mutex::new(0); - -fn io() -> &'static Mutex<Option<Box<dyn IOInterface + Send>>> { - IO.get_or_init(|| Mutex::new(None)) +// PHP keeps `$io` / `$hasShownDeprecationNotice` as process-global statics. Composer runs +// single-threaded, so thread-locals on the main thread reproduce that faithfully while letting +// us hold the same shared (`Rc<RefCell<dyn IOInterface>>`) IO instance the application uses. +thread_local! { + static IO: RefCell<Option<Rc<RefCell<dyn IOInterface>>>> = const { RefCell::new(None) }; + static HAS_SHOWN_DEPRECATION_NOTICE: Cell<i64> = const { Cell::new(0) }; } pub struct ErrorHandler; @@ -65,18 +68,17 @@ impl ErrorHandler { }); } - let mut io_guard = io().lock().unwrap(); - if io_guard.is_some() { - let has_shown = *HAS_SHOWN_DEPRECATION_NOTICE.lock().unwrap(); - if has_shown > 0 && !io_guard.as_ref().unwrap().is_verbose() { + let io = IO.with(|cell| cell.borrow().clone()); + if let Some(io) = io { + let has_shown = HAS_SHOWN_DEPRECATION_NOTICE.with(|c| c.get()); + if has_shown > 0 && !io.is_verbose() { if has_shown == 1 { - io_guard.as_mut().unwrap().write_error("<warning>More deprecation notices were hidden, run again with `-v` to show them.</warning>"); - *HAS_SHOWN_DEPRECATION_NOTICE.lock().unwrap() = 2; + io.write_error("<warning>More deprecation notices were hidden, run again with `-v` to show them.</warning>"); + HAS_SHOWN_DEPRECATION_NOTICE.with(|c| c.set(2)); } return Ok(true); } - *HAS_SHOWN_DEPRECATION_NOTICE.lock().unwrap() = 1; - drop(io_guard); + HAS_SHOWN_DEPRECATION_NOTICE.with(|c| c.set(1)); Self::output_warning( &format!("Deprecation Notice: {} in {}:{}", message, file, line), false, @@ -86,17 +88,17 @@ impl ErrorHandler { Ok(true) } - pub fn register(io: Option<Box<dyn IOInterface + Send>>) { + pub fn register(io: Option<Rc<RefCell<dyn IOInterface>>>) { set_error_handler(|level, message, file, line| { Self::handle(level, message.to_string(), file.to_string(), line).unwrap_or(true) }); error_reporting(Some(E_ALL)); - *self::io().lock().unwrap() = io; + IO.with(|cell| *cell.borrow_mut() = io); } fn output_warning(message: &str, output_even_without_io: bool) { - let mut io_guard = io().lock().unwrap(); - if let Some(io) = io_guard.as_mut() { + let io = IO.with(|cell| cell.borrow().clone()); + if let Some(io) = io { io.write_error(&format!("<warning>{}</warning>", message)); if io.is_verbose() { io.write_error("<warning>Stack trace:</warning>"); @@ -122,7 +124,6 @@ impl ErrorHandler { } return; } - drop(io_guard); if output_even_without_io { if is_resource(&PhpMixed::Int(STDERR)) { diff --git a/crates/shirabe/src/util/http/curl_downloader.rs b/crates/shirabe/src/util/http/curl_downloader.rs index c9eb2f8..a0ef54f 100644 --- a/crates/shirabe/src/util/http/curl_downloader.rs +++ b/crates/shirabe/src/util/http/curl_downloader.rs @@ -645,7 +645,11 @@ impl CurlDownloader { ); // curlHandle, headerHandle, bodyHandle, resolve, reject are PHP resources/callables; // stored as opaque PhpMixed::Null placeholders (real values live in Rust-side fields). - // TODO(phase-b): wire handle/closure storage properly. + // TODO(phase-c): storing the real \CurlHandle and the resolve/reject callables needs the + // Job to become a typed struct (an IndexMap<String, PhpMixed> cannot hold a non-Clone + // closure and the job is cloned at many sites). That refactor only pays off once the curl + // multi-exec loop actually runs — which depends on the curl_multi_* php-shims and the + // React\Promise Deferred (external package), both intentionally todo!(). job.insert("curlHandle".to_string(), PhpMixed::Null); job.insert( "filename".to_string(), @@ -659,7 +663,9 @@ impl CurlDownloader { job.insert("reject".to_string(), PhpMixed::Null); job.insert("primaryIp".to_string(), PhpMixed::String(String::new())); - let _ = (resolve, reject); // TODO(phase-b): store callables in Job + // TODO(phase-c): store the resolve/reject callables in the Job (see curlHandle note + // above) — blocked on the typed-Job refactor and the React\Promise model. + let _ = (resolve, reject); self.jobs.insert(curl_handle_id(&curl_handle), job); @@ -719,7 +725,10 @@ impl CurlDownloader { { let job = self.jobs.get(&id).cloned().unwrap_or_default(); // job['curlHandle'] is the actual \CurlHandle in PHP; in this port we keep - // handles in Rust-owned storage. TODO(phase-b): wire actual handle removal. + // handles in Rust-owned storage. + // TODO(phase-c): curl_multi_remove_handle($this->multiHandle, $job['curlHandle']) needs + // the real \CurlHandle stored in the Job (typed-Job refactor) and the curl_multi_* + // php-shims, which stay todo!(). // curl_multi_remove_handle($this->multiHandle, $job['curlHandle']); if PHP_VERSION_ID < 80000 { // curl_close($job['curlHandle']); @@ -776,7 +785,10 @@ impl CurlDownloader { .map(|b| (**b).clone()) .unwrap_or(PhpMixed::Null); let result_code: i64 = progress.get("result").and_then(|b| b.as_int()).unwrap_or(0); - // TODO(phase-b): correlate handle in `progress['handle']` to its job id. + // TODO(phase-c): the job id is `(int) $progress['handle']` — the integer id of the + // \CurlHandle reported by curl_multi_info_read. Recovering it needs real curl handle + // resources from the curl_multi_* php-shims (todo!()); until then the id cannot be + // correlated and this loop body is unreachable in practice. let i: i64 = 0; if !self.jobs.contains_key(&i) { continue; @@ -1357,10 +1369,12 @@ impl CurlDownloader { if let Some(PhpMixed::String(filename)) = job.get("filename") { rename(&format!("{}~", filename), filename); // job['resolve']($response); - // TODO(phase-b): invoke stored resolve callable + // TODO(phase-c): invoke the stored resolve callable — blocked on the typed-Job + // refactor that holds the React\Promise resolve closure (see download()). } else { // job['resolve']($response); - // TODO(phase-b): invoke stored resolve callable + // TODO(phase-c): invoke the stored resolve callable — blocked on the typed-Job + // refactor that holds the React\Promise resolve closure (see download()). } Ok(Ok(())) })(); @@ -1501,7 +1515,11 @@ impl CurlDownloader { .and_then(|v| v.as_array()) .and_then(|a| a.get("prevent_ip_access_callable")) .is_some(); - // TODO(phase-b): invoke prevent_ip_access_callable + // PHP: is_callable($cb = $job['options']['prevent_ip_access_callable']) && $cb($primaryIp) + // TODO(phase-c): prevent_ip_access_callable is a caller-supplied callable + // carried inside the options array as PhpMixed; invoking it needs a typed + // callable model (options would have to hold an Rc<dyn Fn>), which the + // PhpMixed-keyed options map cannot express yet. let blocked = prevent_ip_access_callable && false; if blocked { let job = self.jobs.get(&i).cloned().unwrap_or_default(); @@ -1794,8 +1812,9 @@ impl CurlDownloader { Some(PhpMixed::Array(a)) => a.iter().map(|(k, v)| (k.clone(), (**v).clone())).collect(), _ => IndexMap::new(), }; - // resolve/reject callables are stored Rust-side; pass placeholders for now. - // TODO(phase-b): forward stored callables here. + // PHP forwards the original job's resolve/reject callables into the restarted download. + // TODO(phase-c): those callables are not stored on the Job yet (typed-Job refactor, see + // download()), so no-op placeholders are forwarded instead of the real promise callbacks. let resolve: Box<dyn Fn(PhpMixed) + Send + Sync> = Box::new(|_| {}); let reject: Box<dyn Fn(PhpMixed) + Send + Sync> = Box::new(|_| {}); self.init_download( @@ -1897,7 +1916,8 @@ impl CurlDownloader { unlink_silent(&format!("{}~", filename)); } // job['reject']($e); - // TODO(phase-b): invoke stored reject callable + // TODO(phase-c): invoke the stored reject callable — blocked on the typed-Job refactor + // that holds the React\Promise reject closure (see download()). } fn check_curl_result(&self, code: i64) -> anyhow::Result<()> { diff --git a/crates/shirabe/src/util/process_executor.rs b/crates/shirabe/src/util/process_executor.rs index b62debe..75b40cb 100644 --- a/crates/shirabe/src/util/process_executor.rs +++ b/crates/shirabe/src/util/process_executor.rs @@ -261,14 +261,19 @@ impl ProcessExecutor { } } + // PHP: $callback = is_callable($output) ? $output : fn($type, $buffer) => $this->outputHandler($type, $buffer); let output_is_callable = output.as_deref().map(|o| is_callable(o)).unwrap_or(false); let _callback: Box<dyn Fn(&str, &str)> = if output_is_callable { - // TODO(phase-b): adapt output PhpMixed callable to closure + // TODO(phase-c): the user-supplied $output is a PhpMixed callable that cannot be + // invoked without a typed callable model (Rc<dyn Fn>); deferred with the callable model. Box::new(|_t: &str, _b: &str| {}) } else { - Box::new(|_t: &str, _b: &str| { - // TODO(phase-b): self.output_handler(t, b) — self is borrowed mutably elsewhere - }) + // TODO(phase-c): the fallback must call self.output_handler(type, buffer) (which is + // &mut self and writes to io / updates last_message), but this is a 'static + // `Box<dyn Fn>` that cannot borrow &mut self. Wiring it needs the handler state shared + // (Rc<RefCell<...>>). The callback is also not yet passed to process.run, whose Symfony + // Process backing stays todo!(). + Box::new(|_t: &str, _b: &str| {}) }; let io_for_signal = self.io.clone(); diff --git a/crates/shirabe/src/util/remote_filesystem.rs b/crates/shirabe/src/util/remote_filesystem.rs index dc6a2a4..ff6f066 100644 --- a/crates/shirabe/src/util/remote_filesystem.rs +++ b/crates/shirabe/src/util/remote_filesystem.rs @@ -307,7 +307,13 @@ impl RemoteFilesystem { let mut error_message = String::new(); let error_code = 0_i64; let mut result: Option<String> = None; - // TODO(phase-b): set_error_handler with a closure capturing `error_message` by reference. + // TODO(phase-c): faithfully accumulating the file_get_contents warnings into + // error_message requires PHP's set_error_handler runtime mechanism — a global handler the + // stream layer invokes per warning, appending into error_message by &-reference. + // set_error_handler / restore_error_handler are php-shim functions that must stay todo!(), + // and get_remote_contents does not surface low-level read warnings through such a side + // channel, so error_message stays empty here. Resolving this needs a thread-local + // error-handler stack wired into the I/O layer. set_error_handler(|_code, _msg, _file, _line| true); let mut http_response_header: Vec<String> = Vec::new(); @@ -611,7 +617,10 @@ impl RemoteFilesystem { } let put_error_message = String::new(); - // TODO(phase-b): set_error_handler closure that captures `put_error_message` by reference + // TODO(phase-c): capturing the file_put_contents warning into put_error_message + // requires PHP's set_error_handler runtime mechanism (see the get() method above); + // set_error_handler is a php-shim that must stay todo!() and file_put_contents does not + // surface its warning text here, so put_error_message stays empty. set_error_handler(|_code, _msg, _file, _line| true); let write_result = file_put_contents(file_name.as_deref().unwrap(), result_str.as_bytes()); diff --git a/crates/shirabe/src/util/svn.rs b/crates/shirabe/src/util/svn.rs index 7634b62..8453a43 100644 --- a/crates/shirabe/src/util/svn.rs +++ b/crates/shirabe/src/util/svn.rs @@ -140,7 +140,13 @@ impl Svn { let command = self.get_command(svn_command.clone(), url, path); let mut output: Option<String> = None; - // TODO(phase-b): handler captures &mut output and io by reference; restructure for Rust closures + // PHP: $handler = function ($type, $buffer) use (&$output, $io, $verbose) { ... }; + // $status = $this->process->execute($command, $handler, $cwd); + // TODO(phase-c): ProcessExecutor::execute does not yet accept a streaming output callback + // (its Symfony Process backing stays todo!()), so this handler — which filters by stream + // type, drops "Redirecting to URL" lines, accumulates into `output`, and echoes when + // verbose — cannot be passed through. The plain-buffer execute_args call below loses that + // filtering; resolving needs the process callback model (cf. process_executor.rs). let _io = &self.io; let _handler = |r#type: &str, buffer: &str| -> Option<()> { if r#type != "out" { @@ -156,7 +162,8 @@ impl Svn { } None }; - // TODO(phase-b): pass handler callback to process.execute + // TODO(phase-c): pass the filtering handler above to process.execute once the callback + // model lands; for now a plain buffer is used and the output filtering is skipped. let mut handler_output = String::new(); let status = self.process.borrow_mut().execute_args( &command, |
