diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-08-01 20:51:40 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-08-01 20:51:40 +0900 |
| commit | e5e6cc6807c9fbbef883ffe0eec9eed477d5bee3 (patch) | |
| tree | 8d5f1ff760e56b370c53ac0311c219ee7a7988c2 /crates/shirabe-external-packages/src | |
| parent | 06d2c5c884eee0b5b663730f4d379a7ceae3a8e4 (diff) | |
| download | php-shirabe-e5e6cc6807c9fbbef883ffe0eec9eed477d5bee3.tar.gz php-shirabe-e5e6cc6807c9fbbef883ffe0eec9eed477d5bee3.tar.zst php-shirabe-e5e6cc6807c9fbbef883ffe0eec9eed477d5bee3.zip | |
feat(symfony-console): implement interactive question/style helpers
Resolve the remaining todo!()s in SymfonyStyle, OutputStyle,
QuestionHelper and SymfonyQuestionHelper:
* Wire up the virtual dispatch PHP performs for the protected
writePrompt()/writeError() overrides, following the codebase's
established inheritance idiom (Command, ArchiveDownloader): the base
class becomes a trait (QuestionHelperInterface, named after the
QuestionInterface precedent) whose provided methods ask/do_ask/
validate_attempts carry the template logic and late-bind the
write_prompt/write_error hooks through Self, with inner()/inner_mut()
reaching the base-class state. SymfonyQuestionHelper overrides the
hooks as plain trait-impl methods, mirroring PHP's protected-method
overriding, so SymfonyStyle-driven questions now render the Symfony
Style Guide prompt.
* Type definition_list input as an enum (string|array|TableSeparator)
because PhpMixed intentionally cannot carry objects; the
InvalidArgumentException branch (a LogicException) becomes
unrepresentable. horizontal_table now takes typed Cells/Rows.
* Propagate the MissingInputException thrown inside autocomplete()
through a Result instead of aborting.
* Implement as_console_output_interface via Ref::filter_map on
ConsoleOutput, the interface's only implementor.
* Port progressIterate eagerly, following ProgressBar::iterate.
* Map __FILE__ to current_exe(): a native binary never runs from a
phar, so the hiddeninput.exe relocation branch correctly never fires.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe-external-packages/src')
4 files changed, 261 insertions, 200 deletions
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 f6ef31da..fd716aaa 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 @@ -35,13 +35,22 @@ static STTY: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new( /// self::$stdinIsInteractive static STDIN_IS_INTERACTIVE: std::sync::Mutex<Option<bool>> = std::sync::Mutex::new(None); -impl QuestionHelper { +/// PHP dispatches `$this->writePrompt()` / `$this->writeError()` virtually, and +/// `SymfonyQuestionHelper` overrides both protected methods. The embedded-super port expresses +/// that late binding as a trait: the template methods are provided here and reach the overridable +/// hooks through `Self`, while `inner()` reaches the base-class state. PHP has no such interface; +/// the invented name follows the `QuestionInterface` precedent. +pub trait QuestionHelperInterface { + fn inner(&self) -> &QuestionHelper; + + fn inner_mut(&mut self) -> &mut QuestionHelper; + /// Asks a question to the user. /// /// @return mixed The user answer /// /// @throws RuntimeException If there is no data to read in the input stream - pub fn ask( + fn ask( &mut self, input: &mut dyn InputInterface, output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, @@ -60,13 +69,13 @@ impl QuestionHelper { } if !input.is_interactive() { - return Ok(Ok(self.get_default_answer(question))); + return Ok(Ok(self.inner().get_default_answer(question))); } if let Some(streamable) = input.as_streamable() && let Some(stream) = streamable.get_stream() { - self.input_stream = Some(stream); + self.inner_mut().input_stream = Some(stream); } let result: anyhow::Result<Result<PhpMixed, MissingInputException>> = (|| { @@ -85,7 +94,7 @@ impl QuestionHelper { Err(exception) => { input.set_interactive(false); - let fallback_output = self.get_default_answer(question); + let fallback_output = self.inner().get_default_answer(question); if matches!(fallback_output, PhpMixed::Null) { return Ok(Err(exception)); } @@ -95,16 +104,7 @@ impl QuestionHelper { } } - pub fn get_name(&self) -> String { - "question".to_string() - } - - /// Prevents usage of stty. - pub fn disable_stty() { - STTY.store(false, std::sync::atomic::Ordering::SeqCst); - } - - /// Asks the question to the user. + /// Asks the question to the user (PHP private; on the trait so it can late-bind the hooks). /// /// @return mixed /// @@ -117,6 +117,7 @@ impl QuestionHelper { self.write_prompt(std::rc::Rc::clone(&output), question); let input_stream = self + .inner() .input_stream .clone() .unwrap_or_else(shirabe_php_shim::stdin); @@ -132,12 +133,15 @@ impl QuestionHelper { // The autocompleter callback yields an iterable (Option here); PHP // treats a null result as an empty list of suggestions. let callback = move |input: &str| callback(input).unwrap_or_default(); - let autocomplete = self.autocomplete( + let autocomplete = match self.inner().autocomplete( std::rc::Rc::clone(&output), question, &input_stream, &callback, - ); + ) { + Ok(value) => value, + Err(exception) => return Ok(Err(exception)), + }; ret = PhpMixed::String(if question.is_trimmable() { shirabe_php_shim::trim(&autocomplete, None) } else { @@ -146,7 +150,7 @@ impl QuestionHelper { } else { let mut r: PhpMixed = PhpMixed::Bool(false); if question.is_hidden() { - match self.get_hidden_response( + match self.inner().get_hidden_response( std::rc::Rc::clone(&output), &input_stream, question.is_trimmable(), @@ -176,7 +180,7 @@ impl QuestionHelper { shirabe_php_shim::stream_set_blocking(&input_stream, true); } - let read = self.read_input(&input_stream, question); + let read = self.inner().read_input(&input_stream, question); if !shirabe_php_shim::boolval(&is_blocked) { shirabe_php_shim::stream_set_blocking(&input_stream, false); @@ -221,6 +225,99 @@ impl QuestionHelper { Ok(Ok(ret)) } + /// Validates an attempt (PHP private; on the trait so it can late-bind the hooks). + /// + /// @param callable $interviewer A callable that will ask for a question and return the result + /// + /// @return mixed The validated response + /// + /// @throws \Exception In case the max number of attempts has been reached and no valid response has been given + fn validate_attempts( + &self, + interviewer: &dyn Fn() -> anyhow::Result<Result<PhpMixed, MissingInputException>>, + output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, + question: &impl QuestionInterface, + ) -> anyhow::Result<Result<PhpMixed, MissingInputException>> { + let mut error: Option<shirabe_php_shim::Exception> = None; + let mut attempts = question.get_max_attempts(); + + loop { + // while (null === $attempts || $attempts--) + match attempts { + None => {} + Some(0) => break, + Some(n) => attempts = Some(n - 1), + } + + if let Some(ref error) = error { + self.write_error(std::rc::Rc::clone(&output), error); + } + + let interviewed = match interviewer()? { + Ok(value) => value, + Err(missing) => return Ok(Err(missing)), + }; + + match question.get_validator().unwrap()(Some(interviewed)) { + Ok(value) => return Ok(Ok(value)), + Err(e) => { + // PHP: `catch (RuntimeException $e) { throw $e; } catch (\Exception $error) {}`. + // The validator return type is fixed to InvalidArgumentException here, so the + // RuntimeException rethrow branch is statically unreachable; record the error + // and retry. + error = Some(shirabe_php_shim::Exception { + message: e.0.message.clone(), + code: e.0.code, + }); + } + } + } + + // throw $error; + Err(anyhow::Error::msg( + error.map(|e| e.message).unwrap_or_default(), + )) + } + + /// Outputs the question prompt (PHP protected; the overridable hook). + fn write_prompt( + &self, + output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, + question: &impl QuestionInterface, + ) { + self.inner().write_prompt(output, question); + } + + /// Outputs an error message (PHP protected; the overridable hook). + fn write_error( + &self, + output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, + error: &shirabe_php_shim::Exception, + ) { + self.inner().write_error(output, error); + } +} + +impl QuestionHelperInterface for QuestionHelper { + fn inner(&self) -> &QuestionHelper { + self + } + + fn inner_mut(&mut self) -> &mut QuestionHelper { + self + } +} + +impl QuestionHelper { + pub fn get_name(&self) -> String { + "question".to_string() + } + + /// Prevents usage of stty. + pub fn disable_stty() { + STTY.store(false, std::sync::atomic::Ordering::SeqCst); + } + /// @return mixed fn get_default_answer(&self, question: &impl QuestionInterface) -> PhpMixed { let default = question.get_default(); @@ -344,7 +441,7 @@ impl QuestionHelper { question: &impl QuestionInterface, input_stream: &shirabe_php_shim::PhpResource, autocomplete: &dyn Fn(&str) -> Vec<PhpMixed>, - ) -> String { + ) -> Result<String, MissingInputException> { let cursor = Cursor::new(std::rc::Rc::clone(&output), Some(input_stream.clone())); let mut full_choice = String::new(); @@ -400,10 +497,12 @@ impl QuestionHelper { && matches!(question.get_default(), PhpMixed::Null)) { shirabe_php_shim::shell_exec(&format!("stty {}", stty_mode)); - // throw new MissingInputException('Aborted.'); - // autocomplete() returns string in PHP; this throw aborts the - // whole read. Faithful exception propagation is resolved later. - todo!("MissingInputException('Aborted.') thrown inside autocomplete"); + return Err(MissingInputException(RuntimeException( + shirabe_php_shim::RuntimeException { + message: "Aborted.".to_string(), + code: 0, + }, + ))); } else if c.as_deref() == Some("\u{7f}") { // Backspace Character if 0 == num_matches && 0 != i { @@ -570,7 +669,7 @@ impl QuestionHelper { // Reset stty so it behaves normally again shirabe_php_shim::shell_exec(&format!("stty {}", stty_mode)); - full_choice + Ok(full_choice) } fn most_recently_entered_value(&self, entered: &str) -> String { @@ -671,60 +770,6 @@ impl QuestionHelper { Ok(Ok(value)) } - /// Validates an attempt. - /// - /// @param callable $interviewer A callable that will ask for a question and return the result - /// - /// @return mixed The validated response - /// - /// @throws \Exception In case the max number of attempts has been reached and no valid response has been given - fn validate_attempts( - &self, - interviewer: &dyn Fn() -> anyhow::Result<Result<PhpMixed, MissingInputException>>, - output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - question: &impl QuestionInterface, - ) -> anyhow::Result<Result<PhpMixed, MissingInputException>> { - let mut error: Option<shirabe_php_shim::Exception> = None; - let mut attempts = question.get_max_attempts(); - - loop { - // while (null === $attempts || $attempts--) - match attempts { - None => {} - Some(0) => break, - Some(n) => attempts = Some(n - 1), - } - - if let Some(ref error) = error { - self.write_error(std::rc::Rc::clone(&output), error); - } - - let interviewed = match interviewer()? { - Ok(value) => value, - Err(missing) => return Ok(Err(missing)), - }; - - match question.get_validator().unwrap()(Some(interviewed)) { - Ok(value) => return Ok(Ok(value)), - Err(e) => { - // PHP: `catch (RuntimeException $e) { throw $e; } catch (\Exception $error) {}`. - // The validator return type is fixed to InvalidArgumentException here, so the - // RuntimeException rethrow branch is statically unreachable; record the error - // and retry. - error = Some(shirabe_php_shim::Exception { - message: e.0.message.clone(), - code: e.0.code, - }); - } - } - } - - // throw $error; - Err(anyhow::Error::msg( - error.map(|e| e.message).unwrap_or_default(), - )) - } - fn is_interactive_input(&self, input_stream: &shirabe_php_shim::PhpResource) -> bool { let uri = shirabe_php_shim::stream_get_meta_data(input_stream) .get("uri") @@ -871,10 +916,14 @@ impl QuestionHelper { } } -// PHP `__FILE__` magic constant. The shim's `file()` is PHP's file() function, -// not the magic constant, and there is no `__FILE__` shim yet (see report). +/// PHP `__FILE__` magic constant. The executing code lives in the Shirabe binary itself, which is +/// the closest analogue for a native executable; it never carries the `phar:` scheme, so the +/// phar-relocation branch in `get_hidden_response` correctly never triggers. fn magic_file() -> String { - todo!("magic_file: shim needs a __FILE__ magic-constant equivalent") + std::env::current_exe() + .expect("current executable path") + .display() + .to_string() } impl HelperInterface for QuestionHelper { diff --git a/crates/shirabe-external-packages/src/symfony/console/helper/symfony_question_helper.rs b/crates/shirabe-external-packages/src/symfony/console/helper/symfony_question_helper.rs index 086f9c1c..cae3434d 100644 --- a/crates/shirabe-external-packages/src/symfony/console/helper/symfony_question_helper.rs +++ b/crates/shirabe-external-packages/src/symfony/console/helper/symfony_question_helper.rs @@ -1,10 +1,11 @@ //! ref: composer/vendor/symfony/console/Helper/SymfonyQuestionHelper.php use crate::symfony::console::formatter::output_formatter::OutputFormatter; -use crate::symfony::console::helper::question_helper::QuestionHelper; +use crate::symfony::console::helper::question_helper::{QuestionHelper, QuestionHelperInterface}; use crate::symfony::console::output::output_interface; use crate::symfony::console::output::output_interface::OutputInterface; use crate::symfony::console::question::QuestionInterface; +use crate::symfony::console::style::style_interface::StyleInterface; use crate::symfony::console::style::symfony_style::SymfonyStyle; use shirabe_php_shim::PhpMixed; use std::ops::{Deref, DerefMut}; @@ -20,7 +21,26 @@ impl SymfonyQuestionHelper { Self::default() } - pub(crate) fn write_prompt( + fn get_eof_shortcut(&self) -> String { + if shirabe_php_shim::php_os_family() == "Windows" { + return "<comment>Ctrl+Z</comment> then <comment>Enter</comment>".to_string(); + } + + "<comment>Ctrl+D</comment>".to_string() + } +} + +impl QuestionHelperInterface for SymfonyQuestionHelper { + fn inner(&self) -> &QuestionHelper { + &self.inner + } + + fn inner_mut(&mut self) -> &mut QuestionHelper { + &mut self.inner + } + + /// {@inheritdoc} + fn write_prompt( &self, output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, question: &impl QuestionInterface, @@ -108,35 +128,23 @@ impl SymfonyQuestionHelper { .write(&[prompt], false, output_interface::OUTPUT_NORMAL); } - pub(crate) fn write_error( + /// {@inheritdoc} + fn write_error( &self, output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, error: &shirabe_php_shim::Exception, ) { - let is_symfony_style = { - let borrowed = output.borrow(); - (*borrowed) - .as_any() - .downcast_ref::<SymfonyStyle>() - .is_some() - }; - if is_symfony_style { - // $output->newLine(); $output->error($error->getMessage()); - // SymfonyStyle's newLine()/error() require mutable access to the - // concrete type; mutable downcasting through the trait object is - // resolved in a later phase. - todo!("SymfonyStyle newLine()/error() require &mut SymfonyStyle"); - } - - self.inner.write_error(output, error); - } + { + let mut borrowed = output.borrow_mut(); + if let Some(style) = (*borrowed).as_any_mut().downcast_mut::<SymfonyStyle>() { + style.new_line(1); + style.error(PhpMixed::String(error.message.clone())); - fn get_eof_shortcut(&self) -> String { - if shirabe_php_shim::php_os_family() == "Windows" { - return "<comment>Ctrl+Z</comment> then <comment>Enter</comment>".to_string(); + return; + } } - "<comment>Ctrl+D</comment>".to_string() + self.inner.write_error(output, error); } } diff --git a/crates/shirabe-external-packages/src/symfony/console/style/output_style.rs b/crates/shirabe-external-packages/src/symfony/console/style/output_style.rs index 7d68ceaf..8d28e76d 100644 --- a/crates/shirabe-external-packages/src/symfony/console/style/output_style.rs +++ b/crates/shirabe-external-packages/src/symfony/console/style/output_style.rs @@ -41,7 +41,6 @@ impl OutputStyle { Self::as_console_output_interface(&self.output) .unwrap() - .borrow() .get_error_output() } @@ -55,10 +54,18 @@ impl OutputStyle { .is_some() } + /// PHP casts to `ConsoleOutputInterface`; `ConsoleOutput` being its only implementor, a + /// borrow of the concrete type serves as the cast result. fn as_console_output_interface( - _output: &std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - ) -> Option<std::rc::Rc<std::cell::RefCell<dyn ConsoleOutputInterface>>> { - todo!() + output: &std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, + ) -> Option<std::cell::Ref<'_, crate::symfony::console::output::console_output::ConsoleOutput>> + { + std::cell::Ref::filter_map(output.borrow(), |output| { + output + .as_any() + .downcast_ref::<crate::symfony::console::output::console_output::ConsoleOutput>() + }) + .ok() } } diff --git a/crates/shirabe-external-packages/src/symfony/console/style/symfony_style.rs b/crates/shirabe-external-packages/src/symfony/console/style/symfony_style.rs index 69cd4191..0cc7cf25 100644 --- a/crates/shirabe-external-packages/src/symfony/console/style/symfony_style.rs +++ b/crates/shirabe-external-packages/src/symfony/console/style/symfony_style.rs @@ -1,7 +1,6 @@ //! ref: composer/vendor/symfony/console/Style/SymfonyStyle.php use crate::symfony::console::exception::invalid_argument_exception::InvalidArgumentException; -use crate::symfony::console::exception::runtime_exception::RuntimeException; use crate::symfony::console::formatter::OutputFormatter; use crate::symfony::console::formatter::OutputFormatterInterface; use crate::symfony::console::helper::Helper; @@ -9,6 +8,8 @@ use crate::symfony::console::helper::ProgressBar; use crate::symfony::console::helper::SymfonyQuestionHelper; use crate::symfony::console::helper::Table; use crate::symfony::console::helper::TableCell; +use crate::symfony::console::helper::TableSeparator; +use crate::symfony::console::helper::question_helper::QuestionHelperInterface; use crate::symfony::console::helper::table::{Cell, Row}; use crate::symfony::console::input::InputInterface; use crate::symfony::console::output::ConsoleOutputInterface; @@ -39,6 +40,16 @@ pub struct SymfonyStyle { pub const MAX_LINE_LENGTH: i64 = 120; +/// A `definition_list` entry. PHP types it as `string|array|TableSeparator`; any other type is +/// rejected with an `InvalidArgumentException` (a `LogicException`), which this enum makes +/// unrepresentable. +#[derive(Debug)] +pub enum DefinitionListItem { + String(String), + Array(indexmap::IndexMap<String, PhpMixed>), + TableSeparator(TableSeparator), +} + impl SymfonyStyle { pub fn new( input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, @@ -126,11 +137,11 @@ impl SymfonyStyle { } /// Formats a horizontal table. - pub fn horizontal_table(&mut self, headers: Vec<PhpMixed>, rows: Vec<PhpMixed>) { + pub fn horizontal_table(&mut self, headers: Vec<Cell>, rows: Vec<Row>) { self.create_table() .set_horizontal(true) - .set_headers(headers.into_iter().map(Cell::from).collect()) - .set_rows(rows.into_iter().map(Row::from).collect()) + .set_headers(headers) + .set_rows(rows) .render(); self.new_line(1); @@ -142,76 +153,66 @@ impl SymfonyStyle { /// * 'A title' /// * ['key' => 'value'] /// * new TableSeparator() - pub fn definition_list(&mut self, list: Vec<PhpMixed>) { - let mut headers: Vec<PhpMixed> = Vec::new(); - let mut row: Vec<PhpMixed> = Vec::new(); + pub fn definition_list(&mut self, list: Vec<DefinitionListItem>) { + let mut headers: Vec<Cell> = Vec::new(); + let mut row: Vec<Cell> = Vec::new(); for value in list { - if Self::is_table_separator(&value) { - headers.push(value.clone()); - row.push(value); - continue; - } - if shirabe_php_shim::is_string(&value) { - // TODO: store a `TableCell` (with colspan => 2) into the mixed array. - let _table_cell = TableCell::new(&Self::php_string(&value), { - let mut options = indexmap::IndexMap::new(); - options.insert( - "colspan".to_string(), - crate::symfony::console::helper::table_cell::TableCellOption::Int(2), - ); - options - }); - let _ = _table_cell; - todo!(); - } - if !shirabe_php_shim::is_array(&value) { - // TODO(plugin): recoverable error path. - let _ = InvalidArgumentException(shirabe_php_shim::InvalidArgumentException { - message: "Value should be an array, string, or an instance of TableSeparator." - .to_string(), - code: 0, - }); - todo!() - } - // $headers[] = key($value); $row[] = current($value); - let (first_key, first_value) = match &value { - PhpMixed::Array(entries) => ( - entries + match value { + DefinitionListItem::TableSeparator(separator) => { + headers.push(Cell::Separator(separator.clone())); + row.push(Cell::Separator(separator)); + } + DefinitionListItem::String(value) => { + headers.push(Cell::Cell( + TableCell::new(&value, { + let mut options = indexmap::IndexMap::new(); + options.insert( + "colspan".to_string(), + crate::symfony::console::helper::table_cell::TableCellOption::Int( + 2, + ), + ); + options + }) + .expect("colspan is a valid TableCell option"), + )); + row.push(Cell::Null); + } + DefinitionListItem::Array(value) => { + // $headers[] = key($value); $row[] = current($value); + let first_key = value .keys() .next() .map(|k| PhpMixed::String(k.clone())) - .unwrap_or(PhpMixed::Null), - entries + .unwrap_or(PhpMixed::Null); + let first_value = value .values() .next() .cloned() - .unwrap_or(PhpMixed::Bool(false)), - ), - PhpMixed::List(items) => ( - if items.is_empty() { - PhpMixed::Null - } else { - PhpMixed::Int(0) - }, - items.first().cloned().unwrap_or(PhpMixed::Bool(false)), - ), - _ => unreachable!("value is an array past the is_array guard"), - }; - headers.push(first_key); - row.push(first_value); + .unwrap_or(PhpMixed::Bool(false)); + headers.push(Cell::from(first_key)); + row.push(Cell::from(first_value)); + } + } } - self.horizontal_table(headers, vec![PhpMixed::List(row.into_iter().collect())]); + self.horizontal_table(headers, vec![Row::Cells(row)]); } + /// @see ProgressBar::iterate() + /// + /// PHP returns a generator (`yield from`); this port evaluates eagerly, following + /// `ProgressBar::iterate`. pub fn progress_iterate( &mut self, - _iterable: Vec<PhpMixed>, - _max: Option<i64>, - ) -> Vec<PhpMixed> { - // TODO(phase-c/d): PHP uses `yield from`; porting the generator semantics of - // ProgressBar::iterate() requires a streaming design not yet in place. - todo!() + iterable: Vec<(PhpMixed, PhpMixed)>, + max: Option<i64>, + ) -> anyhow::Result<Vec<(PhpMixed, PhpMixed)>> { + let yielded = self.create_progress_bar(0).iterate(iterable, max)?; + + self.new_line(2); + + Ok(yielded) } pub fn ask_question(&mut self, question: &impl QuestionInterface) -> PhpMixed { @@ -223,7 +224,9 @@ impl SymfonyStyle { self.question_helper = Some(SymfonyQuestionHelper::new()); } - // TODO(plugin): pass `self` as the OutputInterface to the question helper. + // TODO(phase-c): PHP passes `$this` as the OutputInterface, so SymfonyQuestionHelper's + // write_error renders through SymfonyStyle::error; SymfonyStyle is not an OutputInterface + // trait object here, so the raw output is passed instead. let answer = { let input = self.input.clone(); let mut input = input.borrow_mut(); @@ -253,15 +256,14 @@ impl SymfonyStyle { } pub fn create_table(&mut self) -> Table { - // TODO(plugin): ConsoleOutputInterface::section() requires runtime type info. - let output = if Self::is_console_output_interface(&self.output) { - Self::as_console_output_interface(&self.output) - .unwrap() - .borrow() - .section() as std::rc::Rc<std::cell::RefCell<dyn OutputInterface>> - } else { - self.output.clone() - }; + let output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>> = + if Self::is_console_output_interface(&self.output) { + Self::as_console_output_interface(&self.output) + .unwrap() + .section() + } else { + self.output.clone() + }; let mut style = Table::get_style_definition("symfony-style-guide".to_string()) .expect("style definition lookup") .expect("undefined style definition"); @@ -290,16 +292,12 @@ impl SymfonyStyle { } fn get_progress_bar(&mut self) -> &mut ProgressBar { - if self.progress_bar.is_none() { - // TODO(plugin): recoverable error path. - let _ = RuntimeException(shirabe_php_shim::RuntimeException { - message: "The ProgressBar is not started.".to_string(), - code: 0, - }); - todo!() - } - - self.progress_bar.as_mut().unwrap() + // PHP throws RuntimeException('The ProgressBar is not started.'). Reaching this without a + // prior progress_start() call is a caller bug, and the StyleInterface signatures carry no + // Result, so panic. + self.progress_bar + .as_mut() + .expect("The ProgressBar is not started.") } fn auto_prepend_block(&mut self) { @@ -447,16 +445,15 @@ impl SymfonyStyle { .is_some() } + /// PHP casts to `ConsoleOutputInterface`; `ConsoleOutput` being its only implementor, a + /// borrow of the concrete type serves as the cast result. fn as_console_output_interface( - _output: &std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - ) -> Option<std::rc::Rc<std::cell::RefCell<dyn ConsoleOutputInterface>>> { - todo!() - } - - // TODO(phase-c/d): `PhpMixed` cannot carry a `TableSeparator` object, so the - // `$value instanceof TableSeparator` check has no faithful representation yet. - fn is_table_separator(_value: &PhpMixed) -> bool { - todo!() + output: &std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, + ) -> Option<std::cell::Ref<'_, ConsoleOutput>> { + std::cell::Ref::filter_map(output.borrow(), |output| { + output.as_any().downcast_ref::<ConsoleOutput>() + }) + .ok() } fn php_string(value: &PhpMixed) -> String { |
