diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-06-08 04:22:49 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-06-08 04:23:11 +0900 |
| commit | 632c9793927a30c4bca3c91888e66b369d732dfe (patch) | |
| tree | 5bafb9d7fa2523d5a2f8d70ff59c3173e962e389 | |
| parent | 243450fee9853c2fea66ae0642dabb8db5227d6f (diff) | |
| download | php-shirabe-632c9793927a30c4bca3c91888e66b369d732dfe.tar.gz php-shirabe-632c9793927a30c4bca3c91888e66b369d732dfe.tar.zst php-shirabe-632c9793927a30c4bca3c91888e66b369d732dfe.zip | |
feat(phase-c): resolve PhpMixed-conversion phase-b TODOs
Implement the foundational PhpMixed conversion infrastructure
(From<bool|i64|f64|String>, order-sensitive PartialEq matching PHP ===)
and resolve the category-G phase-b TODOs that depend on it:
- Fix VCS driver cache paths that discarded parsed JSON or diverged on
null caches (svn/forgejo/gitlab/git-bitbucket/github).
- Wire up real conversions previously stubbed or dropped: suggests
platform config, audit ignore-severities, composer_repository search
and ProviderInfo, class_loader prefix/classmap merges, locker lock
diff comparison, advisory JSON serialization, SPDX license fields.
- Make GenericRule take a typed ReasonData; populate RULE_ROOT_REQUIRE
with the constraint and convert PhpMixed at the call sites.
28 files changed, 208 insertions, 223 deletions
diff --git a/crates/shirabe-php-shim/src/lib.rs b/crates/shirabe-php-shim/src/lib.rs index 6acdd0e..ad2736b 100644 --- a/crates/shirabe-php-shim/src/lib.rs +++ b/crates/shirabe-php-shim/src/lib.rs @@ -44,6 +44,39 @@ impl serde::Serialize for PhpMixed { } } +/// PHP `===` semantics: type-strict and, for arrays, order-sensitive. +impl PartialEq for PhpMixed { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (PhpMixed::Null, PhpMixed::Null) => true, + (PhpMixed::Bool(a), PhpMixed::Bool(b)) => a == b, + (PhpMixed::Int(a), PhpMixed::Int(b)) => a == b, + (PhpMixed::Float(a), PhpMixed::Float(b)) => a == b, + (PhpMixed::String(a), PhpMixed::String(b)) => a == b, + (PhpMixed::List(a), PhpMixed::List(b)) => a == b, + (PhpMixed::Array(a), PhpMixed::Array(b)) => { + a.len() == b.len() + && a.iter() + .zip(b.iter()) + .all(|((ka, va), (kb, vb))| ka == kb && va == vb) + } + (PhpMixed::Object(a), PhpMixed::Object(b)) => a == b, + _ => false, + } + } +} + +impl PartialEq for ArrayObject { + fn eq(&self, other: &Self) -> bool { + self.data.len() == other.data.len() + && self + .data + .iter() + .zip(other.data.iter()) + .all(|((ka, va), (kb, vb))| ka == kb && va == vb) + } +} + impl serde::Serialize for ArrayObject { fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where @@ -175,26 +208,26 @@ impl PhpMixed { } impl From<bool> for PhpMixed { - fn from(_value: bool) -> Self { - todo!() + fn from(value: bool) -> Self { + PhpMixed::Bool(value) } } impl From<i64> for PhpMixed { - fn from(_value: i64) -> Self { - todo!() + fn from(value: i64) -> Self { + PhpMixed::Int(value) } } impl From<f64> for PhpMixed { - fn from(_value: f64) -> Self { - todo!() + fn from(value: f64) -> Self { + PhpMixed::Float(value) } } impl From<String> for PhpMixed { - fn from(_value: String) -> Self { - todo!() + fn from(value: String) -> Self { + PhpMixed::String(value) } } diff --git a/crates/shirabe/src/advisory/any_security_advisory.rs b/crates/shirabe/src/advisory/any_security_advisory.rs index c48df33..b6df764 100644 --- a/crates/shirabe/src/advisory/any_security_advisory.rs +++ b/crates/shirabe/src/advisory/any_security_advisory.rs @@ -3,7 +3,8 @@ use crate::advisory::PartialSecurityAdvisory; use crate::advisory::SecurityAdvisory; use shirabe_semver::constraint::AnyConstraint; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, serde::Serialize)] +#[serde(untagged)] pub enum AnySecurityAdvisory { Partial(PartialSecurityAdvisory), Full(SecurityAdvisory), diff --git a/crates/shirabe/src/advisory/auditor.rs b/crates/shirabe/src/advisory/auditor.rs index 2fc93f3..179278a 100644 --- a/crates/shirabe/src/advisory/auditor.rs +++ b/crates/shirabe/src/advisory/auditor.rs @@ -22,6 +22,20 @@ use crate::package::base_package::BasePackage; use crate::repository::RepositorySet; use crate::util::PackageInfo; +/// Shape of the `--format=json` audit output. +#[derive(serde::Serialize)] +struct AuditJsonReport<'a> { + advisories: &'a IndexMap<String, Vec<AnySecurityAdvisory>>, + #[serde(rename = "ignored-advisories", skip_serializing_if = "Option::is_none")] + ignored_advisories: Option<&'a IndexMap<String, Vec<AnySecurityAdvisory>>>, + #[serde( + rename = "unreachable-repositories", + skip_serializing_if = "Option::is_none" + )] + unreachable_repositories: Option<&'a Vec<String>>, + abandoned: IndexMap<String, Option<String>>, +} + /// @internal #[derive(Debug)] pub struct Auditor; @@ -122,24 +136,7 @@ impl Auditor { self.calculate_bitmask(0 < affected_packages_count, 0 < abandoned_count); if Self::FORMAT_JSON == format { - let mut json: IndexMap<String, PhpMixed> = IndexMap::new(); - // TODO(phase-b): serialize advisories / ignored_advisories into PhpMixed - json.insert("advisories".to_string(), PhpMixed::Null); - if !ignored_advisories.is_empty() { - json.insert("ignored-advisories".to_string(), PhpMixed::Null); - } - if !unreachable_repos.is_empty() { - json.insert( - "unreachable-repositories".to_string(), - PhpMixed::List( - unreachable_repos - .iter() - .map(|r| Box::new(PhpMixed::String(r.clone()))) - .collect(), - ), - ); - } - let abandoned_map = array_reduce( + let abandoned = array_reduce( &abandoned_packages, |mut carry: IndexMap<String, Option<String>>, package: &CompletePackageInterfaceHandle| { @@ -148,27 +145,22 @@ impl Auditor { }, IndexMap::new(), ); - json.insert( - "abandoned".to_string(), - PhpMixed::Array( - abandoned_map - .into_iter() - .map(|(k, v)| { - ( - k, - Box::new(match v { - Some(s) => PhpMixed::String(s), - None => PhpMixed::Null, - }), - ) - }) - .collect(), - ), - ); + let report = AuditJsonReport { + advisories: &advisories, + ignored_advisories: if ignored_advisories.is_empty() { + None + } else { + Some(&ignored_advisories) + }, + unreachable_repositories: if unreachable_repos.is_empty() { + None + } else { + Some(&unreachable_repos) + }, + abandoned, + }; - io.write(&JsonFile::encode(&PhpMixed::Array( - json.into_iter().map(|(k, v)| (k, Box::new(v))).collect(), - ))); + io.write(&JsonFile::encode(&report)); return Ok(audit_bitmask); } diff --git a/crates/shirabe/src/advisory/security_advisory.rs b/crates/shirabe/src/advisory/security_advisory.rs index 5cb5c59..2c5a309 100644 --- a/crates/shirabe/src/advisory/security_advisory.rs +++ b/crates/shirabe/src/advisory/security_advisory.rs @@ -7,6 +7,14 @@ use shirabe_semver::constraint::AnyConstraint; use crate::advisory::IgnoredSecurityAdvisory; use crate::advisory::PartialSecurityAdvisory; +/// Matches PHP's `format(DATE_RFC3339)`, e.g. "2020-01-01T00:00:00+00:00". +fn serialize_date_rfc3339<S: serde::Serializer>( + dt: &DateTime<Utc>, + serializer: S, +) -> Result<S::Ok, S::Error> { + serializer.serialize_str(&dt.format("%Y-%m-%dT%H:%M:%S%:z").to_string()) +} + #[derive(Debug, Clone, serde::Serialize)] #[serde(rename_all = "camelCase")] pub struct SecurityAdvisory { @@ -15,6 +23,7 @@ pub struct SecurityAdvisory { pub title: String, pub cve: Option<String>, pub link: Option<String>, + #[serde(serialize_with = "serialize_date_rfc3339")] pub reported_at: DateTime<Utc>, pub sources: Vec<IndexMap<String, String>>, pub severity: Option<String>, diff --git a/crates/shirabe/src/autoload/class_loader.rs b/crates/shirabe/src/autoload/class_loader.rs index 99ee5c2..745e9f2 100644 --- a/crates/shirabe/src/autoload/class_loader.rs +++ b/crates/shirabe/src/autoload/class_loader.rs @@ -79,37 +79,16 @@ impl ClassLoader { pub fn get_prefixes(&self) -> IndexMap<String, Vec<String>> { if !self.prefixes_psr0.is_empty() { // PHP: call_user_func_array('array_merge', array_values($this->prefixesPsr0)) - let prefixes_as_mixed: IndexMap<String, PhpMixed> = self - .prefixes_psr0 - .iter() - .map(|(k, v)| { - ( - k.clone(), - PhpMixed::Array( - v.iter() - .map(|(k2, v2)| { - ( - k2.clone(), - Box::new(PhpMixed::List( - v2.iter() - .map(|s| Box::new(PhpMixed::String(s.clone()))) - .collect(), - )), - ) - }) - .collect(), - ), - ) - }) - .collect(); - let arrays = array_values(&prefixes_as_mixed); - let result = call_user_func_array( - "array_merge", - &PhpMixed::List(arrays.into_iter().map(Box::new).collect()), - ); - // TODO(phase-b): cast result back to IndexMap<String, Vec<String>> - let _ = result; - return IndexMap::new(); + // The per-first-char maps are flattened into one prefix => dirs map. array_merge with + // string keys keeps the first position and lets the later value win, which is exactly + // IndexMap::insert. + let mut result: IndexMap<String, Vec<String>> = IndexMap::new(); + for inner in self.prefixes_psr0.values() { + for (prefix, dirs) in inner { + result.insert(prefix.clone(), dirs.clone()); + } + } + return result; } IndexMap::new() @@ -139,22 +118,8 @@ impl ClassLoader { pub fn add_class_map(&mut self, class_map: IndexMap<String, String>) { if !self.class_map.is_empty() { // PHP: $this->classMap = array_merge($this->classMap, $classMap); - let merged = array_merge( - PhpMixed::Array( - self.class_map - .iter() - .map(|(k, v)| (k.clone(), Box::new(PhpMixed::String(v.clone())))) - .collect(), - ), - PhpMixed::Array( - class_map - .iter() - .map(|(k, v)| (k.clone(), Box::new(PhpMixed::String(v.clone())))) - .collect(), - ), - ); - // TODO(phase-b): cast merged back to IndexMap<String, String> - let _ = merged; + // array_merge keeps existing string keys in place and lets $classMap overwrite their + // values while appending new ones, which is exactly IndexMap::extend. self.class_map.extend(class_map); } else { self.class_map = class_map; diff --git a/crates/shirabe/src/command/audit_command.rs b/crates/shirabe/src/command/audit_command.rs index 2d25052..20532f1 100644 --- a/crates/shirabe/src/command/audit_command.rs +++ b/crates/shirabe/src/command/audit_command.rs @@ -113,10 +113,19 @@ impl AuditCommand { let abandoned = abandoned.unwrap_or_else(|| audit_config.audit_abandoned.clone()); - let ignore_severities = array_merge( - array_fill_keys(input.borrow().get_option("ignore-severity"), PhpMixed::Null), - PhpMixed::from(audit_config.ignore_severity_for_audit.clone()), - ); + let mut ignore_severities: indexmap::IndexMap<String, Option<String>> = + indexmap::IndexMap::new(); + let cli_severities = input.borrow().get_option("ignore-severity"); + if let Some(list) = cli_severities.as_list() { + for sev in list { + if let Some(s) = sev.as_string() { + ignore_severities.insert(s.to_string(), None); + } + } + } + for (k, v) in audit_config.ignore_severity_for_audit.clone() { + ignore_severities.insert(k, v); + } let ignore_unreachable = input .borrow() .get_option("ignore-unreachable") @@ -125,8 +134,6 @@ impl AuditCommand { || audit_config.ignore_unreachable; let audit_format = self.get_audit_format(input, "format")?; - // TODO(phase-b): ignore_severities is PhpMixed; need conversion to IndexMap<String, Option<String>> - let _ = ignore_severities; Ok(auditor .audit( &mut *self.get_io().borrow_mut(), @@ -136,7 +143,7 @@ impl AuditCommand { false, audit_config.ignore_list_for_audit.clone(), &abandoned, - indexmap::IndexMap::new(), + ignore_severities, ignore_unreachable, audit_config.ignore_abandoned_for_audit.clone(), )? diff --git a/crates/shirabe/src/command/base_dependency_command.rs b/crates/shirabe/src/command/base_dependency_command.rs index 1f4d5d4..9317b31 100644 --- a/crates/shirabe/src/command/base_dependency_command.rs +++ b/crates/shirabe/src/command/base_dependency_command.rs @@ -379,8 +379,6 @@ pub trait BaseDependencyCommand: BaseCommand { new_table.extend(table); table = new_table; } - // TODO(phase-b): render_table expects Vec<PhpMixed>; build PhpMixed cells once a - // converter exists for Vec<String> rows. let table_as_mixed: Vec<PhpMixed> = table .into_iter() .map(|row| { diff --git a/crates/shirabe/src/command/init_command.rs b/crates/shirabe/src/command/init_command.rs index f146b30..8e200f0 100644 --- a/crates/shirabe/src/command/init_command.rs +++ b/crates/shirabe/src/command/init_command.rs @@ -134,7 +134,6 @@ impl InitCommand { "license".to_string(), "autoload".to_string(), ]; - // TODO(phase-b): adapt PhpMixed<->Box<PhpMixed> for array_filter_map let filtered_input: IndexMap<String, Box<PhpMixed>> = array_intersect_key( &input.borrow().get_options(), &array_flip_strings(&allowlist), diff --git a/crates/shirabe/src/command/search_command.rs b/crates/shirabe/src/command/search_command.rs index 1cd8e59..b9eacf5 100644 --- a/crates/shirabe/src/command/search_command.rs +++ b/crates/shirabe/src/command/search_command.rs @@ -199,7 +199,10 @@ impl SearchCommand { } } } else if format == "json" { - // TODO(phase-b): JsonFile::encode takes &PhpMixed; convert Vec<SearchResult> into PhpMixed + // TODO(phase-c): faithful JSON output requires SearchResult to retain the raw result + // array. PHP's fulltext search passes through arbitrary API fields (downloads, favers, + // repository, ...) which the typed SearchResult (name/description/abandoned/url) drops, + // so encoding it here would diverge from Composer's output. let _ = &results; io.write(&JsonFile::encode(&PhpMixed::Null)); } diff --git a/crates/shirabe/src/command/show_command.rs b/crates/shirabe/src/command/show_command.rs index 7a2c452..e99005e 100644 --- a/crates/shirabe/src/command/show_command.rs +++ b/crates/shirabe/src/command/show_command.rs @@ -1870,12 +1870,20 @@ impl ShowCommand { let out = match license { None => license_id.clone(), Some(license) => { - // TODO(phase-b): SpdxLicenses returns PhpMixed; field access (osi/fullname/url) - // is placeholder until PHP array offsets are wired. - let _ = &license; - let fullname = String::new(); - let url = String::new(); - let is_osi = false; + // SpdxLicenses::getLicenseByIdentifier returns [0 => fullname, 1 => osiApproved, 2 => url]. + let list = license.as_list(); + let fullname = list + .and_then(|l| l.get(0)) + .and_then(|v| v.as_string()) + .unwrap_or("") + .to_string(); + let is_osi = + list.and_then(|l| l.get(1)).and_then(|v| v.as_bool()) == Some(true); + let url = list + .and_then(|l| l.get(2)) + .and_then(|v| v.as_string()) + .unwrap_or("") + .to_string(); if is_osi { format!("{} ({}) (OSI approved) {}", fullname, license_id, url) } else { @@ -2116,12 +2124,23 @@ impl ShowCommand { match license { None => PhpMixed::String(license_id), Some(l) => { - // TODO(phase-b): SpdxLicenses returns PhpMixed; field access placeholder. - let _ = &l; + // PHP shape: ['name' => $license[0], 'osi' => $licenseId, 'url' => $license[2]]. + // Note 'osi' is the license id string, not the OSI-approved flag. + let list = l.as_list(); + let name = list + .and_then(|x| x.get(0)) + .and_then(|v| v.as_string()) + .unwrap_or("") + .to_string(); + let url = list + .and_then(|x| x.get(2)) + .and_then(|v| v.as_string()) + .unwrap_or("") + .to_string(); let mut m: IndexMap<String, PhpMixed> = IndexMap::new(); - m.insert("name".to_string(), PhpMixed::String(String::new())); + m.insert("name".to_string(), PhpMixed::String(name)); m.insert("osi".to_string(), PhpMixed::String(license_id)); - m.insert("url".to_string(), PhpMixed::String(String::new())); + m.insert("url".to_string(), PhpMixed::String(url)); PhpMixed::Array(m.into_iter().map(|(k, v)| (k, Box::new(v))).collect()) } } diff --git a/crates/shirabe/src/command/suggests_command.rs b/crates/shirabe/src/command/suggests_command.rs index 5c021b2..b93f7ec 100644 --- a/crates/shirabe/src/command/suggests_command.rs +++ b/crates/shirabe/src/command/suggests_command.rs @@ -75,10 +75,11 @@ impl SuggestsCommand { )?; installed_repos.push(locked_repo.into()); } else { - // TODO(phase-b): Config::get returns PhpMixed; need to coerce to IndexMap<String, PhpMixed> - let _platform_cfg = composer.get_config().borrow().get("platform"); - let platform_overrides: IndexMap<String, PhpMixed> = - todo!("extract IndexMap<String, PhpMixed> from PhpMixed config value"); + let platform_cfg = composer.get_config().borrow().get("platform"); + let platform_overrides: IndexMap<String, PhpMixed> = platform_cfg + .as_array() + .map(|m| m.iter().map(|(k, v)| (k.clone(), (**v).clone())).collect()) + .unwrap_or_default(); installed_repos.push(RepositoryInterfaceHandle::new(PlatformRepository::new( vec![], platform_overrides, diff --git a/crates/shirabe/src/command/update_command.rs b/crates/shirabe/src/command/update_command.rs index f0dc830..4fe9b6a 100644 --- a/crates/shirabe/src/command/update_command.rs +++ b/crates/shirabe/src/command/update_command.rs @@ -655,9 +655,6 @@ impl UpdateCommand { .into()); } - // TODO(phase-b): IOInterface::select returns PhpMixed and takes - // Vec<String> choices; convert IndexMap<String, String> autocompleter values - // to choices and downcast PhpMixed back to Vec<String>. let select_result = io.select( "Select packages: (Select more than one value separated by comma) ".to_string(), autocompleter_values diff --git a/crates/shirabe/src/config.rs b/crates/shirabe/src/config.rs index d6bee9e..e036625 100644 --- a/crates/shirabe/src/config.rs +++ b/crates/shirabe/src/config.rs @@ -583,7 +583,7 @@ impl Config { self.get_with_flags(key, 0).unwrap_or(PhpMixed::Null) } - // TODO(phase-b): typed convenience; PHP's Config::get() returns mixed. + /// Typed convenience accessor for keys whose `get()` value is always a string. pub fn get_str(&self, key: &str) -> Result<String> { Ok(self .get_with_flags(key, 0)? diff --git a/crates/shirabe/src/dependency_resolver/generic_rule.rs b/crates/shirabe/src/dependency_resolver/generic_rule.rs index eada1c3..9098e93 100644 --- a/crates/shirabe/src/dependency_resolver/generic_rule.rs +++ b/crates/shirabe/src/dependency_resolver/generic_rule.rs @@ -2,7 +2,7 @@ use crate::dependency_resolver::{Rule, RuleBase}; use anyhow::Result; -use shirabe_php_shim::{PHP_VERSION_ID, PhpMixed, RuntimeException, hash_raw, unpack}; +use shirabe_php_shim::{PHP_VERSION_ID, RuntimeException, hash_raw, unpack}; use super::rule::ReasonData; @@ -13,8 +13,8 @@ pub struct GenericRule { } impl GenericRule { - pub fn new(mut literals: Vec<i64>, reason: PhpMixed, reason_data: PhpMixed) -> Self { - let inner = RuleBase::new(reason.as_int().unwrap_or(0), ReasonData::from(reason_data)); + pub fn new(mut literals: Vec<i64>, reason: i64, reason_data: ReasonData) -> Self { + let inner = RuleBase::new(reason, reason_data); literals.sort(); Self { inner, literals } } diff --git a/crates/shirabe/src/dependency_resolver/rule_set_generator.rs b/crates/shirabe/src/dependency_resolver/rule_set_generator.rs index 048a5f5..295160c 100644 --- a/crates/shirabe/src/dependency_resolver/rule_set_generator.rs +++ b/crates/shirabe/src/dependency_resolver/rule_set_generator.rs @@ -67,8 +67,8 @@ impl RuleSetGenerator { Some(GenericRule::new( literals, - PhpMixed::Int(reason), - reason_data, + reason, + rule::ReasonData::from(reason_data), )) } @@ -83,7 +83,7 @@ impl RuleSetGenerator { reason_data: PhpMixed, ) -> GenericRule { let literals: Vec<i64> = packages.iter().map(|p| p.get_id()).collect(); - GenericRule::new(literals, PhpMixed::Int(reason), reason_data) + GenericRule::new(literals, reason, rule::ReasonData::from(reason_data)) } /// Creates a rule for two conflicting packages. @@ -368,19 +368,14 @@ impl RuleSetGenerator { self.add_rules_for_package(package.clone(), platform_requirement_filter); } - let mut reason_data: IndexMap<String, Box<PhpMixed>> = IndexMap::new(); - reason_data.insert( - "packageName".to_string(), - Box::new(PhpMixed::String(package_name.clone())), - ); - reason_data.insert( - "constraint".to_string(), - Box::new(PhpMixed::Null), // reasonData: $constraint (ConstraintInterface) - ); - let rule = self.create_install_one_of_rule( - &packages, + let literals: Vec<i64> = packages.iter().map(|p| p.get_id()).collect(); + let rule = GenericRule::new( + literals, rule::RULE_ROOT_REQUIRE, - PhpMixed::Array(reason_data), + rule::ReasonData::RootRequire { + package_name: package_name.clone(), + constraint: constraint.clone(), + }, ); self.add_rule(RuleSet::TYPE_REQUEST, Some(Rule::Generic(rule))); } diff --git a/crates/shirabe/src/dependency_resolver/solver.rs b/crates/shirabe/src/dependency_resolver/solver.rs index def586a..28e1e7f 100644 --- a/crates/shirabe/src/dependency_resolver/solver.rs +++ b/crates/shirabe/src/dependency_resolver/solver.rs @@ -211,23 +211,13 @@ impl Solver { .is_empty() { let mut problem = Problem::new(); - let mut reason_data: IndexMap<String, PhpMixed> = IndexMap::new(); - reason_data.insert( - "packageName".to_string(), - PhpMixed::String(package_name.clone()), - ); - // TODO(phase-b): store the constraint inside reason_data; PhpMixed needs to - // accept a `dyn ConstraintInterface` wrapper. - reason_data.insert("constraint".to_string(), PhpMixed::Null); problem.add_rule(Rc::new(RefCell::new(Rule::Generic(GenericRule::new( Vec::new(), - PhpMixed::Int(rule::RULE_ROOT_REQUIRE), - PhpMixed::Array( - reason_data - .into_iter() - .map(|(k, v)| (k, Box::new(v))) - .collect(), - ), + rule::RULE_ROOT_REQUIRE, + rule::ReasonData::RootRequire { + package_name: package_name.clone(), + constraint: active_constraint.clone(), + }, ))))); self.problems.push(problem); } @@ -620,8 +610,8 @@ impl Solver { array_unshift::<i64>(&mut other_learned_literals, learned_literal); let new_rule = GenericRule::new( other_learned_literals, - PhpMixed::Int(rule::RULE_LEARNED), - PhpMixed::Int(why), + rule::RULE_LEARNED, + rule::ReasonData::Int(why), ); Ok((learned_literal, rule_level, new_rule, why)) diff --git a/crates/shirabe/src/installer/library_installer.rs b/crates/shirabe/src/installer/library_installer.rs index daf5e33..decc1c2 100644 --- a/crates/shirabe/src/installer/library_installer.rs +++ b/crates/shirabe/src/installer/library_installer.rs @@ -55,7 +55,6 @@ impl LibraryInstaller { let filesystem = filesystem .unwrap_or_else(|| std::rc::Rc::new(std::cell::RefCell::new(Filesystem::new(None)))); let vendor_dir = rtrim( - // TODO(phase-b): Config::get returns PhpMixed; coerce to String via get_str. &composer_ref .get_config() .borrow_mut() @@ -74,7 +73,6 @@ impl LibraryInstaller { .unwrap_or_default(), Some("/"), ), - // TODO(phase-b): Config::get returns PhpMixed; coerce to String via get_str. composer_ref .get_config() .borrow_mut() diff --git a/crates/shirabe/src/json/json_file.rs b/crates/shirabe/src/json/json_file.rs index b8372b9..a185084 100644 --- a/crates/shirabe/src/json/json_file.rs +++ b/crates/shirabe/src/json/json_file.rs @@ -349,7 +349,7 @@ impl JsonFile { } // PHP: $schemaData = (object) ['$ref' => $schemaFile, '$schema' => "https://json-schema.org/draft-04/schema#"]; - // TODO(phase-b): represent (object) cast as PhpMixed::Array or a dedicated stdClass shim + // A string-keyed `PhpMixed::Array` serializes as a JSON object, matching the (object) cast. let mut schema_data: PhpMixed = { let mut m = indexmap::IndexMap::new(); m.insert( @@ -367,7 +367,6 @@ impl JsonFile { if schema == Self::STRICT_SCHEMA && is_composer_schema_file { schema_data = json_decode(&file_get_contents(&schema_file).unwrap_or_default(), false)?; - // TODO(phase-b): mutate object properties; using PhpMixed::Array we set keys if let PhpMixed::Array(map) = &mut schema_data { map.insert( "additionalProperties".to_string(), diff --git a/crates/shirabe/src/package/loader/validating_array_loader.rs b/crates/shirabe/src/package/loader/validating_array_loader.rs index fbaa0f8..4994d5a 100644 --- a/crates/shirabe/src/package/loader/validating_array_loader.rs +++ b/crates/shirabe/src/package/loader/validating_array_loader.rs @@ -990,7 +990,6 @@ impl ValidatingArrayLoader { .and_then(|v| v.as_array()) .cloned() .unwrap_or_default(); - // TODO(phase-b): convert Box<PhpMixed> maps for the shim signature. let replace_map_flat: IndexMap<String, PhpMixed> = replace_map .iter() .map(|(k, v)| (k.clone(), (**v).clone())) diff --git a/crates/shirabe/src/package/locker.rs b/crates/shirabe/src/package/locker.rs index 91affe2..5f13a31 100644 --- a/crates/shirabe/src/package/locker.rs +++ b/crates/shirabe/src/package/locker.rs @@ -89,7 +89,6 @@ impl Locker { /// Returns the md5 hash of the sorted content of the composer file. pub fn get_content_hash(composer_file_contents: &str) -> Result<String> { let content = JsonFile::parse_json(Some(composer_file_contents), Some("composer.json"))?; - // TODO(phase-b): parse_json returns PhpMixed; downstream expects map-like access let content_map: IndexMap<String, PhpMixed> = match &content { PhpMixed::Array(m) => m.iter().map(|(k, v)| (k.clone(), (**v).clone())).collect(), _ => IndexMap::new(), @@ -620,10 +619,9 @@ impl Locker { } else { None }; - // TODO(phase-b): PhpMixed lacks PartialEq; PHP compares lock array with current data let differs = current_data .as_ref() - .map(|c| !std::ptr::eq(c as *const _, &lock as *const _)) + .map(|c| !c.iter().eq(lock.iter())) .unwrap_or(true); if !is_locked || differs { if write { @@ -725,8 +723,6 @@ impl Locker { }) .unwrap_or(false); if should_replace { - // PHP: $lockData[$key] = new \stdClass(); - // TODO(phase-b): represent empty stdClass distinctly from empty array lock_data.insert(key.to_string(), PhpMixed::Array(IndexMap::new())); } } diff --git a/crates/shirabe/src/repository/artifact_repository.rs b/crates/shirabe/src/repository/artifact_repository.rs index 1509de3..3715c27 100644 --- a/crates/shirabe/src/repository/artifact_repository.rs +++ b/crates/shirabe/src/repository/artifact_repository.rs @@ -203,7 +203,6 @@ impl ArtifactRepository { arr.insert("dist".to_string(), Box::new(PhpMixed::Array(dist))); } - // TODO(phase-b): load wants IndexMap<String, PhpMixed>; convert from PhpMixed::Array. let cfg: IndexMap<String, PhpMixed> = package .as_array() .cloned() diff --git a/crates/shirabe/src/repository/composer_repository.rs b/crates/shirabe/src/repository/composer_repository.rs index 644ece7..3dbca7d 100644 --- a/crates/shirabe/src/repository/composer_repository.rs +++ b/crates/shirabe/src/repository/composer_repository.rs @@ -854,18 +854,27 @@ impl ComposerRepository { return Ok(results); } - // TODO(phase-b): inner.search returns Vec<SearchResult>; convert to PHP-shaped map let inner_results = self.inner.search(query, mode, None)?; let converted: Vec<IndexMap<String, PhpMixed>> = inner_results .into_iter() .map(|sr| { let mut m: IndexMap<String, PhpMixed> = IndexMap::new(); m.insert("name".to_string(), PhpMixed::String(sr.name)); - if let Some(d) = sr.description { - m.insert("description".to_string(), PhpMixed::String(d)); - } - if let Some(u) = sr.url { - m.insert("url".to_string(), PhpMixed::String(u)); + m.insert( + "description".to_string(), + match sr.description { + Some(d) => PhpMixed::String(d), + None => PhpMixed::Null, + }, + ); + if let Some(ab) = sr.abandoned { + m.insert( + "abandoned".to_string(), + match ab { + crate::repository::AbandonedInfo::Replacement(r) => PhpMixed::String(r), + crate::repository::AbandonedInfo::Abandoned => PhpMixed::Bool(true), + }, + ); } m }) @@ -1205,12 +1214,15 @@ impl ComposerRepository { if Countable::count(&self.inner) > 0 { for (k, v) in self.inner.get_providers(package_name.to_string())? { - // TODO(phase-b): ProviderInfo -> IndexMap<String, PhpMixed> conversion needed let mut entry: IndexMap<String, PhpMixed> = IndexMap::new(); entry.insert("name".to_string(), PhpMixed::String(v.name)); - if let Some(d) = v.description { - entry.insert("description".to_string(), PhpMixed::String(d)); - } + entry.insert( + "description".to_string(), + match v.description { + Some(d) => PhpMixed::String(d), + None => PhpMixed::Null, + }, + ); entry.insert("type".to_string(), PhpMixed::String(v.r#type)); result.insert(k, entry); } diff --git a/crates/shirabe/src/repository/filesystem_repository.rs b/crates/shirabe/src/repository/filesystem_repository.rs index bb78886..fd7349a 100644 --- a/crates/shirabe/src/repository/filesystem_repository.rs +++ b/crates/shirabe/src/repository/filesystem_repository.rs @@ -151,7 +151,6 @@ impl FilesystemRepository { let mut loader = ArrayLoader::new(None, true); if let Some(packages_list) = packages.as_list() { for package_data in packages_list.iter() { - // TODO(phase-b): expected IndexMap<String, PhpMixed> but package_data is PhpMixed. let cfg = (**package_data) .as_array() .cloned() @@ -167,7 +166,6 @@ impl FilesystemRepository { } } else if let Some(packages_array) = packages.as_array() { for (_, package_data) in packages_array.iter() { - // TODO(phase-b): expected IndexMap<String, PhpMixed> but package_data is PhpMixed. let cfg = (**package_data) .as_array() .cloned() diff --git a/crates/shirabe/src/repository/vcs/forgejo_driver.rs b/crates/shirabe/src/repository/vcs/forgejo_driver.rs index 3c1366d..a40f6a9 100644 --- a/crates/shirabe/src/repository/vcs/forgejo_driver.rs +++ b/crates/shirabe/src/repository/vcs/forgejo_driver.rs @@ -347,9 +347,10 @@ impl ForgejoDriver { if !self.inner.info_cache.contains_key(identifier) { let composer = if self.inner.should_cache(identifier) { if let Some(res) = self.inner.cache.as_mut().and_then(|c| c.read(identifier)) { - // TODO(phase-b): JsonFile::parse_json returns PhpMixed; convert into Option<IndexMap> - let _ = JsonFile::parse_json(Some(res.as_str()), None)?; - None + let parsed = JsonFile::parse_json(Some(res.as_str()), None)?; + parsed + .as_array() + .map(|m| m.iter().map(|(k, v)| (k.clone(), (**v).clone())).collect()) } else { let file_content = self.get_file_content("composer.json", identifier)?; let c = VcsDriverBase::finish_base_composer_information( diff --git a/crates/shirabe/src/repository/vcs/git_bitbucket_driver.rs b/crates/shirabe/src/repository/vcs/git_bitbucket_driver.rs index 3f0be13..f8cfd7a 100644 --- a/crates/shirabe/src/repository/vcs/git_bitbucket_driver.rs +++ b/crates/shirabe/src/repository/vcs/git_bitbucket_driver.rs @@ -239,7 +239,6 @@ impl GitBitbucketDriver { .and_then(|v| v.as_string()) .map(String::from); - // TODO(phase-b): unwrap PhpMixed::Array into the typed IndexMap stored on self self.repo_data = match repo_data { PhpMixed::Array(m) => m.into_iter().map(|(k, v)| (k, *v)).collect(), _ => IndexMap::new(), @@ -262,18 +261,9 @@ impl GitBitbucketDriver { if self.inner.should_cache(identifier) && { let res = self.inner.cache.as_mut().and_then(|c| c.read(identifier)); if let Some(res) = res { - // TODO(phase-b): wrap parsed PhpMixed::Array into the IndexMap-shaped composer slot - composer = Some( - JsonFile::parse_json(Some(&res), None)? - .as_array() - .cloned() - .map(|m| { - m.into_iter() - .map(|(k, v)| (k, *v)) - .collect::<IndexMap<String, PhpMixed>>() - }) - .unwrap_or_default(), - ); + composer = JsonFile::parse_json(Some(&res), None)? + .as_array() + .map(|m| m.iter().map(|(k, v)| (k.clone(), (**v).clone())).collect()); true } else { false diff --git a/crates/shirabe/src/repository/vcs/github_driver.rs b/crates/shirabe/src/repository/vcs/github_driver.rs index 6fc50ee..45ba7b7 100644 --- a/crates/shirabe/src/repository/vcs/github_driver.rs +++ b/crates/shirabe/src/repository/vcs/github_driver.rs @@ -279,7 +279,6 @@ impl GitHubDriver { .as_mut() .and_then(|c| c.read(identifier)) .unwrap_or_default(); - // TODO(phase-b): cached payload is JSON string; parse to PhpMixed -> Option<IndexMap> let parsed = JsonFile::parse_json(Some(&res), None)?; parsed .as_array() diff --git a/crates/shirabe/src/repository/vcs/gitlab_driver.rs b/crates/shirabe/src/repository/vcs/gitlab_driver.rs index 84961f5..97dcc30 100644 --- a/crates/shirabe/src/repository/vcs/gitlab_driver.rs +++ b/crates/shirabe/src/repository/vcs/gitlab_driver.rs @@ -275,18 +275,9 @@ impl GitLabDriver { .as_mut() .and_then(|c| c.read(identifier)) .unwrap_or_default(); - // TODO(phase-b): cached payload is wrapped to satisfy outer Option type - Some( - JsonFile::parse_json(Some(&res), None)? - .as_array() - .cloned() - .map(|m| { - m.into_iter() - .map(|(k, v)| (k, *v)) - .collect::<IndexMap<String, PhpMixed>>() - }) - .unwrap_or_default(), - ) + JsonFile::parse_json(Some(&res), None)? + .as_array() + .map(|m| m.iter().map(|(k, v)| (k.clone(), (**v).clone())).collect()) } else { let file_content = self.get_file_content("composer.json", identifier)?; let composer = VcsDriverBase::finish_base_composer_information( diff --git a/crates/shirabe/src/repository/vcs/svn_driver.rs b/crates/shirabe/src/repository/vcs/svn_driver.rs index 377d698..ef8787a 100644 --- a/crates/shirabe/src/repository/vcs/svn_driver.rs +++ b/crates/shirabe/src/repository/vcs/svn_driver.rs @@ -186,10 +186,13 @@ impl SvnDriver { } let parsed = JsonFile::parse_json(Some(res.as_str()), None)?; - // TODO(phase-b): info_cache expects Option<IndexMap<String, PhpMixed>>; - // PhpMixed → IndexMap conversion is non-trivial here. Skip insert/return. - let _ = parsed; - return Ok(None); + let composer: Option<IndexMap<String, PhpMixed>> = parsed + .as_array() + .map(|m| m.iter().map(|(k, v)| (k.clone(), (**v).clone())).collect()); + self.inner + .info_cache + .insert(identifier.to_string(), composer.clone()); + return Ok(composer); } } @@ -253,15 +256,6 @@ impl SvnDriver { .info_cache .get(identifier) .and_then(|v| v.clone()); - if cached.is_none() - || !is_array( - // TODO(phase-b): wrap IndexMap to PhpMixed for is_array check - &cached.clone().map(PhpMixed::from).unwrap_or(PhpMixed::Null), - ) - { - return Ok(None); - } - Ok(cached) } |
