aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-06-29 21:56:39 +0900
committernsfisis <nsfisis@gmail.com>2026-06-29 21:58:35 +0900
commit91ccd49cf68bca199e7d41fa65a4a3b0d1368127 (patch)
tree77206f45d192ba27f2d322d2cb1df89f0e4c6dc3 /crates
parentff90c6b259f597a17d73aaf5c3a437bfdda3e68e (diff)
downloadphp-shirabe-91ccd49cf68bca199e7d41fa65a4a3b0d1368127.tar.gz
php-shirabe-91ccd49cf68bca199e7d41fa65a4a3b0d1368127.tar.zst
php-shirabe-91ccd49cf68bca199e7d41fa65a4a3b0d1368127.zip
feat(php-shim): implement DirectoryIterator in fs shim
Give DirectoryIteratorEntry a backing path (mirroring RecursiveIteratorFileInfo) and make directory_iterator enumerate entries, returning Result to match PHP DirectoryIterator's UnexpectedValueException on an unopenable directory. Wire the new Result through DumpCompletionCommand's get_supported_shells. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates')
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/command/dump_completion_command.rs20
-rw-r--r--crates/shirabe-php-shim/Cargo.toml3
-rw-r--r--crates/shirabe-php-shim/src/fs.rs83
3 files changed, 84 insertions, 22 deletions
diff --git a/crates/shirabe-external-packages/src/symfony/console/command/dump_completion_command.rs b/crates/shirabe-external-packages/src/symfony/console/command/dump_completion_command.rs
index 588b868..8e2583e 100644
--- a/crates/shirabe-external-packages/src/symfony/console/command/dump_completion_command.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/command/dump_completion_command.rs
@@ -63,12 +63,12 @@ impl DumpCompletionCommand {
pub fn complete_impl(&self, input: &CompletionInput, suggestions: &mut CompletionSuggestions) {
if input.must_suggest_argument_values_for("shell") {
- suggestions.suggest_values(
- self.get_supported_shells()
- .into_iter()
- .map(StringOrSuggestion::String)
- .collect(),
- );
+ // TODO(phase-d): PHP's complete() lets a DirectoryIterator failure propagate, but the
+ // Command::complete trait is infallible; on error the suggestions are skipped instead.
+ if let Ok(shells) = self.get_supported_shells() {
+ suggestions
+ .suggest_values(shells.into_iter().map(StringOrSuggestion::String).collect());
+ }
}
}
@@ -98,14 +98,14 @@ impl DumpCompletionCommand {
todo!()
}
- fn get_supported_shells(&self) -> Vec<String> {
+ fn get_supported_shells(&self) -> anyhow::Result<Vec<String>> {
let mut shells = vec![];
// foreach (new \DirectoryIterator(__DIR__.'/../Resources/') as $file)
for file in shirabe_php_shim::directory_iterator(&format!(
"{}/../Resources/",
shirabe_php_shim::dir()
- )) {
+ ))? {
if shirabe_php_shim::str_starts_with(&file.get_basename(), "completion.")
&& file.is_file()
{
@@ -113,7 +113,7 @@ impl DumpCompletionCommand {
}
}
- shells
+ Ok(shells)
}
}
@@ -209,7 +209,7 @@ impl Command for DumpCompletionCommand {
shell
);
if !shirabe_php_shim::file_exists(&completion_file) {
- let supported_shells = self.get_supported_shells();
+ let supported_shells = self.get_supported_shells()?;
// TODO: PHP does `$output instanceof ConsoleOutputInterface ? $output->getErrorOutput()
// : $output`. There is no way to test trait membership through `&dyn OutputInterface`
diff --git a/crates/shirabe-php-shim/Cargo.toml b/crates/shirabe-php-shim/Cargo.toml
index 4226234..f8092ec 100644
--- a/crates/shirabe-php-shim/Cargo.toml
+++ b/crates/shirabe-php-shim/Cargo.toml
@@ -19,5 +19,8 @@ sha2.workspace = true
twox-hash.workspace = true
zip.workspace = true
+[dev-dependencies]
+tempfile.workspace = true
+
[lints]
workspace = true
diff --git a/crates/shirabe-php-shim/src/fs.rs b/crates/shirabe-php-shim/src/fs.rs
index bd882c2..ba6eef6 100644
--- a/crates/shirabe-php-shim/src/fs.rs
+++ b/crates/shirabe-php-shim/src/fs.rs
@@ -38,20 +38,29 @@ impl FilesystemIterator {
pub const CURRENT_AS_FILEINFO: i64 = 0;
}
-#[derive(Debug)]
-pub struct DirectoryIteratorEntry;
+#[derive(Debug, Clone)]
+pub struct DirectoryIteratorEntry {
+ path: std::path::PathBuf,
+}
+
impl DirectoryIteratorEntry {
- // TODO(phase-d): DirectoryIterator is a unit struct carrying no entry data; giving it real
- // behavior requires the same field redesign as RecursiveIteratorFileInfo. It has no callers, so
- // it is left unimplemented.
+ // SplFileInfo::getBasename(): the trailing name component.
pub fn get_basename(&self) -> String {
- todo!()
+ basename(&self.path.to_string_lossy())
}
+ // SplFileInfo::isFile() follows symlinks.
pub fn is_file(&self) -> bool {
- todo!()
+ std::fs::metadata(&self.path)
+ .map(|m| m.is_file())
+ .unwrap_or(false)
}
+ // SplFileInfo::getExtension(): substring after the basename's last '.'.
pub fn get_extension(&self) -> String {
- todo!()
+ let base = self.get_basename();
+ match base.rfind('.') {
+ Some(index) => base[index + 1..].to_string(),
+ None => String::new(),
+ }
}
}
@@ -232,10 +241,30 @@ fn rii_walk(
}
}
-pub fn directory_iterator(_path: &str) -> Vec<DirectoryIteratorEntry> {
- // TODO(phase-d): see DirectoryIteratorEntry; the entry type carries no data yet and there are no
- // callers.
- todo!()
+pub fn directory_iterator(
+ path: &str,
+) -> Result<Vec<DirectoryIteratorEntry>, UnexpectedValueException> {
+ let base = std::path::Path::new(path);
+ let rd = std::fs::read_dir(base).map_err(|_| UnexpectedValueException {
+ message: format!(
+ "DirectoryIterator::__construct({}): Failed to open directory",
+ path
+ ),
+ code: 0,
+ })?;
+ // PHP's DirectoryIterator yields the "." and ".." entries before the real ones.
+ let mut entries = vec![
+ DirectoryIteratorEntry {
+ path: base.join("."),
+ },
+ DirectoryIteratorEntry {
+ path: base.join(".."),
+ },
+ ];
+ for entry in rd.flatten() {
+ entries.push(DirectoryIteratorEntry { path: entry.path() });
+ }
+ Ok(entries)
}
/// PHP `fopen()`. Returns a stream resource, or an error mirroring PHP's `false`-on-failure
@@ -1428,4 +1457,34 @@ mod tests {
fn fopen_missing_file_is_err() {
assert!(fopen("/nonexistent/shirabe/test/path", "r").is_err());
}
+
+ #[test]
+ fn directory_iterator_lists_entries() {
+ let dir = tempfile::tempdir().unwrap();
+ std::fs::write(dir.path().join("completion.bash"), b"").unwrap();
+ std::fs::create_dir(dir.path().join("sub")).unwrap();
+
+ let entries = directory_iterator(dir.path().to_str().unwrap()).unwrap();
+ let basenames: Vec<String> = entries.iter().map(|e| e.get_basename()).collect();
+ // PHP's DirectoryIterator includes the "." and ".." entries.
+ assert!(basenames.contains(&".".to_string()));
+ assert!(basenames.contains(&"..".to_string()));
+ assert!(basenames.contains(&"completion.bash".to_string()));
+ assert!(basenames.contains(&"sub".to_string()));
+
+ let file = entries
+ .iter()
+ .find(|e| e.get_basename() == "completion.bash")
+ .unwrap();
+ assert!(file.is_file());
+ assert_eq!(file.get_extension(), "bash");
+
+ let sub = entries.iter().find(|e| e.get_basename() == "sub").unwrap();
+ assert!(!sub.is_file());
+ }
+
+ #[test]
+ fn directory_iterator_missing_dir_is_err() {
+ assert!(directory_iterator("/nonexistent/shirabe/test/dir").is_err());
+ }
}