Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions sjsonnet/src-jvm-native/sjsonnet/Config.scala
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,12 @@ final case class Config(
doc = "Enforce some additional syntax limitations"
)
strict: Flag = Flag(),
@arg(
name = "strict-format-boolean-conversions",
doc =
"Reject boolean values for numeric std.format/% conversion codes instead of coercing true/false to 1/0"
)
strictFormatBooleanConversions: Flag = Flag(),
@arg(
name = "yaml-out",
doc = "Write output as a YAML document"
Expand Down
1 change: 1 addition & 0 deletions sjsonnet/src-jvm-native/sjsonnet/SjsonnetMainBase.scala
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ object SjsonnetMainBase {
new Settings(
preserveOrder = config.preserveOrder.value,
strict = config.strict.value,
strictFormatBooleanConversions = config.strictFormatBooleanConversions.value,
throwErrorForInvalidSets = config.throwErrorForInvalidSets.value,
maxParserRecursionDepth = config.maxParserRecursionDepth,
brokenAssertionLogic = config.brokenAssertionLogic.value,
Expand Down
88 changes: 50 additions & 38 deletions sjsonnet/src/sjsonnet/Format.scala
Original file line number Diff line number Diff line change
Expand Up @@ -735,45 +735,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 _ =>
Error.fail(
"expected number or string at position %d, got boolean".format(i)
)
}
formatBoolean(
formatted,
i,
"true",
numericValue = 1,
evaluator.settings.strictFormatBooleanConversions
)
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 _ =>
Error.fail(
"expected number or string at position %d, got boolean".format(i)
)
}
formatBoolean(
formatted,
i,
"false",
numericValue = 0,
evaluator.settings.strictFormatBooleanConversions
)
case _: Val.Null =>
formatted.conversion match {
case 's' => widenRaw(formatted, "null")
Expand Down Expand Up @@ -812,6 +788,42 @@ object Format {
if (resultAsciiSafe) Val.Str.asciiSafe(pos, resultStr) else Val.Str(pos, resultStr)
}

private def formatBoolean(
formatted: FormatSpec,
index: Int,
textValue: String,
numericValue: Int,
strict: Boolean): String =
if (strict) formatStrictBoolean(formatted, index, textValue)
else
formatted.conversion match {
case 'd' | 'i' | 'u' => formatInteger(formatted, numericValue)
case 'o' => formatOctal(formatted, numericValue)
case 'x' => formatHexadecimal(formatted, numericValue)
case 'X' => formatHexadecimal(formatted, numericValue).toUpperCase
case 'e' => formatExponent(formatted, numericValue).toLowerCase
case 'E' => formatExponent(formatted, numericValue)
case 'f' | 'F' => formatFloat(formatted, numericValue)
case 'g' => formatGeneric(formatted, numericValue).toLowerCase
case 'G' => formatGeneric(formatted, numericValue)
case 'c' => Error.fail("%c expected number or string, got boolean")
case 's' => widenRaw(formatted, textValue)
case _ =>
Error.fail(
"expected number or string at position %d, got boolean".format(index)
)
}

private def formatStrictBoolean(formatted: FormatSpec, index: Int, value: String): String =
formatted.conversion match {
case 's' => widenRaw(formatted, value)
case 'c' => Error.fail("%c expected number or string, got boolean")
case _ =>
Error.fail(
"expected number or string at position %d, got boolean".format(index)
)
}

/**
* Super-fast path for format strings where ALL specs are simple `%(key)s` with a `Val.Obj`. This
* avoids per-spec pattern matching, widenRaw overhead, and caches repeated key lookups. For the
Expand Down
3 changes: 2 additions & 1 deletion sjsonnet/src/sjsonnet/Settings.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ final case class Settings(
brokenAssertionLogic: Boolean = false,
maxMaterializeDepth: Int = 1000,
materializeRecursiveDepthLimit: Int = 128,
maxStack: Int = 500
maxStack: Int = 500,
strictFormatBooleanConversions: Boolean = false
)

object Settings {
Expand Down
17 changes: 17 additions & 0 deletions sjsonnet/test/src-jvm/sjsonnet/MainTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,23 @@ object MainTests extends TestSuite {
assert(!err.contains("sjsonnet debug stats"))
}

test("strictFormatBooleanConversions") {
val (defaultRes, defaultOut, defaultErr) = runMain("'%d' % true", "--exec")
assert(defaultRes == 0)
assert(defaultOut.trim == "\"1\"")
assert(defaultErr.isEmpty)

val (strictRes, strictOut, strictErr) =
runMain("'%d' % true", "--exec", "--strict-format-boolean-conversions")
assert(strictRes == 1)
assert(strictOut.isEmpty)
assert(
normalizeOutput(strictErr).contains(
"sjsonnet.Error: [std.format] expected number or string at position 0, got boolean"
)
)
}

test("jsonnetPathReverseJpathsPriority") {
// With --reverse-jpaths-priority, rightmost -J wins, but -J still beats JSONNET_PATH
val libDirEnv = os.temp.dir()
Expand Down
24 changes: 24 additions & 0 deletions sjsonnet/test/src/sjsonnet/EvaluatorTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -725,6 +725,30 @@ object EvaluatorTests extends TestSuite {
) ==> "sjsonnet.Error: [std.format] %c expected number or string, got null\nat [<root>].(:1:6)"
eval("'%s' % null") ==> ujson.Str("null")
}
test("formatBooleanNumericConversionsDefault") {
eval("'%d' % true") ==> ujson.Str("1")
eval("'%f' % false") ==> ujson.Str("0.000000")
eval("'%x' % true") ==> ujson.Str("1")
eval("'%s' % true") ==> ujson.Str("true")
}
test("formatBooleanNumericConversionsStrict") {
evalErr(
"'%d' % true",
strictFormatBooleanConversions = true
) ==> "sjsonnet.Error: [std.format] expected number or string at position 0, got boolean\nat [<root>].(:1:6)"
evalErr(
"'%f' % false",
strictFormatBooleanConversions = true
) ==> "sjsonnet.Error: [std.format] expected number or string at position 0, got boolean\nat [<root>].(:1:6)"
evalErr(
"'%x' % true",
strictFormatBooleanConversions = true
) ==> "sjsonnet.Error: [std.format] expected number or string at position 0, got boolean\nat [<root>].(:1:6)"
eval(
"'%s' % true",
strictFormatBooleanConversions = true
) ==> ujson.Str("true")
}
test("formatTypeErrorMessages") {
evalErr(
"'%a' % 42"
Expand Down
16 changes: 11 additions & 5 deletions sjsonnet/test/src/sjsonnet/TestUtils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ object TestUtils {
strict: Boolean = false,
brokenAssertionLogic: Boolean = false,
maxStack: Int = 500,
std: sjsonnet.stdlib.StdLibModule = sjsonnet.stdlib.StdLibModule.Default)
std: sjsonnet.stdlib.StdLibModule = sjsonnet.stdlib.StdLibModule.Default,
strictFormatBooleanConversions: Boolean = false)
: Either[String, Value] = {
new Interpreter(
Map(),
Expand All @@ -20,6 +21,7 @@ object TestUtils {
new Settings(
preserveOrder = preserveOrder,
strict = strict,
strictFormatBooleanConversions = strictFormatBooleanConversions,
throwErrorForInvalidSets = true,
brokenAssertionLogic = brokenAssertionLogic,
maxStack = maxStack
Expand All @@ -34,14 +36,16 @@ object TestUtils {
strict: Boolean = false,
brokenAssertionLogic: Boolean = false,
maxStack: Int = 500,
std: sjsonnet.stdlib.StdLibModule = sjsonnet.stdlib.StdLibModule.Default): Value = {
std: sjsonnet.stdlib.StdLibModule = sjsonnet.stdlib.StdLibModule.Default,
strictFormatBooleanConversions: Boolean = false): Value = {
eval0(
s,
preserveOrder,
strict,
brokenAssertionLogic,
maxStack,
std
std,
strictFormatBooleanConversions
) match {
case Right(x) => x
case Left(e) => throw new Exception(e)
Expand All @@ -54,14 +58,16 @@ object TestUtils {
strict: Boolean = false,
brokenAssertionLogic: Boolean = false,
maxStack: Int = 500,
std: sjsonnet.stdlib.StdLibModule = sjsonnet.stdlib.StdLibModule.Default): String = {
std: sjsonnet.stdlib.StdLibModule = sjsonnet.stdlib.StdLibModule.Default,
strictFormatBooleanConversions: Boolean = false): String = {
eval0(
s,
preserveOrder,
strict,
brokenAssertionLogic,
maxStack,
std
std,
strictFormatBooleanConversions
) match {
case Left(err) =>
err.split('\n').map(_.trim).mkString("\n") // normalize inconsistent indenation on JVM vs JS
Expand Down
Loading