From 893fdfdeb086769539976cd5eeb743702c141c91 Mon Sep 17 00:00:00 2001 From: He-Pin Date: Wed, 24 Jun 2026 11:36:01 +0800 Subject: [PATCH 1/2] fix: std.format rejects boolean for numeric conversion codes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Motivation: sjsonnet silently coerced booleans to integers for numeric format codes (%d, %f, %e, %x, %o, %g etc.), treating true as 1 and false as 0. Both C++ jsonnet and jrsonnet correctly reject this with a type error. This is a permissiveness bug that can mask logic errors in Jsonnet programs. Modification: - Format.scala: Restrict Val.True/Val.False match arms to only allow %s conversion. All numeric conversion codes (%d, %i, %u, %o, %x, %X, %e, %E, %f, %F, %g, %G) now produce "expected number or string, got boolean". - Added error tests for %d and %f with boolean values. Result: "%d" % true now errors instead of silently returning "1". Cross-implementation comparison: | Expression | C++ jsonnet | go-jsonnet | jrsonnet | sjsonnet (before) | sjsonnet (after) | |-----------------|---------------------|------------|-----------------------|--------------------|------------------| | "%d" % true | ERROR: got boolean | ERROR | ERROR: got boolean | "1" ❌ | ERROR ✅ | | "%f" % false | ERROR | ERROR | ERROR | "0.000000" ❌ | ERROR ✅ | | "%x" % true | ERROR | ERROR | ERROR | "1" ❌ | ERROR ✅ | | "%e" % true | ERROR | ERROR | ERROR | "1.000000e+00" ❌ | ERROR ✅ | | "%s" % true | "true" | "true" | "true" | "true" ✅ | "true" ✅ | --- sjsonnet/src/sjsonnet/Format.scala | 32 ++++--------------- .../error.format_d_boolean.jsonnet | 1 + .../error.format_d_boolean.jsonnet.golden | 2 ++ .../error.format_f_boolean.jsonnet | 1 + .../error.format_f_boolean.jsonnet.golden | 2 ++ 5 files changed, 12 insertions(+), 26 deletions(-) create mode 100644 sjsonnet/test/resources/new_test_suite/error.format_d_boolean.jsonnet create mode 100644 sjsonnet/test/resources/new_test_suite/error.format_d_boolean.jsonnet.golden create mode 100644 sjsonnet/test/resources/new_test_suite/error.format_f_boolean.jsonnet create mode 100644 sjsonnet/test/resources/new_test_suite/error.format_f_boolean.jsonnet.golden diff --git a/sjsonnet/src/sjsonnet/Format.scala b/sjsonnet/src/sjsonnet/Format.scala index 99a9e1de..58dcdacf 100644 --- a/sjsonnet/src/sjsonnet/Format.scala +++ b/sjsonnet/src/sjsonnet/Format.scala @@ -725,41 +725,21 @@ object Format { ) } case _: Val.True => - val b = 1 formatted.conversion match { - case 'd' | 'i' | 'u' => formatInteger(formatted, b) - case 'o' => formatOctal(formatted, b) - case 'x' => formatHexadecimal(formatted, b) - case 'X' => formatHexadecimal(formatted, b).toUpperCase - case 'e' => formatExponent(formatted, b).toLowerCase - case 'E' => formatExponent(formatted, b) - case 'f' | 'F' => formatFloat(formatted, b) - case 'g' => formatGeneric(formatted, b).toLowerCase - case 'G' => formatGeneric(formatted, b) - case 'c' => - Error.fail("%c expected number or string, got boolean") case 's' => widenRaw(formatted, "true") - case _ => + case 'c' => + Error.fail("%c expected number or string, got boolean") + case _ => Error.fail( "expected number or string at position %d, got boolean".format(i) ) } case _: Val.False => - val b = 0 formatted.conversion match { - case 'd' | 'i' | 'u' => formatInteger(formatted, b) - case 'o' => formatOctal(formatted, b) - case 'x' => formatHexadecimal(formatted, b) - case 'X' => formatHexadecimal(formatted, b).toUpperCase - case 'e' => formatExponent(formatted, b).toLowerCase - case 'E' => formatExponent(formatted, b) - case 'f' | 'F' => formatFloat(formatted, b) - case 'g' => formatGeneric(formatted, b).toLowerCase - case 'G' => formatGeneric(formatted, b) - case 'c' => - Error.fail("%c expected number or string, got boolean") case 's' => widenRaw(formatted, "false") - case _ => + case 'c' => + Error.fail("%c expected number or string, got boolean") + case _ => Error.fail( "expected number or string at position %d, got boolean".format(i) ) diff --git a/sjsonnet/test/resources/new_test_suite/error.format_d_boolean.jsonnet b/sjsonnet/test/resources/new_test_suite/error.format_d_boolean.jsonnet new file mode 100644 index 00000000..956cc3db --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/error.format_d_boolean.jsonnet @@ -0,0 +1 @@ +"%d" % true diff --git a/sjsonnet/test/resources/new_test_suite/error.format_d_boolean.jsonnet.golden b/sjsonnet/test/resources/new_test_suite/error.format_d_boolean.jsonnet.golden new file mode 100644 index 00000000..5b469bea --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/error.format_d_boolean.jsonnet.golden @@ -0,0 +1,2 @@ +sjsonnet.Error: [std.format] expected number or string at position 0, got boolean + at [].(error.format_d_boolean.jsonnet:1:6) diff --git a/sjsonnet/test/resources/new_test_suite/error.format_f_boolean.jsonnet b/sjsonnet/test/resources/new_test_suite/error.format_f_boolean.jsonnet new file mode 100644 index 00000000..152d64e2 --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/error.format_f_boolean.jsonnet @@ -0,0 +1 @@ +"%f" % false diff --git a/sjsonnet/test/resources/new_test_suite/error.format_f_boolean.jsonnet.golden b/sjsonnet/test/resources/new_test_suite/error.format_f_boolean.jsonnet.golden new file mode 100644 index 00000000..3a56f65b --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/error.format_f_boolean.jsonnet.golden @@ -0,0 +1,2 @@ +sjsonnet.Error: [std.format] expected number or string at position 0, got boolean + at [].(error.format_f_boolean.jsonnet:1:6) From 894fe826f70803dcee936dfc4c95ff0f4b96840a Mon Sep 17 00:00:00 2001 From: He-Pin Date: Wed, 24 Jun 2026 12:18:07 +0800 Subject: [PATCH 2/2] test: add comprehensive format boolean regression tests (6 error + 1 success) --- .../new_test_suite/error.format_e_boolean.jsonnet | 1 + .../new_test_suite/error.format_e_boolean.jsonnet.golden | 2 ++ .../new_test_suite/error.format_g_boolean.jsonnet | 1 + .../new_test_suite/error.format_g_boolean.jsonnet.golden | 2 ++ .../new_test_suite/error.format_o_boolean.jsonnet | 1 + .../new_test_suite/error.format_o_boolean.jsonnet.golden | 2 ++ .../new_test_suite/error.format_x_boolean.jsonnet | 1 + .../new_test_suite/error.format_x_boolean.jsonnet.golden | 2 ++ .../new_test_suite/format_boolean_string_only.jsonnet | 9 +++++++++ .../format_boolean_string_only.jsonnet.golden | 1 + 10 files changed, 22 insertions(+) create mode 100644 sjsonnet/test/resources/new_test_suite/error.format_e_boolean.jsonnet create mode 100644 sjsonnet/test/resources/new_test_suite/error.format_e_boolean.jsonnet.golden create mode 100644 sjsonnet/test/resources/new_test_suite/error.format_g_boolean.jsonnet create mode 100644 sjsonnet/test/resources/new_test_suite/error.format_g_boolean.jsonnet.golden create mode 100644 sjsonnet/test/resources/new_test_suite/error.format_o_boolean.jsonnet create mode 100644 sjsonnet/test/resources/new_test_suite/error.format_o_boolean.jsonnet.golden create mode 100644 sjsonnet/test/resources/new_test_suite/error.format_x_boolean.jsonnet create mode 100644 sjsonnet/test/resources/new_test_suite/error.format_x_boolean.jsonnet.golden create mode 100644 sjsonnet/test/resources/new_test_suite/format_boolean_string_only.jsonnet create mode 100644 sjsonnet/test/resources/new_test_suite/format_boolean_string_only.jsonnet.golden diff --git a/sjsonnet/test/resources/new_test_suite/error.format_e_boolean.jsonnet b/sjsonnet/test/resources/new_test_suite/error.format_e_boolean.jsonnet new file mode 100644 index 00000000..c8d83ee1 --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/error.format_e_boolean.jsonnet @@ -0,0 +1 @@ +"%e" % true diff --git a/sjsonnet/test/resources/new_test_suite/error.format_e_boolean.jsonnet.golden b/sjsonnet/test/resources/new_test_suite/error.format_e_boolean.jsonnet.golden new file mode 100644 index 00000000..71a81f07 --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/error.format_e_boolean.jsonnet.golden @@ -0,0 +1,2 @@ +sjsonnet.Error: [std.format] expected number or string at position 0, got boolean + at [].(error.format_e_boolean.jsonnet:1:6) diff --git a/sjsonnet/test/resources/new_test_suite/error.format_g_boolean.jsonnet b/sjsonnet/test/resources/new_test_suite/error.format_g_boolean.jsonnet new file mode 100644 index 00000000..a7f4d5a8 --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/error.format_g_boolean.jsonnet @@ -0,0 +1 @@ +"%g" % true diff --git a/sjsonnet/test/resources/new_test_suite/error.format_g_boolean.jsonnet.golden b/sjsonnet/test/resources/new_test_suite/error.format_g_boolean.jsonnet.golden new file mode 100644 index 00000000..18635540 --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/error.format_g_boolean.jsonnet.golden @@ -0,0 +1,2 @@ +sjsonnet.Error: [std.format] expected number or string at position 0, got boolean + at [].(error.format_g_boolean.jsonnet:1:6) diff --git a/sjsonnet/test/resources/new_test_suite/error.format_o_boolean.jsonnet b/sjsonnet/test/resources/new_test_suite/error.format_o_boolean.jsonnet new file mode 100644 index 00000000..7084a080 --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/error.format_o_boolean.jsonnet @@ -0,0 +1 @@ +"%o" % true diff --git a/sjsonnet/test/resources/new_test_suite/error.format_o_boolean.jsonnet.golden b/sjsonnet/test/resources/new_test_suite/error.format_o_boolean.jsonnet.golden new file mode 100644 index 00000000..68524ca8 --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/error.format_o_boolean.jsonnet.golden @@ -0,0 +1,2 @@ +sjsonnet.Error: [std.format] expected number or string at position 0, got boolean + at [].(error.format_o_boolean.jsonnet:1:6) diff --git a/sjsonnet/test/resources/new_test_suite/error.format_x_boolean.jsonnet b/sjsonnet/test/resources/new_test_suite/error.format_x_boolean.jsonnet new file mode 100644 index 00000000..e7827689 --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/error.format_x_boolean.jsonnet @@ -0,0 +1 @@ +"%x" % true diff --git a/sjsonnet/test/resources/new_test_suite/error.format_x_boolean.jsonnet.golden b/sjsonnet/test/resources/new_test_suite/error.format_x_boolean.jsonnet.golden new file mode 100644 index 00000000..18821057 --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/error.format_x_boolean.jsonnet.golden @@ -0,0 +1,2 @@ +sjsonnet.Error: [std.format] expected number or string at position 0, got boolean + at [].(error.format_x_boolean.jsonnet:1:6) diff --git a/sjsonnet/test/resources/new_test_suite/format_boolean_string_only.jsonnet b/sjsonnet/test/resources/new_test_suite/format_boolean_string_only.jsonnet new file mode 100644 index 00000000..8ff86e9d --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/format_boolean_string_only.jsonnet @@ -0,0 +1,9 @@ +// Regression test: %s must still work with booleans after the type-error fix. +// Verified against cpp-jsonnet 0.21.0, go-jsonnet 0.22.0, jrsonnet 0.5.0-pre99. +std.assertEqual("%s" % true, "true") && +std.assertEqual("%s" % false, "false") && +std.assertEqual("%10s" % true, " true") && +std.assertEqual("%-10s" % false, "false ") && +std.assertEqual("val: %s" % true, "val: true") && +std.assertEqual("%s %s" % [true, false], "true false") && +true diff --git a/sjsonnet/test/resources/new_test_suite/format_boolean_string_only.jsonnet.golden b/sjsonnet/test/resources/new_test_suite/format_boolean_string_only.jsonnet.golden new file mode 100644 index 00000000..27ba77dd --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/format_boolean_string_only.jsonnet.golden @@ -0,0 +1 @@ +true