diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-06-21 16:31:44 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-06-21 16:32:52 +0900 |
| commit | 657440d01423b50153d07ebc288d9bd2fc982d52 (patch) | |
| tree | 1a4b936f8eb0d2df660a390dc9aed0c076ebaa37 /crates | |
| parent | fcef25f6ef36287a4984ffdaab39df84f5aceeba (diff) | |
| download | php-shirabe-657440d01423b50153d07ebc288d9bd2fc982d52.tar.gz php-shirabe-657440d01423b50153d07ebc288d9bd2fc982d52.tar.zst php-shirabe-657440d01423b50153d07ebc288d9bd2fc982d52.zip | |
refactor(json): eliminate json_last_error in favor of Result
json_encode/json_encode_ex now return anyhow::Result<String> instead of
Option, so callers no longer need json_last_error() to get the failure
reason.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates')
| -rw-r--r-- | crates/shirabe-php-shim/src/json.rs | 21 | ||||
| -rw-r--r-- | crates/shirabe-php-shim/src/runtime.rs | 1 | ||||
| -rw-r--r-- | crates/shirabe/src/command/config_command.rs | 2 | ||||
| -rw-r--r-- | crates/shirabe/src/io/base_io.rs | 3 | ||||
| -rw-r--r-- | crates/shirabe/src/json/json_file.rs | 79 | ||||
| -rw-r--r-- | crates/shirabe/tests/json/json_file_test.rs | 20 |
6 files changed, 45 insertions, 81 deletions
diff --git a/crates/shirabe-php-shim/src/json.rs b/crates/shirabe-php-shim/src/json.rs index ed704f9..23c4c41 100644 --- a/crates/shirabe-php-shim/src/json.rs +++ b/crates/shirabe-php-shim/src/json.rs @@ -17,29 +17,22 @@ pub const JSON_PRETTY_PRINT: i64 = 128; pub const JSON_THROW_ON_ERROR: i64 = 4194304; pub const JSON_INVALID_UTF8_IGNORE: i64 = 1048576; -pub const JSON_ERROR_NONE: i64 = 0; -pub const JSON_ERROR_DEPTH: i64 = 1; -pub const JSON_ERROR_STATE_MISMATCH: i64 = 2; -pub const JSON_ERROR_CTRL_CHAR: i64 = 3; -pub const JSON_ERROR_UTF8: i64 = 5; - -pub fn json_last_error() -> i64 { - todo!() -} - -pub fn json_encode<T: serde::Serialize + ?Sized>(value: &T) -> Option<String> { +pub fn json_encode<T: serde::Serialize + ?Sized>(value: &T) -> anyhow::Result<String> { // PHP's json_encode() with no flags escapes slashes and non-ASCII characters. json_encode_ex(value, 0) } -pub fn json_encode_ex<T: serde::Serialize + ?Sized>(value: &T, flags: i64) -> Option<String> { +pub fn json_encode_ex<T: serde::Serialize + ?Sized>( + value: &T, + flags: i64, +) -> anyhow::Result<String> { // serde_json's compact output already matches PHP's `json_encode` with both // JSON_UNESCAPED_SLASHES and JSON_UNESCAPED_UNICODE set: forward slashes and non-ASCII // characters are emitted verbatim. The two flags below re-apply PHP's default escaping when // they are absent. // TODO(phase-c): other flags (e.g. JSON_PRETTY_PRINT, JSON_HEX_*, JSON_THROW_ON_ERROR) are not // handled yet; add them when a call site needs them. - let mut s = serde_json::to_string(value).ok()?; + let mut s = serde_json::to_string(value)?; if flags & JSON_UNESCAPED_SLASHES == 0 { s = s.replace('/', "\\/"); @@ -60,7 +53,7 @@ pub fn json_encode_ex<T: serde::Serialize + ?Sized>(value: &T, flags: i64) -> Op s = out; } - Some(s) + Ok(s) } // PHP's two-argument `json_decode`: without JSON_THROW_ON_ERROR it never throws, diff --git a/crates/shirabe-php-shim/src/runtime.rs b/crates/shirabe-php-shim/src/runtime.rs index cc63af6..2f98891 100644 --- a/crates/shirabe-php-shim/src/runtime.rs +++ b/crates/shirabe-php-shim/src/runtime.rs @@ -61,7 +61,6 @@ pub fn defined(name: &str) -> bool { | "CURL_VERSION_LIBZ" | "CURL_VERSION_ZSTD" | "GLOB_BRACE" - | "JSON_ERROR_UTF8" | "OPENSSL_VERSION_TEXT" | "PHP_BINARY" | "SIGINT" diff --git a/crates/shirabe/src/command/config_command.rs b/crates/shirabe/src/command/config_command.rs index f6dffb7..1517e57 100644 --- a/crates/shirabe/src/command/config_command.rs +++ b/crates/shirabe/src/command/config_command.rs @@ -1332,7 +1332,7 @@ impl ConfigCommand { return Err(RuntimeException { message: format!( "{} is an invalid value{}", - PhpMixed::from(json_encode(&values_mixed)), + PhpMixed::from(json_encode(&values_mixed).ok()), suffix ), code: 0, diff --git a/crates/shirabe/src/io/base_io.rs b/crates/shirabe/src/io/base_io.rs index 99c3d70..79fd7b7 100644 --- a/crates/shirabe/src/io/base_io.rs +++ b/crates/shirabe/src/io/base_io.rs @@ -451,7 +451,8 @@ pub trait BaseIO: IOInterface { Ok(json_encode_ex( &PhpMixed::Array(log_context(context)), JSON_INVALID_UTF8_IGNORE | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE, - )) + ) + .ok()) }); if let Ok(Some(json_str)) = json { message_str += " "; diff --git a/crates/shirabe/src/json/json_file.rs b/crates/shirabe/src/json/json_file.rs index 167a163..4145385 100644 --- a/crates/shirabe/src/json/json_file.rs +++ b/crates/shirabe/src/json/json_file.rs @@ -9,12 +9,10 @@ use shirabe_external_packages::json_schema::Validator; use shirabe_external_packages::seld::json_lint::JsonParser; use shirabe_external_packages::seld::json_lint::ParsingException; use shirabe_php_shim::{ - InvalidArgumentException, JSON_ERROR_CTRL_CHAR, JSON_ERROR_DEPTH, JSON_ERROR_NONE, - JSON_ERROR_STATE_MISMATCH, JSON_ERROR_UTF8, JSON_PRETTY_PRINT, JSON_UNESCAPED_SLASHES, - JSON_UNESCAPED_UNICODE, PhpMixed, RuntimeException, UnexpectedValueException, defined, dirname, - file_exists, file_get_contents, file_put_contents, is_dir, is_file, json_decode, - json_encode_ex, json_last_error, mkdir, php_dir, realpath, str_contains, str_ends_with, - str_repeat, strlen, strpos, usleep, + InvalidArgumentException, JSON_PRETTY_PRINT, JSON_UNESCAPED_SLASHES, JSON_UNESCAPED_UNICODE, + PhpMixed, RuntimeException, UnexpectedValueException, dirname, file_exists, file_get_contents, + file_put_contents, is_dir, is_file, json_decode, json_encode_ex, mkdir, php_dir, realpath, + str_contains, str_ends_with, str_repeat, strlen, strpos, usleep, }; use crate::downloader::TransportException; @@ -413,18 +411,12 @@ impl JsonFile { data: &T, options: JsonEncodeOptions, ) -> String { - let json = json_encode_ex(data, options.to_flags()); - - let json = match json { - Some(j) => j, - None => { - // PHP: self::throwEncodeError(json_last_error()), which throws \RuntimeException. - // TODO(phase-c): faithfully propagating this requires encode/encode_with_options to - // return Result<String>, a signature change rippling across ~53 call sites. - Self::throw_encode_error(json_last_error()).unwrap_or_default(); - String::new() - } - }; + let json = json_encode_ex(data, options.to_flags()) + .map_err(|err| RuntimeException { + message: format!("JSON encoding failed: {}", err), + code: 0, + }) + .unwrap(); // TODO(phase-c): propagating an Err. if options.pretty_print && options.indent != Self::INDENT_DEFAULT { // Pretty printing and not using default indentation @@ -449,31 +441,6 @@ impl JsonFile { json } - /// Throws an exception according to a given code with a customized message - /// - /// @param int $code return code of json_last_error function - /// @throws \RuntimeException - /// @return never - fn throw_encode_error(code: i64) -> Result<()> { - let msg = if code == JSON_ERROR_DEPTH { - "Maximum stack depth exceeded" - } else if code == JSON_ERROR_STATE_MISMATCH { - "Underflow or the modes mismatch" - } else if code == JSON_ERROR_CTRL_CHAR { - "Unexpected control character found" - } else if code == JSON_ERROR_UTF8 { - "Malformed UTF-8 characters, possibly incorrectly encoded" - } else { - "Unknown error" - }; - - Err(RuntimeException { - message: format!("JSON encoding failed: {}", msg), - code: 0, - } - .into()) - } - /// Parses json string and returns hash. /// /// @param null|string $json json string @@ -487,7 +454,11 @@ impl JsonFile { Some(j) => j, }; let mut data = json_decode(json, true)?; - if matches!(data, PhpMixed::Null) && JSON_ERROR_NONE != json_last_error() { + // PHP: `null === $data && JSON_ERROR_NONE !== json_last_error()`, i.e. the decode produced + // null because of an error rather than because the input was the literal `null`. json_decode + // here swallows the error into PhpMixed::Null, so detect the failure by comparing the source + // against `null`, mirroring validateSchema's own `'null' !== $content` check. + if matches!(data, PhpMixed::Null) && json != "null" { // attempt resolving simple conflicts in lock files so that one can run `composer update --lock` and get a valid lock file if let Some(file) = file && str_ends_with(file, ".lock") @@ -524,16 +495,16 @@ impl JsonFile { let mut parser = JsonParser::new(); let result = parser.lint(json); if result.is_none() { - if defined("JSON_ERROR_UTF8") && JSON_ERROR_UTF8 == json_last_error() { - return Err(UnexpectedValueException { - message: match file { - None => "The input is not UTF-8, could not parse as JSON".to_string(), - Some(f) => format!("\"{}\" is not UTF-8, could not parse as JSON", f), - }, - code: 0, - } - .into()); - } + // TODO(phase-c): Rust's &str is guaranteed as UTF-8, but PHP string is not. Change `json` + // to &[u8] and check UTF-8 validity here. + + // if (defined('JSON_ERROR_UTF8') && JSON_ERROR_UTF8 === json_last_error()) { + // if ($file === null) { + // throw new \UnexpectedValueException('The input is not UTF-8, could not parse as JSON'); + // } else { + // throw new \UnexpectedValueException('"' . $file . '" is not UTF-8, could not parse as JSON'); + // } + // } return Ok(true); } diff --git a/crates/shirabe/tests/json/json_file_test.rs b/crates/shirabe/tests/json/json_file_test.rs index 36e1136..b6fecc6 100644 --- a/crates/shirabe/tests/json/json_file_test.rs +++ b/crates/shirabe/tests/json/json_file_test.rs @@ -10,70 +10,70 @@ fn expect_parse_exception(text: &str, json: &str) { } #[test] -#[ignore = "JsonFile::parse_json error path reaches json_last_error (todo!()) in the php-shim"] +#[ignore = "JsonFile::parse_json error path reaches JsonParser::new()/lint() (todo!()); the JSON linter is not yet ported"] fn test_parse_error_detect_extra_comma() { let json = "{\n \"foo\": \"bar\",\n}"; expect_parse_exception("Parse error on line 2", json); } #[test] -#[ignore = "JsonFile::parse_json error path reaches json_last_error (todo!()) in the php-shim"] +#[ignore = "JsonFile::parse_json error path reaches JsonParser::new()/lint() (todo!()); the JSON linter is not yet ported"] fn test_parse_error_detect_extra_comma_in_array() { let json = "{\n \"foo\": [\n \"bar\",\n ]\n}"; expect_parse_exception("Parse error on line 3", json); } #[test] -#[ignore = "JsonFile::parse_json error path reaches json_last_error (todo!()) in the php-shim"] +#[ignore = "JsonFile::parse_json error path reaches JsonParser::new()/lint() (todo!()); the JSON linter is not yet ported"] fn test_parse_error_detect_unescaped_backslash() { let json = "{\n \"fo\\o\": \"bar\"\n}"; expect_parse_exception("Parse error on line 1", json); } #[test] -#[ignore = "JsonFile::parse_json error path reaches json_last_error (todo!()) in the php-shim"] +#[ignore = "JsonFile::parse_json error path reaches JsonParser::new()/lint() (todo!()); the JSON linter is not yet ported"] fn test_parse_error_skips_escaped_backslash() { let json = "{\n \"fo\\\\o\": \"bar\"\n \"a\": \"b\"\n}"; expect_parse_exception("Parse error on line 2", json); } #[test] -#[ignore = "JsonFile::parse_json error path reaches json_last_error (todo!()) in the php-shim"] +#[ignore = "JsonFile::parse_json error path reaches JsonParser::new()/lint() (todo!()); the JSON linter is not yet ported"] fn test_parse_error_detect_single_quotes() { let json = "{\n 'foo': \"bar\"\n}"; expect_parse_exception("Parse error on line 1", json); } #[test] -#[ignore = "JsonFile::parse_json error path reaches json_last_error (todo!()) in the php-shim"] +#[ignore = "JsonFile::parse_json error path reaches JsonParser::new()/lint() (todo!()); the JSON linter is not yet ported"] fn test_parse_error_detect_missing_quotes() { let json = "{\n foo: \"bar\"\n}"; expect_parse_exception("Parse error on line 1", json); } #[test] -#[ignore = "JsonFile::parse_json error path reaches json_last_error (todo!()) in the php-shim"] +#[ignore = "JsonFile::parse_json error path reaches JsonParser::new()/lint() (todo!()); the JSON linter is not yet ported"] fn test_parse_error_detect_array_as_hash() { let json = "{\n \"foo\": [\"bar\": \"baz\"]\n}"; expect_parse_exception("Parse error on line 2", json); } #[test] -#[ignore = "JsonFile::parse_json error path reaches json_last_error (todo!()) in the php-shim"] +#[ignore = "JsonFile::parse_json error path reaches JsonParser::new()/lint() (todo!()); the JSON linter is not yet ported"] fn test_parse_error_detect_missing_comma() { let json = "{\n \"foo\": \"bar\"\n \"bar\": \"foo\"\n}"; expect_parse_exception("Parse error on line 2", json); } #[test] -#[ignore = "JsonFile::parse_json error path reaches json_last_error (todo!()) in the php-shim"] +#[ignore = "JsonFile::parse_json error path reaches JsonParser::new()/lint() (todo!()); the JSON linter is not yet ported"] fn test_parse_error_detect_missing_comma_multiline() { let json = "{\n \"foo\": \"barbar\"\n\n \"bar\": \"foo\"\n}"; expect_parse_exception("Parse error on line 2", json); } #[test] -#[ignore = "JsonFile::parse_json error path reaches json_last_error (todo!()) in the php-shim"] +#[ignore = "JsonFile::parse_json error path reaches JsonParser::new()/lint() (todo!()); the JSON linter is not yet ported"] fn test_parse_error_detect_missing_colon() { let json = "{\n \"foo\": \"bar\",\n \"bar\" \"foo\"\n}"; expect_parse_exception("Parse error on line 3", json); |
