aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-06-08 04:22:49 +0900
committernsfisis <nsfisis@gmail.com>2026-06-08 04:23:11 +0900
commit632c9793927a30c4bca3c91888e66b369d732dfe (patch)
tree5bafb9d7fa2523d5a2f8d70ff59c3173e962e389
parent243450fee9853c2fea66ae0642dabb8db5227d6f (diff)
downloadphp-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.
-rw-r--r--crates/shirabe-php-shim/src/lib.rs49
-rw-r--r--crates/shirabe/src/advisory/any_security_advisory.rs3
-rw-r--r--crates/shirabe/src/advisory/auditor.rs68
-rw-r--r--crates/shirabe/src/advisory/security_advisory.rs9
-rw-r--r--crates/shirabe/src/autoload/class_loader.rs59
-rw-r--r--crates/shirabe/src/command/audit_command.rs21
-rw-r--r--crates/shirabe/src/command/base_dependency_command.rs2
-rw-r--r--crates/shirabe/src/command/init_command.rs1
-rw-r--r--crates/shirabe/src/command/search_command.rs5
-rw-r--r--crates/shirabe/src/command/show_command.rs39
-rw-r--r--crates/shirabe/src/command/suggests_command.rs9
-rw-r--r--crates/shirabe/src/command/update_command.rs3
-rw-r--r--crates/shirabe/src/config.rs2
-rw-r--r--crates/shirabe/src/dependency_resolver/generic_rule.rs6
-rw-r--r--crates/shirabe/src/dependency_resolver/rule_set_generator.rs25
-rw-r--r--crates/shirabe/src/dependency_resolver/solver.rs24
-rw-r--r--crates/shirabe/src/installer/library_installer.rs2
-rw-r--r--crates/shirabe/src/json/json_file.rs3
-rw-r--r--crates/shirabe/src/package/loader/validating_array_loader.rs1
-rw-r--r--crates/shirabe/src/package/locker.rs6
-rw-r--r--crates/shirabe/src/repository/artifact_repository.rs1
-rw-r--r--crates/shirabe/src/repository/composer_repository.rs32
-rw-r--r--crates/shirabe/src/repository/filesystem_repository.rs2
-rw-r--r--crates/shirabe/src/repository/vcs/forgejo_driver.rs7
-rw-r--r--crates/shirabe/src/repository/vcs/git_bitbucket_driver.rs16
-rw-r--r--crates/shirabe/src/repository/vcs/github_driver.rs1
-rw-r--r--crates/shirabe/src/repository/vcs/gitlab_driver.rs15
-rw-r--r--crates/shirabe/src/repository/vcs/svn_driver.rs20
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)
}