Skip to content

Commit ff2f64a

Browse files
authored
fix: convert format parse errors from Internal Error to user-facing messages (#1076)
Motivation: Jsonnet documents `std.format(str, vals)` as Python-style percent formatting, and documents the `%` operator as shorthand for `std.format`. Malformed percent-format specs are therefore user input errors, not interpreter failures. Before this PR, sjsonnet's format scanner/lowering path could throw plain JVM exceptions for malformed specs such as `%z`, a trailing `%`, or an unterminated named label. Those exceptions surfaced as `Internal Error` with Java stack traces, while Python and other Jsonnet implementations report user-facing format/runtime errors. Modification: - Add `parseFormatOrFail` around both cached format-string parsing and legacy lowered-format conversion. - Preserve existing sjsonnet `Error` values unchanged. - Convert `NonFatal` parser/lowering exceptions to `Error.fail(..., pos)` so malformed format specs are reported at the Jsonnet expression position. - Add regression tests for invalid conversions, truncated format specs, unterminated named labels, the `%` shorthand path, and representative successful formats. Result: Malformed format strings now produce user-facing sjsonnet errors instead of leaking Java stack traces. The `%` shorthand still uses the same formatting implementation; its diagnostic keeps the existing binary-operator stack shape, so it does not add a `[std.format]` builtin frame. The table below includes the old sjsonnet behavior from `master` before this PR. | Case | Python `%` formatting | C++ jsonnet observed | go-jsonnet observed | jrsonnet observed | sjsonnet before this PR | sjsonnet after this PR | |---|---|---|---|---|---|---| | `std.format("%z", [1])` | `ValueError: unsupported format character z` | `RUNTIME ERROR: Unrecognised conversion type: z` | `RUNTIME ERROR: Unrecognised conversion type: z` | `format error: unrecognized conversion type: z` | `[std.format] Internal Error`, caused by `Unrecognized conversion type: z` | `[std.format] Unrecognized conversion type: z` | | `std.format("hello %", [1])` | `ValueError: incomplete format` | `RUNTIME ERROR: Truncated format code.` | `RUNTIME ERROR: Truncated format code.` | `format error: truncated format code` | `[std.format] Internal Error`, caused by `Truncated format code at end of string` | `[std.format] Truncated format code at end of string` | | `std.format("%(key", {key: 1})` | `ValueError: incomplete format key` | `RUNTIME ERROR: Truncated format code.` | `RUNTIME ERROR: Truncated format code.` | `format error: truncated format code` | `[std.format] Internal Error`, caused by `Unterminated ( in format spec` | `[std.format] Unterminated ( in format spec` | | `"%z" % [1]` | same malformed `%z` error | `RUNTIME ERROR: Unrecognised conversion type: z` | `RUNTIME ERROR: Unrecognised conversion type: z` | `format error: unrecognized conversion type: z` | `Internal Error`, caused by `Unrecognized conversion type: z` | `Unrecognized conversion type: z` | References: - Official Jsonnet `std.format` documentation: https://jsonnet.org/ref/stdlib.html#std-format - Python printf-style string formatting: https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting
1 parent e686d89 commit ff2f64a

11 files changed

Lines changed: 30 additions & 2 deletions

sjsonnet/src/sjsonnet/Format.scala

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package sjsonnet
22

3+
import scala.util.control.NonFatal
4+
35
/**
46
* Minimal re-implementation of Python's `%` formatting logic, since Jsonnet's `%` formatter is
57
* basically "do whatever python does", with a link to:
@@ -273,7 +275,7 @@ object Format {
273275
}
274276

275277
def format(s: String, values0: Val, pos: Position)(implicit evaluator: EvalScope): Val.Str = {
276-
val parsed = parseFormatCached(s, evaluator.formatCache)
278+
val parsed = parseFormatOrFail(pos)(parseFormatCached(s, evaluator.formatCache))
277279
format(parsed, values0, pos)
278280
}
279281

@@ -558,9 +560,19 @@ object Format {
558560

559561
def format(leading: String, chunks: scala.Seq[(FormatSpec, String)], values0: Val, pos: Position)(
560562
implicit evaluator: EvalScope): Val.Str = {
561-
format(lowerParsedFormat((leading, chunks)), values0, pos)
563+
val parsed = parseFormatOrFail(pos)(lowerParsedFormat((leading, chunks)))
564+
format(parsed, values0, pos)
562565
}
563566

567+
private def parseFormatOrFail(pos: Position)(parse: => RuntimeFormat)(implicit
568+
evaluator: EvalErrorScope): RuntimeFormat =
569+
try parse
570+
catch {
571+
case e: Error => throw e
572+
case NonFatal(e) =>
573+
Error.fail(e.getMessage, pos)
574+
}
575+
564576
private def appendLeading(output: java.lang.StringBuilder, parsed: RuntimeFormat): Unit = {
565577
val source = parsed.source
566578
if (source != null) output.append(source, parsed.leadingStart, parsed.leadingEnd)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
std.format("%z", [1])
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
sjsonnet.Error: [std.format] Unrecognized conversion type: z
2+
at [<root>].(error.format_invalid_conversion.jsonnet:1:11)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"%z" % [1]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
sjsonnet.Error: Unrecognized conversion type: z
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
std.format("hello %", [])
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
sjsonnet.Error: [std.format] Truncated format code at end of string
2+
at [<root>].(error.format_truncated.jsonnet:1:11)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
std.format("%(key", { key: "value" })
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
sjsonnet.Error: [std.format] Unterminated ( in format spec
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
std.assertEqual(std.format("100%% done %s", ["yes"]), "100% done yes") &&
2+
std.assertEqual(std.format("%*s", [10, "hello"]), " hello") &&
3+
std.assertEqual(std.format("%.*f", [3, 3.14159]), "3.142") &&
4+
std.assertEqual(std.format("%#x", [255]), "0xff") &&
5+
true

0 commit comments

Comments
 (0)