aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-06-27 19:44:01 +0900
committernsfisis <nsfisis@gmail.com>2026-06-27 20:18:56 +0900
commite63ccb5cbb479df0f0e5010065ff57bbf4ca238f (patch)
treef91c7db9b3475e845b023f51cef2d6379e80ae5e
parent3cb13f1d7025351615c254b9225a4c5d1bcbd170 (diff)
downloadphp-shirabe-e63ccb5cbb479df0f0e5010065ff57bbf4ca238f.tar.gz
php-shirabe-e63ccb5cbb479df0f0e5010065ff57bbf4ca238f.tar.zst
php-shirabe-e63ccb5cbb479df0f0e5010065ff57bbf4ca238f.zip
fix(json): encode empty stdClass fields as {} not []
Empty PhpMixed::Array now serializes as [], so the several places that build a PHP `new \stdClass` via an empty Array were emitting [] where Composer writes {}. Use PhpMixed::Object for those empty-object cases: - InitCommand: require / require-dev - Locker::fixupJsonDataType: stability-flags / platform / platform-dev - JsonConfigSource fallback: require/config keys that must stay objects Align the affected test expectations with Composer: JsonFile::read() decodes with assoc=true, collapsing the on-disk {} back to [], so reads expect []. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
-rw-r--r--crates/shirabe/src/command/init_command.rs8
-rw-r--r--crates/shirabe/src/config/json_config_source.rs9
-rw-r--r--crates/shirabe/src/package/locker.rs3
-rw-r--r--crates/shirabe/tests/command/init_command_test.rs33
-rw-r--r--crates/shirabe/tests/command/repository_command_test.rs6
5 files changed, 29 insertions, 30 deletions
diff --git a/crates/shirabe/src/command/init_command.rs b/crates/shirabe/src/command/init_command.rs
index ab0e52c..88a2934 100644
--- a/crates/shirabe/src/command/init_command.rs
+++ b/crates/shirabe/src/command/init_command.rs
@@ -226,8 +226,8 @@ impl Command for InitCommand {
.unwrap_or_default();
let formatted = self.format_requirements(req_list)?;
if formatted.is_empty() {
- // PHP: new \stdClass — represented as an empty IndexMap (JSON object)
- PhpMixed::Array(IndexMap::new())
+ // PHP: new \stdClass — empty JSON object
+ PhpMixed::Object(IndexMap::new())
} else {
PhpMixed::Array(
formatted
@@ -238,7 +238,7 @@ impl Command for InitCommand {
}
} else {
// PHP: new \stdClass
- PhpMixed::Array(IndexMap::new())
+ PhpMixed::Object(IndexMap::new())
};
options.insert("require".to_string(), require_value);
@@ -254,7 +254,7 @@ impl Command for InitCommand {
.unwrap_or_default();
let formatted = self.format_requirements(req_list)?;
let value = if formatted.is_empty() {
- PhpMixed::Array(IndexMap::new())
+ PhpMixed::Object(IndexMap::new())
} else {
PhpMixed::Array(
formatted
diff --git a/crates/shirabe/src/config/json_config_source.rs b/crates/shirabe/src/config/json_config_source.rs
index 2509239..1ce51ad 100644
--- a/crates/shirabe/src/config/json_config_source.rs
+++ b/crates/shirabe/src/config/json_config_source.rs
@@ -98,7 +98,7 @@ impl JsonConfigSource {
&& inner.is_empty()
{
// PHP: $config[$prop] = new \stdClass;
- map.insert(prop.to_string(), PhpMixed::Array(IndexMap::new()));
+ map.insert(prop.to_string(), PhpMixed::Object(IndexMap::new()));
}
}
for prop in ["psr-0", "psr-4"] {
@@ -108,14 +108,15 @@ impl JsonConfigSource {
&& let Some(PhpMixed::Array(inner_map)) = autoload_map.get(prop)
&& inner_map.is_empty()
{
- autoload_map.insert(prop.to_string(), PhpMixed::Array(IndexMap::new()));
+ autoload_map.insert(prop.to_string(), PhpMixed::Object(IndexMap::new()));
}
if let Some(autoload_dev) = map.get_mut("autoload-dev")
&& let PhpMixed::Array(autoload_dev_map) = autoload_dev
&& let Some(PhpMixed::Array(inner_map)) = autoload_dev_map.get(prop)
&& inner_map.is_empty()
{
- autoload_dev_map.insert(prop.to_string(), PhpMixed::Array(IndexMap::new()));
+ autoload_dev_map
+ .insert(prop.to_string(), PhpMixed::Object(IndexMap::new()));
}
}
}
@@ -136,7 +137,7 @@ impl JsonConfigSource {
&& let Some(PhpMixed::Array(inner_map)) = cfg_map.get(prop)
&& inner_map.is_empty()
{
- cfg_map.insert(prop.to_string(), PhpMixed::Array(IndexMap::new()));
+ cfg_map.insert(prop.to_string(), PhpMixed::Object(IndexMap::new()));
}
}
self.file.borrow().write(config)?;
diff --git a/crates/shirabe/src/package/locker.rs b/crates/shirabe/src/package/locker.rs
index df5e1de..1f123f1 100644
--- a/crates/shirabe/src/package/locker.rs
+++ b/crates/shirabe/src/package/locker.rs
@@ -705,7 +705,8 @@ impl Locker {
})
.unwrap_or(false);
if should_replace {
- lock_data.insert(key.to_string(), PhpMixed::Array(IndexMap::new()));
+ // PHP: new \stdClass() — empty JSON object
+ lock_data.insert(key.to_string(), PhpMixed::Object(IndexMap::new()));
}
}
diff --git a/crates/shirabe/tests/command/init_command_test.rs b/crates/shirabe/tests/command/init_command_test.rs
index efb3f49..32226f6 100644
--- a/crates/shirabe/tests/command/init_command_test.rs
+++ b/crates/shirabe/tests/command/init_command_test.rs
@@ -159,9 +159,6 @@ fn test_namespace_from_missing_package_name() {
}
/// @return iterable<string, array{0: array<string, mixed>, 1: array<string, mixed>}>
-///
-/// Empty `require` is `{}` (PHP's `new \stdClass`), not `[]`, to match what the command writes:
-/// shirabe's `json_decode` keeps the empty object/array distinction that PHP collapses.
fn run_data_provider() -> Vec<(serde_json::Value, Vec<(PhpMixed, PhpMixed)>)> {
vec![
// name argument
@@ -169,7 +166,7 @@ fn run_data_provider() -> Vec<(serde_json::Value, Vec<(PhpMixed, PhpMixed)>)> {
serde_json::json!({
"name": "test/pkg",
"authors": [default_authors()],
- "require": {},
+ "require": [],
}),
vec![opt("--name", "test/pkg")],
),
@@ -177,7 +174,7 @@ fn run_data_provider() -> Vec<(serde_json::Value, Vec<(PhpMixed, PhpMixed)>)> {
(
serde_json::json!({
"name": "test/pkg",
- "require": {},
+ "require": [],
"authors": [{ "name": "Mr. Test", "email": "test@example.org" }],
}),
vec![
@@ -189,7 +186,7 @@ fn run_data_provider() -> Vec<(serde_json::Value, Vec<(PhpMixed, PhpMixed)>)> {
(
serde_json::json!({
"name": "test/pkg",
- "require": {},
+ "require": [],
"authors": [{ "name": "Mr. Test" }],
}),
vec![opt("--name", "test/pkg"), opt("--author", "Mr. Test")],
@@ -199,7 +196,7 @@ fn run_data_provider() -> Vec<(serde_json::Value, Vec<(PhpMixed, PhpMixed)>)> {
serde_json::json!({
"name": "test/pkg",
"authors": [default_authors()],
- "require": {},
+ "require": [],
"repositories": [{ "type": "vcs", "url": "http://packages.example.com" }],
}),
vec![
@@ -215,7 +212,7 @@ fn run_data_provider() -> Vec<(serde_json::Value, Vec<(PhpMixed, PhpMixed)>)> {
serde_json::json!({
"name": "test/pkg",
"authors": [default_authors()],
- "require": {},
+ "require": [],
"repositories": [
{ "type": "vcs", "url": "http://vcs.example.com" },
{ "type": "composer", "url": "http://composer.example.com" },
@@ -243,7 +240,7 @@ fn run_data_provider() -> Vec<(serde_json::Value, Vec<(PhpMixed, PhpMixed)>)> {
serde_json::json!({
"name": "test/pkg",
"authors": [default_authors()],
- "require": {},
+ "require": [],
"minimum-stability": "dev",
}),
vec![opt("--name", "test/pkg"), opt("--stability", "dev")],
@@ -277,7 +274,7 @@ fn run_data_provider() -> Vec<(serde_json::Value, Vec<(PhpMixed, PhpMixed)>)> {
serde_json::json!({
"name": "test/pkg",
"authors": [default_authors()],
- "require": {},
+ "require": [],
"require-dev": { "first/pkg": "1.0.0" },
}),
vec![
@@ -290,7 +287,7 @@ fn run_data_provider() -> Vec<(serde_json::Value, Vec<(PhpMixed, PhpMixed)>)> {
serde_json::json!({
"name": "test/pkg",
"authors": [default_authors()],
- "require": {},
+ "require": [],
"require-dev": { "first/pkg": "1.0.0", "second/pkg": "^3.4" },
}),
vec![
@@ -303,7 +300,7 @@ fn run_data_provider() -> Vec<(serde_json::Value, Vec<(PhpMixed, PhpMixed)>)> {
serde_json::json!({
"name": "test/pkg",
"authors": [default_authors()],
- "require": {},
+ "require": [],
"autoload": { "psr-4": { "Test\\Pkg\\": "testMapping/" } },
}),
vec![opt("--name", "test/pkg"), opt("--autoload", "testMapping/")],
@@ -313,7 +310,7 @@ fn run_data_provider() -> Vec<(serde_json::Value, Vec<(PhpMixed, PhpMixed)>)> {
serde_json::json!({
"name": "test/pkg",
"authors": [default_authors()],
- "require": {},
+ "require": [],
"homepage": "https://example.org/",
}),
vec![
@@ -326,7 +323,7 @@ fn run_data_provider() -> Vec<(serde_json::Value, Vec<(PhpMixed, PhpMixed)>)> {
serde_json::json!({
"name": "test/pkg",
"authors": [default_authors()],
- "require": {},
+ "require": [],
"description": "My first example package",
}),
vec![
@@ -339,7 +336,7 @@ fn run_data_provider() -> Vec<(serde_json::Value, Vec<(PhpMixed, PhpMixed)>)> {
serde_json::json!({
"name": "test/pkg",
"authors": [default_authors()],
- "require": {},
+ "require": [],
"type": "project",
}),
vec![opt("--name", "test/pkg"), opt("--type", "project")],
@@ -349,7 +346,7 @@ fn run_data_provider() -> Vec<(serde_json::Value, Vec<(PhpMixed, PhpMixed)>)> {
serde_json::json!({
"name": "test/pkg",
"authors": [default_authors()],
- "require": {},
+ "require": [],
"license": "MIT",
}),
vec![opt("--name", "test/pkg"), opt("--license", "MIT")],
@@ -510,7 +507,7 @@ fn test_run_guess_name_from_dir_sanitizes_dir() {
let expected = serde_json::json!({
"name": "vendor-name/foo-bar_baz.qux",
"authors": [default_authors()],
- "require": {},
+ "require": [],
});
assert_eq!(
expected,
@@ -562,7 +559,7 @@ fn test_interactive_run() {
"license": "AGPL-3.0-only",
"authors": [{ "name": "Mr. Test", "email": "test@example.org" }],
"minimum-stability": "stable",
- "require": {},
+ "require": [],
});
assert_eq!(expected, read_composer_json(&dir));
}
diff --git a/crates/shirabe/tests/command/repository_command_test.rs b/crates/shirabe/tests/command/repository_command_test.rs
index 1d104e7..901b9f3 100644
--- a/crates/shirabe/tests/command/repository_command_test.rs
+++ b/crates/shirabe/tests/command/repository_command_test.rs
@@ -34,7 +34,7 @@ fn test_list_with_no_repositories() {
app_tester.get_display().trim()
);
// composer.json should remain unchanged
- assert_eq!(serde_json::json!({}), read_composer_json());
+ assert_eq!(serde_json::json!([]), read_composer_json());
drop(tear_down);
}
@@ -216,7 +216,7 @@ fn test_remove_repository() {
if let Some(repositories) = json.get("repositories") {
assert_eq!(&serde_json::json!([]), repositories);
} else {
- assert_eq!(serde_json::json!({}), json);
+ assert_eq!(serde_json::json!([]), json);
}
drop(tear_down);
@@ -392,7 +392,7 @@ fn test_disable_and_enable_packagist() {
)
.unwrap();
assert_eq!(0, status_code);
- assert_eq!(serde_json::json!({}), read_composer_json());
+ assert_eq!(serde_json::json!([]), read_composer_json());
drop(tear_down);
}