aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates
diff options
context:
space:
mode:
Diffstat (limited to 'crates')
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/application.rs25
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/formatter/output_formatter.rs41
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/formatter/output_formatter_interface.rs5
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/helper/formatter_helper.rs8
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/helper/helper_interface.rs17
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/helper/helper_set.rs15
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/helper/mod.rs2
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/helper/question_helper.rs9
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/input/input_definition.rs6
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/output/console_output.rs11
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/output/output_interface.rs9
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/output/stream_output.rs9
-rw-r--r--crates/shirabe/src/command/base_command.rs49
-rw-r--r--crates/shirabe/src/command/base_dependency_command.rs17
-rw-r--r--crates/shirabe/src/command/clear_cache_command.rs22
-rw-r--r--crates/shirabe/src/command/create_project_command.rs22
-rw-r--r--crates/shirabe/src/command/global_command.rs6
-rw-r--r--crates/shirabe/src/command/init_command.rs28
-rw-r--r--crates/shirabe/src/command/outdated_command.rs6
-rw-r--r--crates/shirabe/src/command/reinstall_command.rs17
-rw-r--r--crates/shirabe/src/command/require_command.rs21
-rw-r--r--crates/shirabe/src/command/run_script_command.rs22
-rw-r--r--crates/shirabe/src/command/script_alias_command.rs7
-rw-r--r--crates/shirabe/src/command/show_command.rs11
-rw-r--r--crates/shirabe/src/console/application.rs165
-rw-r--r--crates/shirabe/src/dependency_resolver/multi_conflict_rule.rs6
-rw-r--r--crates/shirabe/src/dependency_resolver/rule.rs26
-rw-r--r--crates/shirabe/src/dependency_resolver/rule2_literals.rs6
-rw-r--r--crates/shirabe/src/dependency_resolver/rule_set_generator.rs48
-rw-r--r--crates/shirabe/src/downloader/path_downloader.rs6
-rw-r--r--crates/shirabe/src/event_dispatcher/event_dispatcher.rs7
-rw-r--r--crates/shirabe/src/factory.rs44
-rw-r--r--crates/shirabe/src/installer/installation_manager.rs31
-rw-r--r--crates/shirabe/src/installer/library_installer.rs3
-rw-r--r--crates/shirabe/src/installer/plugin_installer.rs9
-rw-r--r--crates/shirabe/src/io/base_io.rs3
-rw-r--r--crates/shirabe/src/io/buffer_io.rs15
-rw-r--r--crates/shirabe/src/io/console_io.rs78
-rw-r--r--crates/shirabe/src/io/io_interface.rs4
-rw-r--r--crates/shirabe/src/io/null_io.rs4
-rw-r--r--crates/shirabe/src/package/version/version_guesser.rs20
-rw-r--r--crates/shirabe/src/plugin/plugin_manager.rs13
-rw-r--r--crates/shirabe/src/question/strict_confirmation_question.rs4
-rw-r--r--crates/shirabe/src/repository/platform_repository.rs7
-rw-r--r--crates/shirabe/src/repository/vcs/git_bitbucket_driver.rs15
-rw-r--r--crates/shirabe/src/repository/vcs/vcs_driver.rs2
-rw-r--r--crates/shirabe/src/util/auth_helper.rs1
-rw-r--r--crates/shirabe/src/util/error_handler.rs39
-rw-r--r--crates/shirabe/src/util/http/curl_downloader.rs40
-rw-r--r--crates/shirabe/src/util/process_executor.rs13
-rw-r--r--crates/shirabe/src/util/remote_filesystem.rs13
-rw-r--r--crates/shirabe/src/util/svn.rs11
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,