From 632c9793927a30c4bca3c91888e66b369d732dfe Mon Sep 17 00:00:00 2001 From: nsfisis Date: Mon, 8 Jun 2026 04:22:49 +0900 Subject: feat(phase-c): resolve PhpMixed-conversion phase-b TODOs Implement the foundational PhpMixed conversion infrastructure (From, 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. --- crates/shirabe/src/command/audit_command.rs | 21 ++++++++---- .../shirabe/src/command/base_dependency_command.rs | 2 -- crates/shirabe/src/command/init_command.rs | 1 - crates/shirabe/src/command/search_command.rs | 5 ++- crates/shirabe/src/command/show_command.rs | 39 ++++++++++++++++------ crates/shirabe/src/command/suggests_command.rs | 9 ++--- crates/shirabe/src/command/update_command.rs | 3 -- 7 files changed, 52 insertions(+), 28 deletions(-) (limited to 'crates/shirabe/src/command') 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> = + 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> - 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; build PhpMixed cells once a - // converter exists for Vec rows. let table_as_mixed: Vec = 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 for array_filter_map let filtered_input: IndexMap> = 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 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 = 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 - let _platform_cfg = composer.get_config().borrow().get("platform"); - let platform_overrides: IndexMap = - todo!("extract IndexMap from PhpMixed config value"); + let platform_cfg = composer.get_config().borrow().get("platform"); + let platform_overrides: IndexMap = 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 choices; convert IndexMap autocompleter values - // to choices and downcast PhpMixed back to Vec. let select_result = io.select( "Select packages: (Select more than one value separated by comma) ".to_string(), autocompleter_values -- cgit v1.3.1